mdas-jsview-sdk 1.0.21-uat.0 → 1.0.26-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
  /* ========================================
@@ -2800,8 +2857,36 @@ const TimeSalesStyles = `
2800
2857
  }
2801
2858
  `;
2802
2859
 
2860
+ /**
2861
+ * @deprecated SharedStyles is deprecated and will be removed in a future version.
2862
+ *
2863
+ * Please migrate to BaseStyles + CommonWidgetPatterns:
2864
+ *
2865
+ * BEFORE:
2866
+ * import { SharedStyles } from './styles/index.js';
2867
+ * export const MyWidgetStyles = `${SharedStyles} ...widget styles...`;
2868
+ *
2869
+ * AFTER:
2870
+ * import { BaseStyles } from './styles/BaseStyles.js';
2871
+ * import { getLoadingOverlayStyles, getErrorStyles } from './styles/CommonWidgetPatterns.js';
2872
+ * export const MyWidgetStyles = `
2873
+ * ${BaseStyles}
2874
+ * ${getLoadingOverlayStyles('my-widget')}
2875
+ * ${getErrorStyles('my-widget')}
2876
+ * ...widget-specific styles...
2877
+ * `;
2878
+ *
2879
+ * Benefits of migration:
2880
+ * - Proper CSS scoping (no global conflicts)
2881
+ * - Reduced code duplication
2882
+ * - Better maintainability
2883
+ * - Smaller bundle size
2884
+ *
2885
+ * See STYLING_CUSTOMIZATION_GUIDE.md for details.
2886
+ */
2803
2887
  const SharedStyles = `
2804
2888
  /* ========================================
2889
+ ⚠️ DEPRECATED - Use BaseStyles.js instead
2805
2890
  FONT SIZE SYSTEM - CSS Variables
2806
2891
  Adjust --mdas-base-font-size to scale all fonts
2807
2892
  ======================================== */
@@ -4477,99 +4562,46 @@ class MarketDataWidget extends BaseWidget {
4477
4562
  this.showLoading();
4478
4563
 
4479
4564
  // 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;
4565
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
4566
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
4567
+ // Store for use in updateWidget
4568
+ if (data && data[0]) {
4569
+ this.initialValidationData = data[0];
4491
4570
  }
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
- }
4504
- }
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
4571
+ });
4572
+
4573
+ // If validation failed due to access/permission issues, stop initialization
4574
+ if (validationSuccess === false) {
4575
+ return; // Error is already shown, don't continue
4510
4576
  }
4577
+ this.subscribeToData();
4511
4578
  }
4512
4579
  subscribeToData() {
4513
4580
  // Subscribe with symbol for routing
4514
4581
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
4515
4582
  );
4516
-
4517
- // Send subscription message
4518
- /* this.wsManager.send({
4519
- type: 'queryl1',
4520
- symbol: this.symbol
4521
- }); */
4522
4583
  }
4584
+ handleData(message) {
4585
+ //console.log('DEBUG', message)
4523
4586
 
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');
4587
+ // Check for error: false and display message if present
4588
+ if (message.error === true) {
4589
+ if (this.debug) {
4590
+ console.log('[MarketDataWidget] Received error response:', message.message);
4551
4591
  }
4552
- } */
4592
+ const errorMsg = message.message || message.Message;
4553
4593
 
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');
4594
+ // If there's a message field, show it as an error
4595
+ if (errorMsg) {
4596
+ this.hideLoading();
4597
+ this.showError(errorMsg);
4598
+ return;
4568
4599
  }
4569
- } */
4570
-
4571
- handleData(message) {
4572
- console.log('DEBUG', message);
4600
+ } else if (message.Data && typeof message.Data === 'string') {
4601
+ this.hideLoading();
4602
+ this.showError(message.Message);
4603
+ return;
4604
+ }
4573
4605
  if (this.loadingTimeout) {
4574
4606
  clearTimeout(this.loadingTimeout);
4575
4607
  this.loadingTimeout = null;
@@ -4655,126 +4687,6 @@ class MarketDataWidget extends BaseWidget {
4655
4687
  this.showConnectionQuality(); // Show cache indicator
4656
4688
  }
4657
4689
  }
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
4690
  updateWidget(data) {
4779
4691
  if (this.isDestroyed) return;
4780
4692
  try {
@@ -5279,7 +5191,20 @@ class NightSessionWidget extends BaseWidget {
5279
5191
  this.showLoading();
5280
5192
 
5281
5193
  // Validate initial symbol via API to get company info
5282
- await this.validateInitialSymbol();
5194
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
5195
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
5196
+ // Extract company info from first data item
5197
+ if (data && data[0]) {
5198
+ this.companyName = data[0].comp_name || '';
5199
+ this.exchangeName = data[0].market_name || '';
5200
+ this.mic = data[0].mic || '';
5201
+ }
5202
+ });
5203
+
5204
+ // If validation failed due to access/permission issues, stop initialization
5205
+ if (validationSuccess === false) {
5206
+ return; // Error is already shown, don't continue
5207
+ }
5283
5208
 
5284
5209
  // Set timeout to detect no data on initial load
5285
5210
  this.loadingTimeout = setTimeout(() => {
@@ -5296,38 +5221,6 @@ class NightSessionWidget extends BaseWidget {
5296
5221
 
5297
5222
  this.subscribeToData();
5298
5223
  }
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
5224
  subscribeToData() {
5332
5225
  let subscriptionType;
5333
5226
  if (this.source === 'bruce') {
@@ -5347,6 +5240,7 @@ class NightSessionWidget extends BaseWidget {
5347
5240
  }
5348
5241
  handleData(message) {
5349
5242
  console.log('DEBUG NIGHT', message);
5243
+
5350
5244
  //message = message.data || message.Data
5351
5245
 
5352
5246
  if (this.loadingTimeout) {
@@ -5354,64 +5248,41 @@ class NightSessionWidget extends BaseWidget {
5354
5248
  this.loadingTimeout = null;
5355
5249
  }
5356
5250
 
5357
- // Handle error messages from server (plain text converted to structured format)
5358
- if (message.type === 'error' || message.error == true) {
5251
+ // Check for error: false and display message if present
5252
+ if (message.error === true) {
5359
5253
  if (this.debug) {
5360
- console.log('[NightSessionWidget] Received no data message:', message.message);
5254
+ console.log('[Nigh] Received error response:', message.message);
5361
5255
  }
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 {
5256
+ const errorMsg = message.message || message.Message;
5257
+
5258
+ // If there's a message field, show it as an error
5259
+ if (errorMsg) {
5370
5260
  this.hideLoading();
5371
- if (this.debug) {
5372
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5373
- }
5261
+ this.showError(errorMsg);
5262
+ return;
5374
5263
  }
5264
+ } else if (message.Data && typeof message.Data === 'string') {
5265
+ this.hideLoading();
5266
+ this.showError(message.Message);
5375
5267
  return;
5376
5268
  }
5377
5269
 
5378
- // Handle general error messages - CHECK IF IT'S ACCESS RELATED
5270
+ // Handle error messages from server (plain text converted to structured format)
5379
5271
  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
- }
5272
+ if (this.debug) {
5273
+ console.log('[NightSessionWidget] Received no data message:', message.message);
5274
+ }
5275
+ // Only show no data state if we don't have cached data
5276
+ if (!this.data) {
5277
+ this.showError(errorMsg);
5398
5278
  } 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
- }
5279
+ this.hideLoading();
5280
+ if (this.debug) {
5281
+ console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
5408
5282
  }
5409
5283
  }
5410
5284
  return;
5411
5285
  }
5412
-
5413
- // Filter for night session data
5414
-
5415
5286
  if (Array.isArray(message.data)) {
5416
5287
  // First, try to find data matching our symbol regardless of MarketName
5417
5288
  const symbolData = message.data.find(item => item.Symbol === this.symbol);
@@ -6312,36 +6183,20 @@ class OptionsWidget extends BaseWidget {
6312
6183
  this.showLoading();
6313
6184
 
6314
6185
  // 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;
6326
- }
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
- }
6186
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
6187
+ const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
6188
+ // Store for use in updateWidget
6189
+ // Note: quoteOptionl1 returns array directly, not wrapped in data field
6190
+ if (data && data[0] && !data[0].error && !data[0].not_found) {
6191
+ this.initialValidationData = data[0];
6338
6192
  }
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
6193
+ });
6194
+
6195
+ // If validation failed due to access/permission issues, stop initialization
6196
+ if (validationSuccess === false) {
6197
+ return; // Error is already shown, don't continue
6344
6198
  }
6199
+ this.subscribeToData();
6345
6200
  }
6346
6201
  subscribeToData() {
6347
6202
  // Subscribe with symbol for routing
@@ -6811,6 +6666,15 @@ const OptionChainTemplate = `
6811
6666
  </select>
6812
6667
  <button class="fetch-button" disabled>Search</button>
6813
6668
  </div>
6669
+ <div class="filter-section">
6670
+ <label for="strike-filter" class="filter-label">Display:</label>
6671
+ <select class="strike-filter" id="strike-filter">
6672
+ <option value="5">Near the money (±5 strikes)</option>
6673
+ <option value="10">Near the money (±10 strikes)</option>
6674
+ <option value="15">Near the money (±15 strikes)</option>
6675
+ <option value="all">All strikes</option>
6676
+ </select>
6677
+ </div>
6814
6678
  </div>
6815
6679
 
6816
6680
  <!-- Data Grid Section -->
@@ -6867,11 +6731,383 @@ const OptionChainTemplate = `
6867
6731
  </div>
6868
6732
  `;
6869
6733
 
6734
+ // src/widgets/styles/BaseStyles.js
6735
+
6736
+ /**
6737
+ * Base Styles - CSS Variables and Universal Utilities
6738
+ *
6739
+ * This file contains:
6740
+ * 1. CSS Custom Properties (variables) for consistent theming
6741
+ * 2. Responsive font sizing system
6742
+ * 3. Optional utility classes
6743
+ *
6744
+ * Usage:
6745
+ * import { BaseStyles } from './styles/BaseStyles';
6746
+ * export const MyWidgetStyles = `${BaseStyles} ...widget styles...`;
6747
+ */
6748
+
6749
+ const BaseStyles = `
6750
+ /* ============================================
6751
+ MDAS WIDGET CSS VARIABLES
6752
+ ============================================ */
6753
+
6754
+ :root {
6755
+ /* --- FONT SIZE SYSTEM --- */
6756
+ /* Base font size - all other sizes are relative to this */
6757
+ --mdas-base-font-size: 14px;
6758
+
6759
+ /* Component-specific font sizes (em units scale with base) */
6760
+ --mdas-small-text-size: 0.79em; /* 11px at 14px base */
6761
+ --mdas-medium-text-size: 0.93em; /* 13px at 14px base */
6762
+ --mdas-large-text-size: 1.14em; /* 16px at 14px base */
6763
+
6764
+ /* Widget element sizes */
6765
+ --mdas-company-name-size: 1.43em; /* 20px at 14px base */
6766
+ --mdas-symbol-size: 1.79em; /* 25px at 14px base */
6767
+ --mdas-price-size: 2.29em; /* 32px at 14px base */
6768
+ --mdas-change-size: 1.14em; /* 16px at 14px base */
6769
+
6770
+ /* Data display sizes */
6771
+ --mdas-label-size: 0.86em; /* 12px at 14px base */
6772
+ --mdas-value-size: 1em; /* 14px at 14px base */
6773
+ --mdas-footer-size: 0.79em; /* 11px at 14px base */
6774
+
6775
+ /* Chart-specific sizes */
6776
+ --mdas-chart-title-size: 1.29em; /* 18px at 14px base */
6777
+ --mdas-chart-label-size: 0.93em; /* 13px at 14px base */
6778
+ --mdas-chart-value-size: 1.43em; /* 20px at 14px base */
6779
+
6780
+ /* --- COLOR PALETTE (for future theming support) --- */
6781
+ /* Primary colors */
6782
+ --mdas-primary-color: #3b82f6;
6783
+ --mdas-primary-hover: #2563eb;
6784
+
6785
+ /* Status colors */
6786
+ --mdas-color-positive: #059669;
6787
+ --mdas-color-positive-bg: #d1fae5;
6788
+ --mdas-color-negative: #dc2626;
6789
+ --mdas-color-negative-bg: #fee2e2;
6790
+ --mdas-color-neutral: #6b7280;
6791
+
6792
+ /* Background colors */
6793
+ --mdas-bg-primary: #ffffff;
6794
+ --mdas-bg-secondary: #f9fafb;
6795
+ --mdas-bg-tertiary: #f3f4f6;
6796
+
6797
+ /* Text colors */
6798
+ --mdas-text-primary: #111827;
6799
+ --mdas-text-secondary: #6b7280;
6800
+ --mdas-text-tertiary: #9ca3af;
6801
+
6802
+ /* Border colors */
6803
+ --mdas-border-primary: #e5e7eb;
6804
+ --mdas-border-secondary: #d1d5db;
6805
+
6806
+ /* --- SPACING SYSTEM --- */
6807
+ --mdas-spacing-xs: 4px;
6808
+ --mdas-spacing-sm: 8px;
6809
+ --mdas-spacing-md: 16px;
6810
+ --mdas-spacing-lg: 24px;
6811
+ --mdas-spacing-xl: 32px;
6812
+
6813
+ /* --- EFFECTS --- */
6814
+ --mdas-border-radius: 8px;
6815
+ --mdas-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
6816
+ --mdas-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
6817
+ --mdas-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
6818
+
6819
+ /* --- Z-INDEX LAYERS --- */
6820
+ --mdas-z-base: 1;
6821
+ --mdas-z-sticky: 10;
6822
+ --mdas-z-overlay: 100;
6823
+ --mdas-z-modal: 10000;
6824
+ }
6825
+
6826
+ /* ============================================
6827
+ RESPONSIVE FONT SCALING
6828
+ ============================================ */
6829
+
6830
+ /* Tablet breakpoint - slightly smaller fonts */
6831
+ @media (max-width: 768px) {
6832
+ :root {
6833
+ --mdas-base-font-size: 13px;
6834
+ }
6835
+ }
6836
+
6837
+ /* Mobile breakpoint - smaller fonts for small screens */
6838
+ @media (max-width: 480px) {
6839
+ :root {
6840
+ --mdas-base-font-size: 12px;
6841
+ }
6842
+ }
6843
+
6844
+ /* ============================================
6845
+ OPTIONAL UTILITY CLASSES
6846
+ Use these classes for consistent styling
6847
+ ============================================ */
6848
+
6849
+ /* Widget card styling - apply to widget root element */
6850
+ .mdas-card {
6851
+ background: var(--mdas-bg-primary);
6852
+ border-radius: var(--mdas-border-radius);
6853
+ padding: var(--mdas-spacing-lg);
6854
+ box-shadow: var(--mdas-shadow-md);
6855
+ border: 1px solid var(--mdas-border-primary);
6856
+ }
6857
+
6858
+ /* Responsive container */
6859
+ .mdas-container {
6860
+ width: 100%;
6861
+ max-width: 1400px;
6862
+ margin: 0 auto;
6863
+ }
6864
+
6865
+ /* Text utilities */
6866
+ .mdas-text-primary {
6867
+ color: var(--mdas-text-primary);
6868
+ }
6869
+
6870
+ .mdas-text-secondary {
6871
+ color: var(--mdas-text-secondary);
6872
+ }
6873
+
6874
+ .mdas-text-muted {
6875
+ color: var(--mdas-text-tertiary);
6876
+ }
6877
+
6878
+ /* Status colors */
6879
+ .mdas-positive {
6880
+ color: var(--mdas-color-positive);
6881
+ }
6882
+
6883
+ .mdas-negative {
6884
+ color: var(--mdas-color-negative);
6885
+ }
6886
+
6887
+ .mdas-neutral {
6888
+ color: var(--mdas-color-neutral);
6889
+ }
6890
+ `;
6891
+
6892
+ // src/widgets/styles/CommonWidgetPatterns.js
6893
+
6894
+ /**
6895
+ * Common Widget Patterns - Reusable Style Functions
6896
+ *
6897
+ * These functions generate properly scoped CSS for common widget patterns.
6898
+ * Each function takes a widget class name and returns scoped CSS.
6899
+ *
6900
+ * Benefits:
6901
+ * - Eliminates code duplication
6902
+ * - Ensures proper scoping (no global conflicts)
6903
+ * - Consistent styling across widgets
6904
+ * - Easy to maintain and update
6905
+ *
6906
+ * Usage:
6907
+ * import { getLoadingOverlayStyles } from './CommonWidgetPatterns';
6908
+ * const styles = `${getLoadingOverlayStyles('my-widget')} ...other styles...`;
6909
+ */
6910
+
6911
+ /**
6912
+ * Generate loading overlay styles for a widget
6913
+ * @param {string} widgetClass - Widget class name (e.g., 'option-chain-widget')
6914
+ * @returns {string} Scoped CSS for loading overlay
6915
+ */
6916
+ const getLoadingOverlayStyles = widgetClass => `
6917
+ /* Loading Overlay for ${widgetClass} */
6918
+ .${widgetClass} .widget-loading-overlay {
6919
+ position: absolute;
6920
+ top: 0;
6921
+ left: 0;
6922
+ right: 0;
6923
+ bottom: 0;
6924
+ background: rgba(255, 255, 255, 0.95);
6925
+ backdrop-filter: blur(2px);
6926
+ display: flex;
6927
+ flex-direction: column;
6928
+ align-items: center;
6929
+ justify-content: center;
6930
+ z-index: var(--mdas-z-overlay, 100);
6931
+ border-radius: var(--mdas-border-radius, 12px);
6932
+ }
6933
+
6934
+ .${widgetClass} .widget-loading-overlay.hidden {
6935
+ display: none;
6936
+ }
6937
+
6938
+ .${widgetClass} .loading-content {
6939
+ display: flex;
6940
+ flex-direction: column;
6941
+ align-items: center;
6942
+ gap: 12px;
6943
+ }
6944
+
6945
+ .${widgetClass} .loading-spinner {
6946
+ width: 40px;
6947
+ height: 40px;
6948
+ border: 4px solid var(--mdas-border-primary, #e5e7eb);
6949
+ border-top-color: var(--mdas-primary-color, #3b82f6);
6950
+ border-radius: 50%;
6951
+ animation: ${widgetClass}-spin 1s linear infinite;
6952
+ }
6953
+
6954
+ .${widgetClass} .loading-text {
6955
+ color: var(--mdas-text-secondary, #6b7280);
6956
+ font-size: var(--mdas-medium-text-size, 0.93em);
6957
+ font-weight: 500;
6958
+ }
6959
+
6960
+ @keyframes ${widgetClass}-spin {
6961
+ to { transform: rotate(360deg); }
6962
+ }
6963
+ `;
6964
+
6965
+ /**
6966
+ * Generate widget error display styles
6967
+ * @param {string} widgetClass - Widget class name
6968
+ * @returns {string} Scoped CSS for error display
6969
+ */
6970
+ const getErrorStyles = widgetClass => `
6971
+ /* Error Display for ${widgetClass} */
6972
+ .${widgetClass} .widget-error {
6973
+ padding: 16px 20px;
6974
+ background: var(--mdas-color-negative-bg, #fee2e2);
6975
+ border: 1px solid #fecaca;
6976
+ border-radius: var(--mdas-border-radius, 8px);
6977
+ color: var(--mdas-color-negative, #dc2626);
6978
+ font-size: var(--mdas-medium-text-size, 0.93em);
6979
+ margin: 16px;
6980
+ text-align: center;
6981
+ }
6982
+
6983
+ .${widgetClass} .widget-error-container {
6984
+ display: flex;
6985
+ flex-direction: column;
6986
+ align-items: center;
6987
+ gap: 12px;
6988
+ padding: 24px;
6989
+ }
6990
+ `;
6991
+
6992
+ /**
6993
+ * Generate widget footer styles
6994
+ * @param {string} widgetClass - Widget class name
6995
+ * @returns {string} Scoped CSS for widget footer
6996
+ */
6997
+ const getFooterStyles = widgetClass => `
6998
+ /* Footer for ${widgetClass} */
6999
+ .${widgetClass} .widget-footer {
7000
+ display: flex;
7001
+ justify-content: space-between;
7002
+ align-items: center;
7003
+ padding: 8px 16px;
7004
+ background: var(--mdas-bg-secondary, #f9fafb);
7005
+ border-top: 1px solid var(--mdas-border-primary, #e5e7eb);
7006
+ font-size: var(--mdas-footer-size, 0.79em);
7007
+ color: var(--mdas-text-secondary, #6b7280);
7008
+ }
7009
+
7010
+ .${widgetClass} .widget-footer .last-update {
7011
+ color: var(--mdas-text-tertiary, #9ca3af);
7012
+ }
7013
+
7014
+ .${widgetClass} .widget-footer .data-source {
7015
+ color: var(--mdas-text-secondary, #6b7280);
7016
+ font-weight: 500;
7017
+ }
7018
+ `;
7019
+
7020
+ /**
7021
+ * Generate no-data state styles
7022
+ * @param {string} widgetClass - Widget class name
7023
+ * @returns {string} Scoped CSS for no-data state
7024
+ */
7025
+ const getNoDataStyles = widgetClass => `
7026
+ /* No Data State for ${widgetClass} */
7027
+ .${widgetClass} .no-data-state {
7028
+ display: flex;
7029
+ flex-direction: column;
7030
+ align-items: center;
7031
+ justify-content: center;
7032
+ padding: 40px 20px;
7033
+ text-align: center;
7034
+ color: var(--mdas-text-secondary, #6b7280);
7035
+ }
7036
+
7037
+ .${widgetClass} .no-data-content {
7038
+ max-width: 400px;
7039
+ display: flex;
7040
+ flex-direction: column;
7041
+ align-items: center;
7042
+ gap: 16px;
7043
+ }
7044
+
7045
+ .${widgetClass} .no-data-icon {
7046
+ width: 64px;
7047
+ height: 64px;
7048
+ display: flex;
7049
+ align-items: center;
7050
+ justify-content: center;
7051
+ opacity: 0.5;
7052
+ }
7053
+
7054
+ .${widgetClass} .no-data-icon svg {
7055
+ width: 100%;
7056
+ height: 100%;
7057
+ }
7058
+
7059
+ .${widgetClass} .no-data-title {
7060
+ font-size: var(--mdas-large-text-size, 1.14em);
7061
+ font-weight: 600;
7062
+ color: var(--mdas-text-primary, #111827);
7063
+ margin: 0;
7064
+ }
7065
+
7066
+ .${widgetClass} .no-data-description {
7067
+ font-size: var(--mdas-medium-text-size, 0.93em);
7068
+ color: var(--mdas-text-secondary, #6b7280);
7069
+ margin: 0;
7070
+ line-height: 1.5;
7071
+ }
7072
+
7073
+ .${widgetClass} .no-data-description strong {
7074
+ color: var(--mdas-text-primary, #111827);
7075
+ font-weight: 600;
7076
+ }
7077
+
7078
+ .${widgetClass} .no-data-guidance {
7079
+ font-size: var(--mdas-small-text-size, 0.79em);
7080
+ color: var(--mdas-text-tertiary, #9ca3af);
7081
+ margin: 0;
7082
+ }
7083
+
7084
+ /* Error variant of no-data state */
7085
+ .${widgetClass} .no-data-state.error-access {
7086
+ color: var(--mdas-color-negative, #dc2626);
7087
+ }
7088
+
7089
+ .${widgetClass} .no-data-state.error-access .no-data-title {
7090
+ color: var(--mdas-color-negative, #dc2626);
7091
+ }
7092
+
7093
+ .${widgetClass} .no-data-state.error-access .no-data-icon {
7094
+ opacity: 0.8;
7095
+ }
7096
+
7097
+ .${widgetClass} .no-data-state.error-access .no-data-icon svg circle[fill] {
7098
+ fill: var(--mdas-color-negative, #dc2626);
7099
+ }
7100
+ `;
7101
+
6870
7102
  // src/widgets/styles/OptionChainStyles.js
6871
7103
  const OptionChainStyles = `
6872
- ${SharedStyles}
7104
+ ${BaseStyles}
7105
+ ${getLoadingOverlayStyles('option-chain-widget')}
7106
+ ${getErrorStyles('option-chain-widget')}
7107
+ ${getFooterStyles('option-chain-widget')}
7108
+ ${getNoDataStyles('option-chain-widget')}
6873
7109
 
6874
- /* Base styles remain the same until responsive section */
7110
+ /* Option Chain Widget Specific Styles */
6875
7111
  .option-chain-widget {
6876
7112
  border: 1px solid #e5e7eb;
6877
7113
  border-radius: 8px;
@@ -6896,8 +7132,22 @@ ${SharedStyles}
6896
7132
  flex-wrap: wrap;
6897
7133
  }
6898
7134
 
7135
+ .option-chain-widget .filter-section {
7136
+ display: flex;
7137
+ gap: 8px;
7138
+ align-items: center;
7139
+ margin-top: 8px;
7140
+ }
7141
+
7142
+ .option-chain-widget .filter-label {
7143
+ font-size: 13px;
7144
+ font-weight: 500;
7145
+ color: #374151;
7146
+ }
7147
+
6899
7148
  .option-chain-widget .symbol-input,
6900
- .option-chain-widget .date-select {
7149
+ .option-chain-widget .date-select,
7150
+ .option-chain-widget .strike-filter {
6901
7151
  padding: 8px 12px;
6902
7152
  border: 1px solid #d1d5db;
6903
7153
  border-radius: 4px;
@@ -6918,6 +7168,12 @@ ${SharedStyles}
6918
7168
  background: white;
6919
7169
  }
6920
7170
 
7171
+ .option-chain-widget .strike-filter {
7172
+ min-width: 200px;
7173
+ background: white;
7174
+ cursor: pointer;
7175
+ }
7176
+
6921
7177
  .option-chain-widget .fetch-button {
6922
7178
  padding: 8px 16px;
6923
7179
  background: #3b82f6;
@@ -7073,10 +7329,22 @@ ${SharedStyles}
7073
7329
  min-height: 32px;
7074
7330
  }
7075
7331
 
7332
+ /* In-the-money highlighting */
7333
+ .option-chain-widget .calls-data.in-the-money span,
7334
+ .option-chain-widget .puts-data.in-the-money span {
7335
+ background-color: #fffbeb;
7336
+ }
7337
+
7076
7338
  .option-chain-widget .option-row:hover {
7077
7339
  background: #f9fafb;
7078
7340
  }
7079
- .option-chain-widget .option-row:hover .calls-data span,
7341
+
7342
+ .option-chain-widget .option-row:hover .calls-data.in-the-money span,
7343
+ .option-chain-widget .option-row:hover .puts-data.in-the-money span {
7344
+ background-color: #fef3c7;
7345
+ }
7346
+
7347
+ .option-chain-widget .option-row:hover .calls-data span,
7080
7348
  .option-chain-widget .option-row:hover .puts-data span,
7081
7349
  .option-chain-widget .option-row:hover .strike-data {
7082
7350
  background: #f9fafb;
@@ -7100,14 +7368,32 @@ ${SharedStyles}
7100
7368
  .option-chain-widget .puts-data span {
7101
7369
  padding: 6px 4px;
7102
7370
  text-align: center;
7103
- font-size: 12px; /* Changed from 12px to 14px */
7104
- font-weight: 400; /* Added to match data values in other widgets */
7105
- color: #111827; /* Changed from #374151 to match other widgets */
7371
+ font-size: 12px;
7372
+ font-weight: 400;
7373
+ color: #111827;
7106
7374
  overflow: hidden;
7107
7375
  text-overflow: ellipsis;
7108
7376
  white-space: nowrap;
7109
7377
  }
7110
7378
 
7379
+ /* Override color for change values with positive/negative classes */
7380
+ .option-chain-widget .calls-data span.positive,
7381
+ .option-chain-widget .puts-data span.positive {
7382
+ color: #059669 !important;
7383
+ font-weight: 500;
7384
+ }
7385
+
7386
+ .option-chain-widget .calls-data span.negative,
7387
+ .option-chain-widget .puts-data span.negative {
7388
+ color: #dc2626 !important;
7389
+ font-weight: 500;
7390
+ }
7391
+
7392
+ .option-chain-widget .calls-data span.neutral,
7393
+ .option-chain-widget .puts-data span.neutral {
7394
+ color: #6b7280 !important;
7395
+ }
7396
+
7111
7397
  .option-chain-widget .contract-cell {
7112
7398
  font-size: 10px;
7113
7399
  color: #6b7280;
@@ -7119,78 +7405,42 @@ ${SharedStyles}
7119
7405
  white-space: nowrap;
7120
7406
  }
7121
7407
 
7122
- .option-chain-widget .positive {
7123
- color: #059669;
7124
- }
7125
-
7126
- .option-chain-widget .negative {
7127
- color: #dc2626;
7128
- }
7129
-
7130
- .option-chain-widget .neutral {
7131
- color: #6b7280;
7132
- }
7133
-
7134
- .option-chain-widget .widget-footer {
7135
- background: #f9fafb;
7136
- padding: 8px 16px;
7137
- border-top: 1px solid #e5e7eb;
7138
- text-align: center;
7139
- font-size: 11px;
7140
- color: #6b7280;
7408
+ .option-chain-widget .contract-cell.clickable {
7409
+ color: #3b82f6;
7410
+ cursor: pointer;
7411
+ text-decoration: underline;
7412
+ transition: all 0.2s ease;
7141
7413
  }
7142
7414
 
7143
- .option-chain-widget .widget-loading-overlay {
7144
- position: absolute;
7145
- top: 0;
7146
- left: 0;
7147
- right: 0;
7148
- bottom: 0;
7149
- background: rgba(255, 255, 255, 0.9);
7150
- display: flex;
7151
- align-items: center;
7152
- justify-content: center;
7153
- z-index: 20;
7415
+ .option-chain-widget .contract-cell.clickable:hover {
7416
+ color: #2563eb;
7417
+ background-color: #eff6ff;
7154
7418
  }
7155
7419
 
7156
- .option-chain-widget .widget-loading-overlay.hidden {
7157
- display: none;
7420
+ .option-chain-widget .contract-cell.clickable:focus {
7421
+ outline: 2px solid #3b82f6;
7422
+ outline-offset: 2px;
7423
+ border-radius: 2px;
7158
7424
  }
7159
7425
 
7160
- .option-chain-widget .loading-content {
7161
- text-align: center;
7426
+ .option-chain-widget .contract-cell.clickable:active {
7427
+ color: #1e40af;
7428
+ background-color: #dbeafe;
7162
7429
  }
7163
7430
 
7164
- .option-chain-widget .loading-spinner {
7165
- width: 32px;
7166
- height: 32px;
7167
- border: 3px solid #e5e7eb;
7168
- border-top: 3px solid #3b82f6;
7169
- border-radius: 50%;
7170
- animation: spin 1s linear infinite;
7171
- margin: 0 auto 12px;
7431
+ .option-chain-widget .positive {
7432
+ color: #059669;
7172
7433
  }
7173
7434
 
7174
- @keyframes spin {
7175
- 0% { transform: rotate(0deg); }
7176
- 100% { transform: rotate(360deg); }
7435
+ .option-chain-widget .negative {
7436
+ color: #dc2626;
7177
7437
  }
7178
7438
 
7179
- .option-chain-widget .no-data-state {
7180
- padding: 40px;
7181
- text-align: center;
7439
+ .option-chain-widget .neutral {
7182
7440
  color: #6b7280;
7183
7441
  }
7184
7442
 
7185
- .option-chain-widget .widget-error {
7186
- padding: 20px;
7187
- background: #fef2f2;
7188
- color: #dc2626;
7189
- border: 1px solid #fecaca;
7190
- margin: 16px;
7191
- border-radius: 4px;
7192
- text-align: center;
7193
- }
7443
+ /* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
7194
7444
 
7195
7445
  /* RESPONSIVE STYLES */
7196
7446
 
@@ -7201,13 +7451,19 @@ ${SharedStyles}
7201
7451
  align-items: stretch;
7202
7452
  gap: 8px;
7203
7453
  }
7204
-
7454
+
7455
+ .option-chain-widget .filter-section {
7456
+ flex-direction: row;
7457
+ margin-top: 0;
7458
+ }
7459
+
7205
7460
  .option-chain-widget .symbol-input,
7206
- .option-chain-widget .date-select {
7461
+ .option-chain-widget .date-select,
7462
+ .option-chain-widget .strike-filter {
7207
7463
  width: 100%;
7208
7464
  max-width: none;
7209
7465
  }
7210
-
7466
+
7211
7467
  .option-chain-widget .fetch-button {
7212
7468
  width: 100%;
7213
7469
  }
@@ -7363,6 +7619,121 @@ ${SharedStyles}
7363
7619
  padding: 2px 1px;
7364
7620
  }
7365
7621
  }
7622
+
7623
+ /* Modal Styles */
7624
+ .option-chain-modal-overlay {
7625
+ position: fixed;
7626
+ top: 0;
7627
+ left: 0;
7628
+ right: 0;
7629
+ bottom: 0;
7630
+ background: rgba(0, 0, 0, 0.6);
7631
+ display: flex;
7632
+ align-items: center;
7633
+ justify-content: center;
7634
+ z-index: 10000;
7635
+ padding: 20px;
7636
+ animation: fadeIn 0.2s ease-out;
7637
+ }
7638
+
7639
+ @keyframes fadeIn {
7640
+ from {
7641
+ opacity: 0;
7642
+ }
7643
+ to {
7644
+ opacity: 1;
7645
+ }
7646
+ }
7647
+
7648
+ .option-chain-modal {
7649
+ background: white;
7650
+ border-radius: 12px;
7651
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
7652
+ max-width: 800px;
7653
+ width: 100%;
7654
+ max-height: 90vh;
7655
+ display: flex;
7656
+ flex-direction: column;
7657
+ animation: slideUp 0.3s ease-out;
7658
+ }
7659
+
7660
+ @keyframes slideUp {
7661
+ from {
7662
+ transform: translateY(20px);
7663
+ opacity: 0;
7664
+ }
7665
+ to {
7666
+ transform: translateY(0);
7667
+ opacity: 1;
7668
+ }
7669
+ }
7670
+
7671
+ .option-chain-modal-header {
7672
+ display: flex;
7673
+ align-items: center;
7674
+ justify-content: space-between;
7675
+ padding: 20px 24px;
7676
+ border-bottom: 1px solid #e5e7eb;
7677
+ }
7678
+
7679
+ .option-chain-modal-header h3 {
7680
+ margin: 0;
7681
+ font-size: 18px;
7682
+ font-weight: 600;
7683
+ color: #111827;
7684
+ }
7685
+
7686
+ .option-chain-modal-close {
7687
+ background: none;
7688
+ border: none;
7689
+ font-size: 28px;
7690
+ line-height: 1;
7691
+ color: #6b7280;
7692
+ cursor: pointer;
7693
+ padding: 0;
7694
+ width: 32px;
7695
+ height: 32px;
7696
+ display: flex;
7697
+ align-items: center;
7698
+ justify-content: center;
7699
+ border-radius: 4px;
7700
+ transition: all 0.2s ease;
7701
+ }
7702
+
7703
+ .option-chain-modal-close:hover {
7704
+ background: #f3f4f6;
7705
+ color: #111827;
7706
+ }
7707
+
7708
+ .option-chain-modal-close:active {
7709
+ background: #e5e7eb;
7710
+ }
7711
+
7712
+ .option-chain-modal-body {
7713
+ flex: 1;
7714
+ overflow-y: auto;
7715
+ padding: 24px;
7716
+ }
7717
+
7718
+ /* Responsive modal */
7719
+ @media (max-width: 768px) {
7720
+ .option-chain-modal {
7721
+ max-width: 95%;
7722
+ max-height: 95vh;
7723
+ }
7724
+
7725
+ .option-chain-modal-header {
7726
+ padding: 16px;
7727
+ }
7728
+
7729
+ .option-chain-modal-header h3 {
7730
+ font-size: 16px;
7731
+ }
7732
+
7733
+ .option-chain-modal-body {
7734
+ padding: 16px;
7735
+ }
7736
+ }
7366
7737
  `;
7367
7738
 
7368
7739
  class OptionChainWidget extends BaseWidget {
@@ -7377,7 +7748,18 @@ class OptionChainWidget extends BaseWidget {
7377
7748
  this.data = null;
7378
7749
  this.isDestroyed = false;
7379
7750
  this.unsubscribe = null;
7751
+ this.unsubscribeUnderlying = null; // For underlying stock price
7752
+ this.underlyingPrice = null; // Current price of underlying stock
7380
7753
  this.loadingTimeout = null;
7754
+ this.renderDebounceTimeout = null; // For debouncing re-renders
7755
+
7756
+ // Separate cache for option chain data and underlying price
7757
+ this.cachedOptionChainData = null;
7758
+ this.cachedUnderlyingPrice = null;
7759
+
7760
+ // Modal for Options widget
7761
+ this.optionsModal = null;
7762
+ this.optionsWidgetInstance = null;
7381
7763
 
7382
7764
  // INPUT VALIDATION: Validate initial symbol if provided
7383
7765
  if (options.symbol) {
@@ -7393,6 +7775,7 @@ class OptionChainWidget extends BaseWidget {
7393
7775
  }
7394
7776
  this.date = '';
7395
7777
  this.availableDates = {};
7778
+ this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
7396
7779
 
7397
7780
  // RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
7398
7781
  this.fetchRateLimiter = createRateLimitValidator(1000);
@@ -7423,6 +7806,7 @@ class OptionChainWidget extends BaseWidget {
7423
7806
  this.dateSelect = this.container.querySelector('.date-select');
7424
7807
  this.fetchButton = this.container.querySelector('.fetch-button');
7425
7808
  this.dataGrid = this.container.querySelector('.option-chain-data-grid');
7809
+ this.strikeFilterSelect = this.container.querySelector('.strike-filter');
7426
7810
 
7427
7811
  // Set initial symbol value if provided in options
7428
7812
  if (this.symbol) {
@@ -7497,6 +7881,22 @@ class OptionChainWidget extends BaseWidget {
7497
7881
  }
7498
7882
  this.loadAvailableDates(symbolValidation.sanitized);
7499
7883
  });
7884
+
7885
+ // MEMORY LEAK FIX: Use BaseWidget's addEventListener
7886
+ // Strike filter dropdown
7887
+ this.addEventListener(this.strikeFilterSelect, 'change', e => {
7888
+ const value = e.target.value;
7889
+ if (value === 'all') {
7890
+ this.strikeFilterRange = 'all';
7891
+ } else {
7892
+ this.strikeFilterRange = parseInt(value, 10);
7893
+ }
7894
+
7895
+ // Re-render with new filter if we have data
7896
+ if (this.data) {
7897
+ this.displayOptionChain(this.data);
7898
+ }
7899
+ });
7500
7900
  }
7501
7901
  async loadAvailableDates(symbol) {
7502
7902
  if (!symbol) return;
@@ -7512,7 +7912,9 @@ class OptionChainWidget extends BaseWidget {
7512
7912
  // Use API service from wsManager
7513
7913
  const apiService = this.wsManager.getApiService();
7514
7914
  const data = await apiService.getOptionChainDates(symbol);
7515
- console.log("Available dates:", data.dates_dictionary);
7915
+ if (this.debug) {
7916
+ console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
7917
+ }
7516
7918
  this.availableDates = data.dates_dictionary || {};
7517
7919
  this.populateDateOptions();
7518
7920
 
@@ -7606,11 +8008,15 @@ class OptionChainWidget extends BaseWidget {
7606
8008
  this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
7607
8009
  }, 10000); // 10 second timeout
7608
8010
 
7609
- // Unsubscribe from previous subscription if exists
8011
+ // Unsubscribe from previous subscriptions if they exist
7610
8012
  if (this.unsubscribe) {
7611
8013
  this.unsubscribe();
7612
8014
  this.unsubscribe = null;
7613
8015
  }
8016
+ if (this.unsubscribeUnderlying) {
8017
+ this.unsubscribeUnderlying();
8018
+ this.unsubscribeUnderlying = null;
8019
+ }
7614
8020
 
7615
8021
  // Subscribe to option chain data
7616
8022
  this.subscribeToData();
@@ -7620,25 +8026,62 @@ class OptionChainWidget extends BaseWidget {
7620
8026
  }
7621
8027
  }
7622
8028
  subscribeToData() {
7623
- // Subscribe with symbol and date for routing
7624
- this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
7625
- // Pass symbol as string
7626
- {
8029
+ // Subscribe to option chain data
8030
+ this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
7627
8031
  date: this.date
7628
- } // Pass date as additional parameter
7629
- );
8032
+ });
7630
8033
 
7631
- // Send subscription message with both symbol and date
7632
- /* this.wsManager.send({
7633
- type: 'queryoptionchain',
7634
- underlying: this.symbol,
7635
- date: this.date
7636
- }); */
8034
+ // Subscribe to underlying stock price for ITM highlighting
8035
+ this.unsubscribeUnderlying = this.wsManager.subscribe(`${this.widgetId}-underlying`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8036
+ }
8037
+
8038
+ /**
8039
+ * Find the nearest strike price to the underlying price (ATM strike)
8040
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8041
+ * @param {Number} underlyingPrice - Current price of underlying stock
8042
+ * @returns {String} - The nearest strike price
8043
+ */
8044
+ findNearestStrike(sortedStrikes, underlyingPrice) {
8045
+ if (!sortedStrikes || sortedStrikes.length === 0 || !underlyingPrice) {
8046
+ return null;
8047
+ }
8048
+
8049
+ // Find strike closest to underlying price
8050
+ let nearestStrike = sortedStrikes[0];
8051
+ let minDifference = Math.abs(parseFloat(sortedStrikes[0]) - underlyingPrice);
8052
+ for (const strike of sortedStrikes) {
8053
+ const difference = Math.abs(parseFloat(strike) - underlyingPrice);
8054
+ if (difference < minDifference) {
8055
+ minDifference = difference;
8056
+ nearestStrike = strike;
8057
+ }
8058
+ }
8059
+ return nearestStrike;
8060
+ }
8061
+
8062
+ /**
8063
+ * Filter strikes to show only those near the money
8064
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8065
+ * @param {String} atmStrike - The at-the-money strike price
8066
+ * @param {Number} range - Number of strikes to show above and below ATM
8067
+ * @returns {Array} - Filtered array of strikes
8068
+ */
8069
+ filterNearMoneyStrikes(sortedStrikes, atmStrike, range) {
8070
+ if (!atmStrike || !sortedStrikes || sortedStrikes.length === 0) {
8071
+ return sortedStrikes;
8072
+ }
8073
+ const atmIndex = sortedStrikes.indexOf(atmStrike);
8074
+ if (atmIndex === -1) {
8075
+ return sortedStrikes;
8076
+ }
8077
+ const startIndex = Math.max(0, atmIndex - range);
8078
+ const endIndex = Math.min(sortedStrikes.length - 1, atmIndex + range);
8079
+ return sortedStrikes.slice(startIndex, endIndex + 1);
7637
8080
  }
7638
8081
  handleData(message) {
7639
- // Clear loading timeout since we received data
8082
+ // Clear loading timeout since we received data (use BaseWidget method)
7640
8083
  if (this.loadingTimeout) {
7641
- clearTimeout(this.loadingTimeout);
8084
+ this.clearTimeout(this.loadingTimeout);
7642
8085
  this.loadingTimeout = null;
7643
8086
  }
7644
8087
 
@@ -7670,7 +8113,9 @@ class OptionChainWidget extends BaseWidget {
7670
8113
  }
7671
8114
  }
7672
8115
  } else {
7673
- this.data = message; // Store for caching
8116
+ // Cache the option chain data
8117
+ this.data = message;
8118
+ this.cachedOptionChainData = message;
7674
8119
  this.displayOptionChain(message);
7675
8120
  }
7676
8121
  } else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
@@ -7685,9 +8130,35 @@ class OptionChainWidget extends BaseWidget {
7685
8130
  }
7686
8131
  }
7687
8132
  } else {
7688
- this.data = message.data; // Store for caching
8133
+ // Cache the option chain data
8134
+ this.data = message.data;
8135
+ this.cachedOptionChainData = message.data;
7689
8136
  this.displayOptionChain(message.data);
7690
8137
  }
8138
+ } else if (message.type === 'queryl1' && message.Data) {
8139
+ if (this.debug) {
8140
+ console.log('[OptionChainWidget] Received underlying price update');
8141
+ }
8142
+ const data = message.Data.find(d => d.Symbol === this.symbol);
8143
+ if (data && data.LastPx) {
8144
+ // Cache the underlying price
8145
+ this.underlyingPrice = parseFloat(data.LastPx);
8146
+ this.cachedUnderlyingPrice = parseFloat(data.LastPx);
8147
+
8148
+ // Debounce re-render to prevent excessive updates
8149
+ // Clear any pending render
8150
+ if (this.renderDebounceTimeout) {
8151
+ this.clearTimeout(this.renderDebounceTimeout);
8152
+ }
8153
+
8154
+ // Schedule a debounced re-render (500ms delay)
8155
+ this.renderDebounceTimeout = this.setTimeout(() => {
8156
+ if (this.data) {
8157
+ this.displayOptionChain(this.data);
8158
+ }
8159
+ this.renderDebounceTimeout = null;
8160
+ }, 500);
8161
+ }
7691
8162
  }
7692
8163
  if (message._cached) {
7693
8164
  this.showConnectionQuality(); // Show cache indicator
@@ -7695,6 +8166,14 @@ class OptionChainWidget extends BaseWidget {
7695
8166
  }
7696
8167
  displayOptionChain(data) {
7697
8168
  if (this.isDestroyed) return;
8169
+
8170
+ // Validate data
8171
+ if (!data || !Array.isArray(data) || data.length === 0) {
8172
+ if (this.debug) {
8173
+ console.warn('[OptionChainWidget] Invalid or empty data passed to displayOptionChain');
8174
+ }
8175
+ return;
8176
+ }
7698
8177
  try {
7699
8178
  // Hide loading overlay
7700
8179
  this.hideLoading();
@@ -7727,9 +8206,18 @@ class OptionChainWidget extends BaseWidget {
7727
8206
  }
7728
8207
  });
7729
8208
 
7730
- // Sort strikes and display
8209
+ // Sort strikes
7731
8210
  const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
7732
- sortedStrikes.forEach(strike => {
8211
+
8212
+ // Apply ATM filter if not showing all
8213
+ let displayStrikes = sortedStrikes;
8214
+ if (this.strikeFilterRange !== 'all' && this.underlyingPrice) {
8215
+ const atmStrike = this.findNearestStrike(sortedStrikes, this.underlyingPrice);
8216
+ if (atmStrike) {
8217
+ displayStrikes = this.filterNearMoneyStrikes(sortedStrikes, atmStrike, this.strikeFilterRange);
8218
+ }
8219
+ }
8220
+ displayStrikes.forEach(strike => {
7733
8221
  const {
7734
8222
  call,
7735
8223
  put
@@ -7748,7 +8236,7 @@ class OptionChainWidget extends BaseWidget {
7748
8236
  const formatted = parseFloat(change).toFixed(2);
7749
8237
  const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
7750
8238
  const sign = change > 0 ? '+' : '';
7751
- span.className = `option-chain-${className}`;
8239
+ span.className = className;
7752
8240
  span.textContent = `${sign}${formatted}`;
7753
8241
  return span;
7754
8242
  };
@@ -7761,7 +8249,28 @@ class OptionChainWidget extends BaseWidget {
7761
8249
  // Create calls data section
7762
8250
  const callsData = document.createElement('div');
7763
8251
  callsData.className = 'calls-data';
7764
- callsData.appendChild(createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell'));
8252
+
8253
+ // Add ITM highlighting for calls (ITM when strike < underlying price)
8254
+ if (this.underlyingPrice && parseFloat(strike) < this.underlyingPrice) {
8255
+ callsData.classList.add('in-the-money');
8256
+ }
8257
+
8258
+ // Make contract cell clickable
8259
+ const callContractCell = createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell');
8260
+ if (call && call.symbol) {
8261
+ callContractCell.classList.add('clickable');
8262
+ callContractCell.setAttribute('role', 'button');
8263
+ callContractCell.setAttribute('tabindex', '0');
8264
+ callContractCell.setAttribute('data-symbol', call.symbol);
8265
+ this.addEventListener(callContractCell, 'click', () => this.showOptionsModal(call.symbol));
8266
+ this.addEventListener(callContractCell, 'keypress', e => {
8267
+ if (e.key === 'Enter' || e.key === ' ') {
8268
+ e.preventDefault();
8269
+ this.showOptionsModal(call.symbol);
8270
+ }
8271
+ });
8272
+ }
8273
+ callsData.appendChild(callContractCell);
7765
8274
  callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
7766
8275
  const callChangeSpan = document.createElement('span');
7767
8276
  if (call) {
@@ -7784,7 +8293,28 @@ class OptionChainWidget extends BaseWidget {
7784
8293
  // Create puts data section
7785
8294
  const putsData = document.createElement('div');
7786
8295
  putsData.className = 'puts-data';
7787
- putsData.appendChild(createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell'));
8296
+
8297
+ // Add ITM highlighting for puts (ITM when strike > underlying price)
8298
+ if (this.underlyingPrice && parseFloat(strike) > this.underlyingPrice) {
8299
+ putsData.classList.add('in-the-money');
8300
+ }
8301
+
8302
+ // Make contract cell clickable
8303
+ const putContractCell = createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell');
8304
+ if (put && put.symbol) {
8305
+ putContractCell.classList.add('clickable');
8306
+ putContractCell.setAttribute('role', 'button');
8307
+ putContractCell.setAttribute('tabindex', '0');
8308
+ putContractCell.setAttribute('data-symbol', put.symbol);
8309
+ this.addEventListener(putContractCell, 'click', () => this.showOptionsModal(put.symbol));
8310
+ this.addEventListener(putContractCell, 'keypress', e => {
8311
+ if (e.key === 'Enter' || e.key === ' ') {
8312
+ e.preventDefault();
8313
+ this.showOptionsModal(put.symbol);
8314
+ }
8315
+ });
8316
+ }
8317
+ putsData.appendChild(putContractCell);
7788
8318
  putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
7789
8319
  const putChangeSpan = document.createElement('span');
7790
8320
  if (put) {
@@ -7808,14 +8338,22 @@ class OptionChainWidget extends BaseWidget {
7808
8338
 
7809
8339
  // Add footer - with array length check
7810
8340
  if (data && data.length > 0) {
7811
- const timestamp = formatTimestampET(data[0].quoteTime || Date.now());
8341
+ const isDelayed = data[0].DataSource === 'OPRA-D';
8342
+
8343
+ // Get timestamp - subtract 20 minutes if delayed data
8344
+ let timestampValue = data[0].quoteTime || Date.now();
8345
+ if (isDelayed) {
8346
+ // Subtract 20 minutes (20 * 60 * 1000 milliseconds)
8347
+ timestampValue = timestampValue - 20 * 60 * 1000;
8348
+ }
8349
+ const timestamp = formatTimestampET(timestampValue);
7812
8350
  const lastUpdateElement = this.container.querySelector('.last-update');
7813
8351
  if (lastUpdateElement) {
7814
8352
  lastUpdateElement.textContent = `Last update: ${timestamp}`;
7815
8353
  }
7816
8354
 
7817
8355
  // Update footer
7818
- const source = data[0].DataSource === 'OPRA-D' ? '20 mins delayed' : 'Real-time';
8356
+ const source = isDelayed ? '20 mins delayed' : 'Real-time';
7819
8357
  const dataSourceElement = this.container.querySelector('.data-source');
7820
8358
  if (dataSourceElement) {
7821
8359
  dataSourceElement.textContent = `Source: ${source}`;
@@ -7838,35 +8376,6 @@ class OptionChainWidget extends BaseWidget {
7838
8376
  loadingOverlay.classList.add('hidden');
7839
8377
  }
7840
8378
  }
7841
-
7842
- /* showInputError(message) {
7843
- const errorDiv = document.createElement('div');
7844
- errorDiv.className = 'symbol-error-message';
7845
- errorDiv.textContent = message;
7846
- errorDiv.style.cssText = `
7847
- color: #dc2626;
7848
- font-size: 12px;
7849
- margin-bottom: 2px;
7850
- position: absolute;
7851
- background: white;
7852
- z-index: 1000;
7853
- transform: translateY(-100%);
7854
- top: -4px;
7855
- left: 0; // Align with input box
7856
- `;
7857
-
7858
- // Ensure the parent is positioned
7859
- const parent = this.symbolInput;
7860
- if (parent) {
7861
- parent.style.position = 'relative';
7862
- // Insert the errorDiv as the first child of the parent
7863
- parent.insertBefore(errorDiv, parent.firstChild);
7864
- }
7865
-
7866
- this.symbolInput.errorMessage = errorDiv;
7867
- this.symbolInput.focus();
7868
- } */
7869
-
7870
8379
  showError(message) {
7871
8380
  this.hideLoading();
7872
8381
  this.clearError();
@@ -7939,19 +8448,135 @@ class OptionChainWidget extends BaseWidget {
7939
8448
  this.container.appendChild(noDataElement);
7940
8449
  }
7941
8450
  }
8451
+
8452
+ /**
8453
+ * Show Options widget in a modal for a specific option contract
8454
+ * @param {string} optionSymbol - The option contract symbol
8455
+ */
8456
+ showOptionsModal(optionSymbol) {
8457
+ if (!optionSymbol || optionSymbol === '--') {
8458
+ return;
8459
+ }
8460
+
8461
+ // Close existing modal if any
8462
+ this.closeOptionsModal();
8463
+
8464
+ // Create modal overlay
8465
+ this.optionsModal = document.createElement('div');
8466
+ this.optionsModal.className = 'option-chain-modal-overlay';
8467
+ this.optionsModal.innerHTML = `
8468
+ <div class="option-chain-modal">
8469
+ <div class="option-chain-modal-header">
8470
+ <h3>Option Details: ${sanitizeSymbol(optionSymbol)}</h3>
8471
+ <button class="option-chain-modal-close" aria-label="Close modal">&times;</button>
8472
+ </div>
8473
+ <div class="option-chain-modal-body">
8474
+ <div id="option-chain-modal-widget"></div>
8475
+ </div>
8476
+ </div>
8477
+ `;
8478
+
8479
+ // Add to body
8480
+ document.body.appendChild(this.optionsModal);
8481
+
8482
+ // Add close event
8483
+ const closeBtn = this.optionsModal.querySelector('.option-chain-modal-close');
8484
+ this.addEventListener(closeBtn, 'click', () => this.closeOptionsModal());
8485
+
8486
+ // Close on overlay click
8487
+ this.addEventListener(this.optionsModal, 'click', e => {
8488
+ if (e.target === this.optionsModal) {
8489
+ this.closeOptionsModal();
8490
+ }
8491
+ });
8492
+
8493
+ // Close on Escape key
8494
+ const escapeHandler = e => {
8495
+ if (e.key === 'Escape') {
8496
+ this.closeOptionsModal();
8497
+ }
8498
+ };
8499
+ document.addEventListener('keydown', escapeHandler);
8500
+ this._escapeHandler = escapeHandler;
8501
+
8502
+ // Create Options widget instance
8503
+ try {
8504
+ const widgetContainer = this.optionsModal.querySelector('#option-chain-modal-widget');
8505
+ this.optionsWidgetInstance = new OptionsWidget(widgetContainer, {
8506
+ symbol: optionSymbol,
8507
+ wsManager: this.wsManager,
8508
+ debug: this.debug,
8509
+ styled: true
8510
+ }, `${this.widgetId}-options-modal`);
8511
+ if (this.debug) {
8512
+ console.log('[OptionChainWidget] Options modal opened for:', optionSymbol);
8513
+ }
8514
+ } catch (error) {
8515
+ console.error('[OptionChainWidget] Error creating Options widget:', error);
8516
+ this.closeOptionsModal();
8517
+ this.showError(`Failed to load option details: ${error.message}`);
8518
+ }
8519
+ }
8520
+
8521
+ /**
8522
+ * Close the Options modal and cleanup
8523
+ */
8524
+ closeOptionsModal() {
8525
+ // Destroy widget instance
8526
+ if (this.optionsWidgetInstance) {
8527
+ this.optionsWidgetInstance.destroy();
8528
+ this.optionsWidgetInstance = null;
8529
+ }
8530
+
8531
+ // Remove modal from DOM
8532
+ if (this.optionsModal) {
8533
+ this.optionsModal.remove();
8534
+ this.optionsModal = null;
8535
+ }
8536
+
8537
+ // Remove escape key handler
8538
+ if (this._escapeHandler) {
8539
+ document.removeEventListener('keydown', this._escapeHandler);
8540
+ this._escapeHandler = null;
8541
+ }
8542
+ if (this.debug) {
8543
+ console.log('[OptionChainWidget] Options modal closed');
8544
+ }
8545
+ }
7942
8546
  destroy() {
7943
8547
  this.isDestroyed = true;
8548
+
8549
+ // Close modal if open
8550
+ this.closeOptionsModal();
7944
8551
  if (this.unsubscribe) {
7945
8552
  this.unsubscribe();
7946
8553
  this.unsubscribe = null;
7947
8554
  }
7948
8555
 
8556
+ // Unsubscribe from underlying price
8557
+ if (this.unsubscribeUnderlying) {
8558
+ this.unsubscribeUnderlying();
8559
+ this.unsubscribeUnderlying = null;
8560
+ }
8561
+
7949
8562
  // MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
7950
8563
  if (this.loadingTimeout) {
7951
8564
  this.clearTimeout(this.loadingTimeout);
7952
8565
  this.loadingTimeout = null;
7953
8566
  }
7954
8567
 
8568
+ // Clear debounce timeout
8569
+ if (this.renderDebounceTimeout) {
8570
+ this.clearTimeout(this.renderDebounceTimeout);
8571
+ this.renderDebounceTimeout = null;
8572
+ }
8573
+
8574
+ // Clear cached data
8575
+ this.cachedOptionChainData = null;
8576
+ this.cachedUnderlyingPrice = null;
8577
+ this.underlyingPrice = null;
8578
+ this.data = null;
8579
+
7955
8580
  // Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
7956
8581
  super.destroy();
7957
8582
  }
@@ -8037,80 +8662,81 @@ const DataStyles = `
8037
8662
  font-family: Arial, sans-serif;
8038
8663
  }
8039
8664
 
8040
- .widget-header {
8665
+ .data-widget .widget-header {
8041
8666
  display: flex;
8042
8667
  flex-direction: column;
8043
8668
  margin-bottom: 10px;
8044
8669
  }
8045
8670
 
8046
- .symbol {
8671
+ .data-widget .symbol {
8047
8672
  font-size: 24px;
8048
8673
  font-weight: bold;
8049
8674
  margin-right: 10px;
8050
8675
  }
8051
8676
 
8052
- .company-info {
8677
+ .data-widget .company-info {
8053
8678
  font-size: 12px;
8054
8679
  color: #666;
8055
8680
  }
8056
8681
 
8057
- .trading-info {
8682
+ .data-widget .trading-info {
8058
8683
  font-size: 12px;
8059
8684
  color: #999;
8060
8685
  }
8061
8686
 
8062
- .price-section {
8687
+ .data-widget .price-section {
8063
8688
  display: flex;
8064
8689
  align-items: baseline;
8065
8690
  margin-bottom: 10px;
8066
8691
  }
8067
8692
 
8068
- .current-price {
8693
+ .data-widget .current-price {
8069
8694
  font-size: 36px;
8070
8695
  font-weight: bold;
8071
8696
  margin-right: 10px;
8072
8697
  }
8073
8698
 
8074
- .price-change {
8699
+ .data-widget .price-change {
8075
8700
  font-size: 18px;
8076
8701
  }
8077
8702
 
8078
- .price-change.positive {
8703
+ .data-widget .price-change.positive {
8079
8704
  color: green;
8080
8705
  }
8081
8706
 
8082
- .price-change.negative {
8707
+ .data-widget .price-change.negative {
8083
8708
  color: red;
8084
8709
  }
8085
8710
 
8086
- .bid-ask-section {
8711
+ .data-widget .bid-ask-section {
8087
8712
  display: flex;
8088
8713
  justify-content: space-between;
8089
8714
  margin-bottom: 10px;
8090
8715
  }
8091
8716
 
8092
- .ask, .bid {
8717
+ .data-widget .ask,
8718
+ .data-widget .bid {
8093
8719
  display: flex;
8094
8720
  align-items: center;
8095
8721
  }
8096
8722
 
8097
- .label {
8723
+ .data-widget .label {
8098
8724
  font-size: 14px;
8099
8725
  margin-right: 5px;
8100
8726
  }
8101
8727
 
8102
- .value {
8728
+ .data-widget .value {
8103
8729
  font-size: 16px;
8104
8730
  font-weight: bold;
8105
8731
  margin-right: 5px;
8106
8732
  }
8107
8733
 
8108
- .size {
8734
+ .data-widget .size {
8109
8735
  font-size: 14px;
8110
8736
  color: red;
8111
8737
  }
8112
8738
 
8113
- .widget-footer {
8739
+ .data-widget .widget-footer {
8114
8740
  font-size: 12px;
8115
8741
  color: #333;
8116
8742
  }
@@ -8521,76 +9147,50 @@ class CombinedMarketWidget extends BaseWidget {
8521
9147
 
8522
9148
  // Subscribe to regular market data (Level 1)
8523
9149
  // Create a wrapper to add data type context
8524
- this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], messageWrapper => {
8525
- const {
8526
- event,
8527
- data
8528
- } = messageWrapper;
8529
-
8530
- // Handle connection events
8531
- if (event === 'connection') {
8532
- this.handleConnectionStatus(data);
8533
- return;
8534
- }
8535
-
8536
- // For data events, add type context and use base handleMessage pattern
8537
- if (event === 'data') {
8538
- data._dataType = 'market';
8539
- this.handleMessage({
8540
- event,
8541
- data
8542
- });
8543
- }
8544
- }, this.symbol);
9150
+ this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8545
9151
 
8546
9152
  // Subscribe to night session data (BlueOcean or Bruce)
8547
- this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], messageWrapper => {
8548
- const {
8549
- event,
8550
- data
8551
- } = messageWrapper;
8552
-
8553
- // Connection already handled by market subscription
8554
- if (event === 'connection') {
8555
- return;
8556
- }
8557
-
8558
- // For data events, add type context and use base handleMessage pattern
8559
- if (event === 'data') {
8560
- data._dataType = 'night';
8561
- this.handleMessage({
8562
- event,
8563
- data
8564
- });
8565
- }
8566
- }, this.symbol);
9153
+ this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
8567
9154
  }
8568
9155
  handleData(message) {
8569
9156
  // Extract data type from metadata
8570
- const dataType = message._dataType;
9157
+ const dataType = message.type;
8571
9158
  if (this.debug) {
8572
9159
  console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
8573
9160
  }
8574
9161
 
8575
- // Safety check - if no data type, try to infer from structure
8576
- if (!dataType) {
8577
- if (this.debug) {
8578
- console.warn('[CombinedMarketWidget] No data type specified, attempting to infer from structure');
9162
+ // Clear loading timeout since we received data (use BaseWidget method)
9163
+ if (this.loadingTimeout) {
9164
+ this.clearTimeout(this.loadingTimeout);
9165
+ this.loadingTimeout = null;
9166
+ }
9167
+
9168
+ // Handle option chain data
9169
+ if (Array.isArray(message)) {
9170
+ if (message.length === 0) {
9171
+ // Only show no data state if we don't have cached data
9172
+ if (!this.data) {
9173
+ this.showNoDataState();
9174
+ } else {
9175
+ this.hideLoading();
9176
+ if (this.debug) {
9177
+ console.log('[OptionChainWidget] No new data, keeping cached data visible');
9178
+ }
9179
+ }
8579
9180
  }
8580
- return;
8581
9181
  }
8582
9182
 
8583
9183
  // Handle error messages from server
8584
- if (message.type === 'error' && message.noData) {
9184
+ if (message.type === 'error' || message.error == true) {
8585
9185
  if (this.debug) {
8586
9186
  console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
8587
9187
  }
8588
9188
  // Keep existing cached data visible, just hide loading
8589
- if (dataType === 'market' && !this.marketData) {
9189
+ if (dataType === 'queryl1' && !this.marketData) {
8590
9190
  if (this.debug) {
8591
9191
  console.log('[CombinedMarketWidget] No market data available');
8592
9192
  }
8593
- } else if (dataType === 'night' && !this.nightSessionData) {
9193
+ } else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
8594
9194
  if (this.debug) {
8595
9195
  console.log('[CombinedMarketWidget] No night session data available');
8596
9196
  }
@@ -9043,7 +9643,7 @@ const IntradayChartStyles = `
9043
9643
  margin: 0 auto;
9044
9644
  }
9045
9645
 
9046
- .chart-header {
9646
+ .intraday-chart-widget .chart-header {
9047
9647
  display: flex;
9048
9648
  justify-content: space-between;
9049
9649
  align-items: center;
@@ -9052,13 +9652,13 @@ const IntradayChartStyles = `
9052
9652
  border-bottom: 1px solid #e5e7eb;
9053
9653
  }
9054
9654
 
9055
- .chart-title-section {
9655
+ .intraday-chart-widget .chart-title-section {
9056
9656
  display: flex;
9057
9657
  flex-direction: column;
9058
9658
  gap: 4px;
9059
9659
  }
9060
9660
 
9061
- .company-market-info {
9661
+ .intraday-chart-widget .company-market-info {
9062
9662
  display: flex;
9063
9663
  gap: 8px;
9064
9664
  align-items: center;
@@ -9066,18 +9666,18 @@ const IntradayChartStyles = `
9066
9666
  color: #6b7280;
9067
9667
  }
9068
9668
 
9069
- .intraday-company-name {
9669
+ .intraday-chart-widget .intraday-company-name {
9070
9670
  font-weight: 500;
9071
9671
  }
9072
9672
 
9073
- .intraday-chart-symbol {
9673
+ .intraday-chart-widget .intraday-chart-symbol {
9074
9674
  font-size: 1.5em;
9075
9675
  font-weight: 700;
9076
9676
  color: #1f2937;
9077
9677
  margin: 0;
9078
9678
  }
9079
9679
 
9080
- .intraday-chart-source {
9680
+ .intraday-chart-widget .intraday-chart-source {
9081
9681
  font-size: 0.75em;
9082
9682
  padding: 4px 8px;
9083
9683
  border-radius: 4px;
@@ -9086,36 +9686,36 @@ const IntradayChartStyles = `
9086
9686
  font-weight: 600;
9087
9687
  }
9088
9688
 
9089
- .chart-change {
9689
+ .intraday-chart-widget .chart-change {
9090
9690
  font-size: 1.1em;
9091
9691
  font-weight: 600;
9092
9692
  padding: 6px 12px;
9093
9693
  border-radius: 6px;
9094
9694
  }
9095
9695
 
9096
- .chart-change.positive {
9696
+ .intraday-chart-widget .chart-change.positive {
9097
9697
  color: #059669;
9098
9698
  background: #d1fae5;
9099
9699
  }
9100
9700
 
9101
- .chart-change.negative {
9701
+ .intraday-chart-widget .chart-change.negative {
9102
9702
  color: #dc2626;
9103
9703
  background: #fee2e2;
9104
9704
  }
9105
9705
 
9106
- .chart-controls {
9706
+ .intraday-chart-widget .chart-controls {
9107
9707
  display: flex;
9108
9708
  justify-content: space-between;
9109
9709
  align-items: center;
9110
9710
  margin-bottom: 15px;
9111
9711
  }
9112
9712
 
9113
- .chart-range-selector {
9713
+ .intraday-chart-widget .chart-range-selector {
9114
9714
  display: flex;
9115
9715
  gap: 8px;
9116
9716
  }
9117
9717
 
9118
- .range-btn {
9718
+ .intraday-chart-widget .range-btn {
9119
9719
  padding: 8px 16px;
9120
9720
  border: 1px solid #e5e7eb;
9121
9721
  background: white;
@@ -9127,28 +9727,28 @@ const IntradayChartStyles = `
9127
9727
  transition: all 0.2s ease;
9128
9728
  }
9129
9729
 
9130
- .range-btn:hover {
9730
+ .intraday-chart-widget .range-btn:hover {
9131
9731
  background: #f9fafb;
9132
9732
  border-color: #d1d5db;
9133
9733
  }
9134
9734
 
9135
- .range-btn.active {
9735
+ .intraday-chart-widget .range-btn.active {
9136
9736
  background: #667eea;
9137
9737
  color: white;
9138
9738
  border-color: #667eea;
9139
9739
  }
9140
9740
 
9141
- .range-btn:focus {
9741
+ .intraday-chart-widget .range-btn:focus {
9142
9742
  outline: none;
9143
9743
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
9144
9744
  }
9145
9745
 
9146
- .chart-type-selector {
9746
+ .intraday-chart-widget .chart-type-selector {
9147
9747
  display: flex;
9148
9748
  gap: 8px;
9149
9749
  }
9150
9750
 
9151
- .type-btn {
9751
+ .intraday-chart-widget .type-btn {
9152
9752
  padding: 8px 16px;
9153
9753
  border: 1px solid #e5e7eb;
9154
9754
  background: white;
@@ -9160,23 +9760,23 @@ const IntradayChartStyles = `
9160
9760
  transition: all 0.2s ease;
9161
9761
  }
9162
9762
 
9163
- .type-btn:hover {
9763
+ .intraday-chart-widget .type-btn:hover {
9164
9764
  background: #f9fafb;
9165
9765
  border-color: #d1d5db;
9166
9766
  }
9167
9767
 
9168
- .type-btn.active {
9768
+ .intraday-chart-widget .type-btn.active {
9169
9769
  background: #10b981;
9170
9770
  color: white;
9171
9771
  border-color: #10b981;
9172
9772
  }
9173
9773
 
9174
- .type-btn:focus {
9774
+ .intraday-chart-widget .type-btn:focus {
9175
9775
  outline: none;
9176
9776
  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
9177
9777
  }
9178
9778
 
9179
- .zoom-reset-btn {
9779
+ .intraday-chart-widget .zoom-reset-btn {
9180
9780
  display: flex;
9181
9781
  align-items: center;
9182
9782
  gap: 6px;
@@ -9191,34 +9791,34 @@ const IntradayChartStyles = `
9191
9791
  transition: all 0.2s ease;
9192
9792
  }
9193
9793
 
9194
- .zoom-reset-btn:hover {
9794
+ .intraday-chart-widget .zoom-reset-btn:hover {
9195
9795
  background: #f9fafb;
9196
9796
  border-color: #667eea;
9197
9797
  color: #667eea;
9198
9798
  }
9199
9799
 
9200
- .zoom-reset-btn:focus {
9800
+ .intraday-chart-widget .zoom-reset-btn:focus {
9201
9801
  outline: none;
9202
9802
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
9203
9803
  }
9204
9804
 
9205
- .zoom-reset-btn svg {
9805
+ .intraday-chart-widget .zoom-reset-btn svg {
9206
9806
  flex-shrink: 0;
9207
9807
  }
9208
9808
 
9209
- .chart-container {
9809
+ .intraday-chart-widget .chart-container {
9210
9810
  height: 500px;
9211
9811
  margin-bottom: 20px;
9212
9812
  position: relative;
9213
9813
  }
9214
9814
 
9215
- .chart-stats {
9815
+ .intraday-chart-widget .chart-stats {
9216
9816
  padding: 15px;
9217
9817
  background: #f9fafb;
9218
9818
  border-radius: 8px;
9219
9819
  }
9220
9820
 
9221
- .stats-header {
9821
+ .intraday-chart-widget .stats-header {
9222
9822
  font-size: 0.875em;
9223
9823
  font-weight: 700;
9224
9824
  color: #374151;
@@ -9229,19 +9829,19 @@ const IntradayChartStyles = `
9229
9829
  border-bottom: 2px solid #e5e7eb;
9230
9830
  }
9231
9831
 
9232
- .stats-grid {
9832
+ .intraday-chart-widget .stats-grid {
9233
9833
  display: grid;
9234
9834
  grid-template-columns: repeat(5, 1fr);
9235
9835
  gap: 15px;
9236
9836
  }
9237
9837
 
9238
- .stat-item {
9838
+ .intraday-chart-widget .stat-item {
9239
9839
  display: flex;
9240
9840
  flex-direction: column;
9241
9841
  gap: 4px;
9242
9842
  }
9243
9843
 
9244
- .stat-label {
9844
+ .intraday-chart-widget .stat-label {
9245
9845
  font-size: 0.75em;
9246
9846
  color: #6b7280;
9247
9847
  font-weight: 600;
@@ -9249,13 +9849,13 @@ const IntradayChartStyles = `
9249
9849
  letter-spacing: 0.5px;
9250
9850
  }
9251
9851
 
9252
- .stat-value {
9852
+ .intraday-chart-widget .stat-value {
9253
9853
  font-size: 1.1em;
9254
9854
  font-weight: 700;
9255
9855
  color: #1f2937;
9256
9856
  }
9257
9857
 
9258
- .widget-loading-overlay {
9858
+ .intraday-chart-widget .widget-loading-overlay {
9259
9859
  position: absolute;
9260
9860
  top: 0;
9261
9861
  left: 0;
@@ -9274,26 +9874,26 @@ const IntradayChartStyles = `
9274
9874
  backdrop-filter: blur(1px);
9275
9875
  }
9276
9876
 
9277
- .widget-loading-overlay.hidden {
9877
+ .intraday-chart-widget .widget-loading-overlay.hidden {
9278
9878
  display: none;
9279
9879
  }
9280
9880
 
9281
- .loading-spinner {
9881
+ .intraday-chart-widget .loading-spinner {
9282
9882
  width: 20px;
9283
9883
  height: 20px;
9284
9884
  border: 3px solid #e5e7eb;
9285
9885
  border-top-color: #667eea;
9286
9886
  border-radius: 50%;
9287
- animation: spin 0.8s linear infinite;
9887
+ animation: intraday-spin 0.8s linear infinite;
9288
9888
  }
9289
9889
 
9290
- .loading-text {
9890
+ .intraday-chart-widget .loading-text {
9291
9891
  color: #6b7280;
9292
9892
  font-size: 0.875em;
9293
9893
  font-weight: 500;
9294
9894
  }
9295
9895
 
9296
- @keyframes spin {
9896
+ @keyframes intraday-spin {
9297
9897
  to { transform: rotate(360deg); }
9298
9898
  }
9299
9899
 
@@ -9303,26 +9903,26 @@ const IntradayChartStyles = `
9303
9903
  padding: 15px;
9304
9904
  }
9305
9905
 
9306
- .stats-grid {
9906
+ .intraday-chart-widget .stats-grid {
9307
9907
  grid-template-columns: repeat(3, 1fr);
9308
9908
  gap: 10px;
9309
9909
  }
9310
9910
 
9311
- .stats-header {
9911
+ .intraday-chart-widget .stats-header {
9312
9912
  font-size: 0.8em;
9313
9913
  margin-bottom: 10px;
9314
9914
  }
9315
9915
 
9316
- .chart-container {
9916
+ .intraday-chart-widget .chart-container {
9317
9917
  height: 350px;
9318
9918
  }
9319
9919
 
9320
- .intraday-chart-symbol {
9920
+ .intraday-chart-widget .intraday-chart-symbol {
9321
9921
  font-size: 1.2em;
9322
9922
  }
9323
9923
  }
9324
9924
 
9325
- .widget-error {
9925
+ .intraday-chart-widget .widget-error {
9326
9926
  padding: 15px;
9327
9927
  background: #fee2e2;
9328
9928
  border: 1px solid #fecaca;
@@ -38698,7 +39298,20 @@ class IntradayChartWidget extends BaseWidget {
38698
39298
  }
38699
39299
  async initialize() {
38700
39300
  // Validate initial symbol via API to get company info
38701
- await this.validateInitialSymbol();
39301
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
39302
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
39303
+ // Extract company info from first data item
39304
+ if (data && data[0]) {
39305
+ this.companyName = data[0].comp_name || '';
39306
+ this.exchangeName = data[0].market_name || '';
39307
+ this.mic = data[0].mic || '';
39308
+ }
39309
+ });
39310
+
39311
+ // If validation failed due to access/permission issues, stop initialization
39312
+ if (validationSuccess === false) {
39313
+ return; // Error is already shown, don't continue
39314
+ }
38702
39315
 
38703
39316
  // Update company name in the header
38704
39317
  this.updateCompanyName();
@@ -38706,38 +39319,6 @@ class IntradayChartWidget extends BaseWidget {
38706
39319
  // Load chart data
38707
39320
  await this.loadChartData();
38708
39321
  }
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
39322
  updateCompanyName() {
38742
39323
  const companyNameElement = this.container.querySelector('.intraday-company-name');
38743
39324
  if (companyNameElement) {
@@ -39161,19 +39742,12 @@ class IntradayChartWidget extends BaseWidget {
39161
39742
  // Handle array format (standard night session format)
39162
39743
  if (Array.isArray(message.data)) {
39163
39744
  console.log('[IntradayChartWidget] Processing array format, length:', message.length);
39164
- const symbolData = message.find(item => item.Symbol === this.symbol);
39745
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
39165
39746
  console.log('[IntradayChartWidget] Found symbol data:', symbolData);
39166
39747
  if (symbolData && !symbolData.NotFound) {
39167
39748
  priceData = symbolData;
39168
39749
  }
39169
39750
  }
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
39751
  // Handle direct data format
39178
39752
  else if (message.Symbol === this.symbol && !message.NotFound) {
39179
39753
  console.log('[IntradayChartWidget] Processing direct format');
@@ -40276,7 +40850,19 @@ class ONBBOLevel2Widget extends BaseWidget {
40276
40850
  this.showLoading();
40277
40851
 
40278
40852
  // Fetch company info for initial symbol
40279
- await this.validateInitialSymbol();
40853
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40854
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40855
+ // Extract company info from first data item
40856
+ if (data && data[0]) {
40857
+ this.companyName = data[0].comp_name || '';
40858
+ this.exchangeName = data[0].market_name || '';
40859
+ }
40860
+ });
40861
+
40862
+ // If validation failed due to access/permission issues, stop initialization
40863
+ if (validationSuccess === false) {
40864
+ return; // Error is already shown, don't continue
40865
+ }
40280
40866
 
40281
40867
  // Set timeout to detect no data on initial load
40282
40868
  this.loadingTimeout = setTimeout(() => {
@@ -40288,36 +40874,6 @@ class ONBBOLevel2Widget extends BaseWidget {
40288
40874
  }, 10000);
40289
40875
  this.subscribeToData();
40290
40876
  }
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
40877
  subscribeToData() {
40322
40878
  // Subscribe to ONBBO Level 2 data (order book)
40323
40879
  // Use separate widget IDs to avoid "already active" duplicate detection
@@ -40836,6 +41392,7 @@ class TimeSalesWidget extends BaseWidget {
40836
41392
  this.styled = options.styled !== undefined ? options.styled : true;
40837
41393
  this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
40838
41394
  this.data = new TimeSalesModel([], this.maxTrades);
41395
+ this.data.symbol = this.symbol; // Set the symbol in the data model
40839
41396
  this.isDestroyed = false;
40840
41397
  this.unsubscribe = null;
40841
41398
  this.symbolEditor = null;
@@ -40847,6 +41404,7 @@ class TimeSalesWidget extends BaseWidget {
40847
41404
 
40848
41405
  // Create widget structure
40849
41406
  this.createWidgetStructure();
41407
+ this.initializeSymbolEditor();
40850
41408
 
40851
41409
  // Initialize the widget
40852
41410
  this.initialize();
@@ -40863,7 +41421,26 @@ class TimeSalesWidget extends BaseWidget {
40863
41421
  }
40864
41422
  }
40865
41423
  this.addStyles();
40866
- this.setupSymbolEditor();
41424
+ //this.setupSymbolEditor();
41425
+ }
41426
+ initializeSymbolEditor() {
41427
+ // Initialize symbol editor with stock symbol validation
41428
+ this.symbolEditor = new SymbolEditor(this, {
41429
+ maxLength: 10,
41430
+ placeholder: 'Enter symbol...',
41431
+ //validator: this.validateStockSymbol.bind(this),
41432
+ onSymbolChange: this.handleSymbolChange.bind(this),
41433
+ debug: this.debug,
41434
+ autoUppercase: true,
41435
+ symbolType: 'quotel1'
41436
+ });
41437
+
41438
+ // Set initial symbol
41439
+ const symbolElement = this.container.querySelector('.symbol');
41440
+ if (symbolElement) {
41441
+ symbolElement.textContent = this.symbol;
41442
+ symbolElement.dataset.originalSymbol = this.symbol;
41443
+ }
40867
41444
  }
40868
41445
  addStyles() {
40869
41446
  // Inject styles from the styles file into document head
@@ -40886,6 +41463,8 @@ class TimeSalesWidget extends BaseWidget {
40886
41463
  // Keep editor open when clicking buttons
40887
41464
  symbolType: 'quotel1' // Use standard quote validation
40888
41465
  });
41466
+
41467
+ //console.log('SETUP COMPLETE')
40889
41468
  }
40890
41469
  async handleSymbolChange(newSymbol, oldSymbol) {
40891
41470
  let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
@@ -40988,7 +41567,19 @@ class TimeSalesWidget extends BaseWidget {
40988
41567
  this.showLoading();
40989
41568
 
40990
41569
  // Validate initial symbol via API to get company info
40991
- await this.validateInitialSymbol();
41570
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
41571
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
41572
+ // Extract company info from first data item
41573
+ if (data && data[0]) {
41574
+ this.companyName = data[0].comp_name || '';
41575
+ this.exchangeName = data[0].market_name || '';
41576
+ }
41577
+ });
41578
+
41579
+ // If validation failed due to access/permission issues, stop initialization
41580
+ if (validationSuccess === false) {
41581
+ return; // Error is already shown, don't continue
41582
+ }
40992
41583
 
40993
41584
  // Set timeout to detect no data on initial load
40994
41585
  this.loadingTimeout = setTimeout(() => {
@@ -41001,36 +41592,6 @@ class TimeSalesWidget extends BaseWidget {
41001
41592
 
41002
41593
  this.subscribeToData();
41003
41594
  }
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
41595
  subscribeToData() {
41035
41596
  // Subscribe to Time & Sales data (using queryonbbotimesale)
41036
41597
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
@@ -41046,6 +41607,9 @@ class TimeSalesWidget extends BaseWidget {
41046
41607
  clearTimeout(this.loadingTimeout);
41047
41608
  this.loadingTimeout = null;
41048
41609
  }
41610
+ if (message.type != 'queryonbbotimesale') {
41611
+ return;
41612
+ }
41049
41613
 
41050
41614
  // Handle error messages
41051
41615
  if (message.type === 'error') {
@@ -41058,30 +41622,16 @@ class TimeSalesWidget extends BaseWidget {
41058
41622
  }
41059
41623
 
41060
41624
  // Handle Time & Sales data - Array of trades
41061
- if (Array.isArray(message)) {
41625
+ if (Array.isArray(message.data)) {
41062
41626
  if (this.debug) {
41063
41627
  console.log('[TimeSalesWidget] Received trades array:', message);
41064
41628
  }
41065
41629
 
41066
41630
  // Add new trades to existing data
41067
- this.data.addTrades(message);
41631
+ this.data.addTrades(message.data);
41068
41632
  this.updateWidget();
41069
41633
  return;
41070
41634
  }
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
41635
  if (this.debug) {
41086
41636
  console.log('[TimeSalesWidget] Unexpected message format:', message);
41087
41637
  }
@@ -41308,7 +41858,15 @@ class ApiService {
41308
41858
  try {
41309
41859
  const response = await fetch(url, config);
41310
41860
  if (!response.ok) {
41311
- throw new Error(`HTTP error! status: ${response.status}`);
41861
+ // Try to get response body for error details
41862
+ let errorBody = '';
41863
+ try {
41864
+ errorBody = await response.text();
41865
+ console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
41866
+ } catch (e) {
41867
+ console.error(`[ApiService] Could not read error response body:`, e);
41868
+ }
41869
+ throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
41312
41870
  }
41313
41871
  return await response.json();
41314
41872
  } catch (error) {
@@ -42068,8 +42626,8 @@ class WebSocketManager {
42068
42626
  */
42069
42627
  _normalizeMessage(message) {
42070
42628
  // Check for error field (case-insensitive)
42071
- const errorField = message.error || message.Error;
42072
- const typeField = message.type || message.Type;
42629
+ const errorField = message.error;
42630
+ const typeField = message.type;
42073
42631
  const messageField = message.message || message.Message;
42074
42632
 
42075
42633
  // Handle explicit error messages - route based on type field
@@ -42109,54 +42667,16 @@ class WebSocketManager {
42109
42667
  }
42110
42668
  }
42111
42669
 
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
42670
  // Old format or no type/data structure, return as is
42149
42671
  return message;
42150
42672
  }
42151
42673
  _routeMessage(message) {
42152
42674
  //console.log('message', message);
42153
42675
 
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
- }
42676
+ // IMPORTANT: Don't filter out empty arrays here - route them based on message.type
42677
+ // Widgets need to receive empty arrays to clear loading states and show "no data" messages
42678
+ // The message structure is: { type: 'queryoptionchain', data: [] }
42679
+ // We need to route based on the 'type' field, not the data content
42160
42680
 
42161
42681
  // Cache the message for later use by new subscribers
42162
42682
  this._cacheMessage(message);
@@ -42255,27 +42775,34 @@ class WebSocketManager {
42255
42775
  _getRelevantWidgets(message) {
42256
42776
  const relevantWidgets = new Set();
42257
42777
 
42778
+ // OPTIMIZATION: Extract message type once from the entire message, not from each data item
42779
+ // Message structure: { type: 'queryoptionchain', data: [] }
42780
+ let dataField = message.Data || message.data;
42781
+ const messageType = message.type || this._extractMessageType(dataField);
42782
+
42258
42783
  // Handle array messages
42259
- this._addRelevantWidgetsForItem(message, relevantWidgets);
42784
+ this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
42260
42785
  return relevantWidgets;
42261
42786
  }
42262
- _addRelevantWidgetsForItem(item, relevantWidgets) {
42787
+ _addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
42263
42788
  // If item has Data array, process it
42264
42789
  if (item.Data && Array.isArray(item.Data)) {
42265
42790
  //console.log('Data array found with length:', item.Data.length);
42266
42791
  item.Data.forEach(dataItem => {
42267
- this._processDataItem(dataItem, relevantWidgets);
42792
+ // Pass messageType from parent message, don't extract from each data item
42793
+ this._processDataItem(dataItem, relevantWidgets, messageType);
42268
42794
  });
42269
- } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42270
- this._processOptionChainData(item, relevantWidgets);
42795
+ } else if (item.data && item.data[0] && item.data[0].Strike !== undefined && item.data[0].Expire && !item.data[0].underlyingSymbol) {
42796
+ this._processOptionChainData(item.data, relevantWidgets);
42271
42797
  } else {
42272
- // Process single item
42273
- this._processDataItem(item, relevantWidgets);
42798
+ // Process single item - messageType already extracted from message
42799
+ this._processDataItem(item, relevantWidgets, messageType);
42274
42800
  }
42275
42801
  }
42276
42802
 
42277
42803
  // Simplified option chain processing - handle entire array at once
42278
42804
  _processOptionChainData(optionChainArray, relevantWidgets) {
42805
+ //console.log('PROCESSING DATA OPTIONS')
42279
42806
  if (!optionChainArray || optionChainArray.length === 0) return;
42280
42807
  // Extract underlying symbol and date from first option contract
42281
42808
  const firstOption = optionChainArray[0];
@@ -42314,12 +42841,11 @@ class WebSocketManager {
42314
42841
  }
42315
42842
  }
42316
42843
  }
42317
- _processDataItem(dataItem, relevantWidgets) {
42844
+ _processDataItem(dataItem, relevantWidgets, messageType) {
42318
42845
  // Process individual data items and route to appropriate widgets
42319
42846
  // Option chain arrays are handled separately by _processOptionChainData
42320
42847
 
42321
42848
  const symbol = this._extractSymbol(dataItem);
42322
- const messageType = this._extractMessageType(dataItem);
42323
42849
  if (this.config.debug) {
42324
42850
  console.log('[WebSocketManager] Processing data item:', {
42325
42851
  symbol,
@@ -42479,25 +43005,15 @@ class WebSocketManager {
42479
43005
  return symbolMappings[extracted] || extracted;
42480
43006
  }
42481
43007
  _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
43008
+ // FALLBACK: Infer type from data structure (for legacy/malformed messages)
42491
43009
 
42492
43010
  // IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
42493
43011
  // Level 2 ONBBO data - array of MMID objects or object with MMID field
42494
43012
  if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
42495
43013
  return 'queryonbbol2';
42496
43014
  }
42497
-
42498
- // Check for single MMID object
42499
- if (item.MMID !== undefined) {
42500
- return 'queryonbbol2';
43015
+ if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
43016
+ return 'queryoptionchain';
42501
43017
  }
42502
43018
 
42503
43019
  // Check for Source field AFTER MMID check