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.
package/dist/mdas-sdk.js CHANGED
@@ -412,6 +412,85 @@
412
412
  console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
413
413
  }
414
414
  }
415
+
416
+ /**
417
+ * Validate initial symbol with the API and handle access/permission errors
418
+ * This is a reusable method for all widgets that need to validate symbols on initialization
419
+ *
420
+ * @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
421
+ * @param {string} symbol - The symbol to validate
422
+ * @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
423
+ * @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
424
+ */
425
+ async validateInitialSymbol(apiMethod, symbol) {
426
+ let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
427
+ try {
428
+ const apiService = this.wsManager?.getApiService();
429
+ if (!apiService) {
430
+ if (this.debug) {
431
+ console.log(`[${this.constructor.name}] API service not available for initial validation`);
432
+ }
433
+ return true; // Continue without validation
434
+ }
435
+ if (!apiService[apiMethod]) {
436
+ console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
437
+ return true; // Continue without validation
438
+ }
439
+ const result = await apiService[apiMethod](symbol);
440
+ if (result && result.data && result.data.length > 0) {
441
+ if (this.debug) {
442
+ console.log(`[${this.constructor.name}] Initial symbol validated:`, {
443
+ symbol,
444
+ method: apiMethod,
445
+ dataCount: result.data.length
446
+ });
447
+ }
448
+
449
+ // Call success callback if provided
450
+ if (onSuccess && typeof onSuccess === 'function') {
451
+ onSuccess(result.data, result);
452
+ }
453
+ return true; // Validation successful
454
+ }
455
+
456
+ // No data returned - might be access issue, but continue anyway
457
+ if (this.debug) {
458
+ console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
459
+ }
460
+ return true; // Continue anyway, WebSocket might still work
461
+ } catch (error) {
462
+ if (this.debug) {
463
+ console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
464
+ }
465
+
466
+ // Check if it's an access/permission error
467
+ const errorMessage = (error.message || '').toLowerCase();
468
+ const isAccessError = errorMessage.includes('400') ||
469
+ // Bad Request (often used for access issues)
470
+ errorMessage.includes('401') ||
471
+ // Unauthorized
472
+ errorMessage.includes('403') ||
473
+ // Forbidden
474
+ errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
475
+ // Specific OPRA access denial
476
+ errorMessage.includes('permission denied') || errorMessage.includes('access denied');
477
+ if (isAccessError) {
478
+ // Hide loading and show access error
479
+ this.hideLoading();
480
+
481
+ // Extract more specific error message if available
482
+ let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
483
+ if (errorMessage.includes('no opra access')) {
484
+ userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
485
+ }
486
+ this.showError(userMessage);
487
+ return false; // Stop initialization
488
+ }
489
+
490
+ // For other errors, continue with WebSocket (might still work)
491
+ return true;
492
+ }
493
+ }
415
494
  }
416
495
 
417
496
  // src/models/MarketDataModel.js
@@ -2510,28 +2589,6 @@
2510
2589
  font-size: 1.71em;
2511
2590
  font-weight: 700;
2512
2591
  color: #111827;
2513
- cursor: pointer;
2514
- transition: all 0.2s ease;
2515
- position: relative;
2516
- }
2517
-
2518
- .time-sales-widget .symbol.editable-symbol:hover {
2519
- opacity: 0.8;
2520
- transform: scale(1.02);
2521
- }
2522
-
2523
- .time-sales-widget .symbol.editable-symbol:hover::after {
2524
- content: "✎";
2525
- position: absolute;
2526
- top: -8px;
2527
- right: -8px;
2528
- background: #3b82f6;
2529
- color: white;
2530
- font-size: 10px;
2531
- padding: 2px 4px;
2532
- border-radius: 4px;
2533
- opacity: 0.8;
2534
- pointer-events: none;
2535
2592
  }
2536
2593
 
2537
2594
  /* ========================================
@@ -2806,8 +2863,36 @@
2806
2863
  }
2807
2864
  `;
2808
2865
 
2866
+ /**
2867
+ * @deprecated SharedStyles is deprecated and will be removed in a future version.
2868
+ *
2869
+ * Please migrate to BaseStyles + CommonWidgetPatterns:
2870
+ *
2871
+ * BEFORE:
2872
+ * import { SharedStyles } from './styles/index.js';
2873
+ * export const MyWidgetStyles = `${SharedStyles} ...widget styles...`;
2874
+ *
2875
+ * AFTER:
2876
+ * import { BaseStyles } from './styles/BaseStyles.js';
2877
+ * import { getLoadingOverlayStyles, getErrorStyles } from './styles/CommonWidgetPatterns.js';
2878
+ * export const MyWidgetStyles = `
2879
+ * ${BaseStyles}
2880
+ * ${getLoadingOverlayStyles('my-widget')}
2881
+ * ${getErrorStyles('my-widget')}
2882
+ * ...widget-specific styles...
2883
+ * `;
2884
+ *
2885
+ * Benefits of migration:
2886
+ * - Proper CSS scoping (no global conflicts)
2887
+ * - Reduced code duplication
2888
+ * - Better maintainability
2889
+ * - Smaller bundle size
2890
+ *
2891
+ * See STYLING_CUSTOMIZATION_GUIDE.md for details.
2892
+ */
2809
2893
  const SharedStyles = `
2810
2894
  /* ========================================
2895
+ ⚠️ DEPRECATED - Use BaseStyles.js instead
2811
2896
  FONT SIZE SYSTEM - CSS Variables
2812
2897
  Adjust --mdas-base-font-size to scale all fonts
2813
2898
  ======================================== */
@@ -4483,99 +4568,46 @@
4483
4568
  this.showLoading();
4484
4569
 
4485
4570
  // Validate initial symbol via API to get company info
4486
- await this.validateInitialSymbol();
4487
- this.subscribeToData();
4488
- }
4489
- async validateInitialSymbol() {
4490
- try {
4491
- const apiService = this.wsManager.getApiService();
4492
- if (!apiService) {
4493
- if (this.debug) {
4494
- console.log('[MarketDataWidget] API service not available for initial validation');
4495
- }
4496
- return;
4571
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
4572
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
4573
+ // Store for use in updateWidget
4574
+ if (data && data[0]) {
4575
+ this.initialValidationData = data[0];
4497
4576
  }
4498
- const result = await apiService.quotel1(this.symbol);
4499
- if (result && result.data && result.data[0]) {
4500
- const symbolData = result.data[0];
4501
- // Store for use in updateWidget
4502
- this.initialValidationData = symbolData;
4503
- if (this.debug) {
4504
- console.log('[MarketDataWidget] Initial symbol validated:', {
4505
- symbol: this.symbol,
4506
- companyName: symbolData.comp_name,
4507
- exchangeName: symbolData.market_name
4508
- });
4509
- }
4510
- }
4511
- } catch (error) {
4512
- if (this.debug) {
4513
- console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
4514
- }
4515
- // Don't throw - let the widget continue with WebSocket data
4577
+ });
4578
+
4579
+ // If validation failed due to access/permission issues, stop initialization
4580
+ if (validationSuccess === false) {
4581
+ return; // Error is already shown, don't continue
4516
4582
  }
4583
+ this.subscribeToData();
4517
4584
  }
4518
4585
  subscribeToData() {
4519
4586
  // Subscribe with symbol for routing
4520
4587
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
4521
4588
  );
4522
-
4523
- // Send subscription message
4524
- /* this.wsManager.send({
4525
- type: 'queryl1',
4526
- symbol: this.symbol
4527
- }); */
4528
4589
  }
4590
+ handleData(message) {
4591
+ //console.log('DEBUG', message)
4529
4592
 
4530
- /* handleMessage(messageWrapper) {
4531
- if (this.isDestroyed) return;
4532
- try {
4533
- const { event, data } = messageWrapper;
4534
- if (this.debug) {
4535
- console.log('[MarketDataWidget] Received:', event, data);
4536
- }
4537
- if (event === 'connection') {
4538
- this.handleConnectionStatus(data);
4539
- return;
4540
- }
4541
- if (event === 'data') {
4542
- this.handleData(data);
4543
- }
4544
- if (event === 'session_revoked') {
4545
- if (data.status === 'attempting_relogin') {
4546
- this.showLoading();
4547
- } else if (data.status === 'relogin_failed') {
4548
- this.showError(data.error);
4549
- } else if (data.status === 'relogin_successful') {
4550
- this.hideLoading();
4551
- }
4552
- return;
4553
- }
4554
- } catch (error) {
4555
- console.error('[MarketDataWidget] Error handling message:', error);
4556
- this.showError('Error processing data');
4593
+ // Check for error: false and display message if present
4594
+ if (message.error === true) {
4595
+ if (this.debug) {
4596
+ console.log('[MarketDataWidget] Received error response:', message.message);
4557
4597
  }
4558
- } */
4598
+ const errorMsg = message.message || message.Message;
4559
4599
 
4560
- /* handleConnectionStatus(status) {
4561
- if (status.status === 'connected') {
4562
- if (this.debug) {
4563
- console.log('[MarketDataWidget] Connected to WebSocket');
4564
- }
4565
- // Re-send subscription when reconnected
4566
- this.wsManager.send({
4567
- type: 'queryl1',
4568
- symbol: this.symbol
4569
- });
4570
- } else if (status.status === 'disconnected') {
4571
- this.showError('Disconnected from data service');
4572
- } else if (status.status === 'error') {
4573
- this.showError(status.error || 'Connection error');
4600
+ // If there's a message field, show it as an error
4601
+ if (errorMsg) {
4602
+ this.hideLoading();
4603
+ this.showError(errorMsg);
4604
+ return;
4574
4605
  }
4575
- } */
4576
-
4577
- handleData(message) {
4578
- console.log('DEBUG', message);
4606
+ } else if (message.Data && typeof message.Data === 'string') {
4607
+ this.hideLoading();
4608
+ this.showError(message.Message);
4609
+ return;
4610
+ }
4579
4611
  if (this.loadingTimeout) {
4580
4612
  clearTimeout(this.loadingTimeout);
4581
4613
  this.loadingTimeout = null;
@@ -4661,126 +4693,6 @@
4661
4693
  this.showConnectionQuality(); // Show cache indicator
4662
4694
  }
4663
4695
  }
4664
- showNoDataState() {
4665
- let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
4666
- if (this.isDestroyed) return;
4667
- try {
4668
- // Hide loading overlay
4669
- this.hideLoading();
4670
-
4671
- // Clear any existing errors
4672
- this.clearError();
4673
-
4674
- // Update header with dimmed styling and basic info from data
4675
- const symbol = data.Symbol || this.symbol;
4676
-
4677
- // Add null checks for all DOM elements
4678
- const symbolElement = this.container.querySelector('.symbol');
4679
- if (symbolElement) {
4680
- symbolElement.textContent = symbol;
4681
- }
4682
- const companyNameElement = this.container.querySelector('.company-name');
4683
- if (companyNameElement) {
4684
- companyNameElement.textContent = `${symbol} Inc`;
4685
- }
4686
-
4687
- // Set price to $0.00 and add dimmed class
4688
- const currentPriceElement = this.container.querySelector('.current-price');
4689
- if (currentPriceElement) {
4690
- currentPriceElement.textContent = '$0.00';
4691
- }
4692
- const changeElement = this.container.querySelector('.price-change');
4693
- if (changeElement) {
4694
- const changeValueElement = changeElement.querySelector('.change-value');
4695
- const changePercentElement = changeElement.querySelector('.change-percent');
4696
- if (changeValueElement) {
4697
- changeValueElement.textContent = '+0.00';
4698
- }
4699
- if (changePercentElement) {
4700
- changePercentElement.textContent = ' (0.00%)';
4701
- }
4702
- changeElement.classList.remove('positive', 'negative');
4703
- changeElement.classList.add('neutral');
4704
- }
4705
-
4706
- // Add dimmed styling to header
4707
- const widgetHeader = this.container.querySelector('.widget-header');
4708
- if (widgetHeader) {
4709
- widgetHeader.classList.add('dimmed');
4710
- }
4711
-
4712
- // Replace the data grid with no data message
4713
- this.showNoDataMessage(symbol, data);
4714
-
4715
- // Update footer with current timestamp
4716
- const lastUpdateElement = this.container.querySelector('.last-update');
4717
- if (lastUpdateElement) {
4718
- const timestamp = formatTimestampET();
4719
- lastUpdateElement.textContent = `Checked: ${timestamp}`;
4720
- }
4721
- const dataSourceElement = this.container.querySelector('.data-source');
4722
- if (dataSourceElement) {
4723
- dataSourceElement.textContent = 'Source: No data available';
4724
- }
4725
- } catch (error) {
4726
- console.error('Error showing no data state:', error);
4727
- this.showError('Error displaying no data state');
4728
- }
4729
- }
4730
- showNoDataMessage(symbol) {
4731
- const dataGrid = this.container.querySelector('.data-grid');
4732
-
4733
- // Hide the data grid if it exists
4734
- if (dataGrid) {
4735
- dataGrid.style.display = 'none';
4736
- }
4737
-
4738
- // Remove existing no data message if present
4739
- const existingNoData = this.container.querySelector('.no-data-state');
4740
- if (existingNoData) {
4741
- existingNoData.remove();
4742
- }
4743
-
4744
- // Create no data message element - safely without XSS risk
4745
- const noDataElement = createElement('div', '', 'no-data-state');
4746
- const noDataContent = createElement('div', '', 'no-data-content');
4747
-
4748
- // Create icon
4749
- const iconDiv = document.createElement('div');
4750
- iconDiv.className = 'no-data-icon';
4751
- iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4752
- <circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
4753
- <path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
4754
- <circle cx="12" cy="16" r="1" fill="#9ca3af"/>
4755
- </svg>`;
4756
- noDataContent.appendChild(iconDiv);
4757
-
4758
- // Create title
4759
- const title = createElement('h3', 'No Market Data', 'no-data-title');
4760
- noDataContent.appendChild(title);
4761
-
4762
- // Create description with sanitized symbol
4763
- const description = createElement('p', '', 'no-data-description');
4764
- description.appendChild(document.createTextNode('Market data for '));
4765
- const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
4766
- description.appendChild(symbolStrong);
4767
- description.appendChild(document.createTextNode(' was not found'));
4768
- noDataContent.appendChild(description);
4769
-
4770
- // Create guidance
4771
- const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
4772
- noDataContent.appendChild(guidance);
4773
- noDataElement.appendChild(noDataContent);
4774
-
4775
- // Insert before footer, with null check
4776
- const footer = this.container.querySelector('.widget-footer');
4777
- if (footer && footer.parentNode) {
4778
- footer.parentNode.insertBefore(noDataElement, footer);
4779
- } else {
4780
- // Fallback: append to container if footer not found
4781
- this.container.appendChild(noDataElement);
4782
- }
4783
- }
4784
4696
  updateWidget(data) {
4785
4697
  if (this.isDestroyed) return;
4786
4698
  try {
@@ -5285,7 +5197,20 @@
5285
5197
  this.showLoading();
5286
5198
 
5287
5199
  // Validate initial symbol via API to get company info
5288
- await this.validateInitialSymbol();
5200
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
5201
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
5202
+ // Extract company info from first data item
5203
+ if (data && data[0]) {
5204
+ this.companyName = data[0].comp_name || '';
5205
+ this.exchangeName = data[0].market_name || '';
5206
+ this.mic = data[0].mic || '';
5207
+ }
5208
+ });
5209
+
5210
+ // If validation failed due to access/permission issues, stop initialization
5211
+ if (validationSuccess === false) {
5212
+ return; // Error is already shown, don't continue
5213
+ }
5289
5214
 
5290
5215
  // Set timeout to detect no data on initial load
5291
5216
  this.loadingTimeout = setTimeout(() => {
@@ -5302,38 +5227,6 @@
5302
5227
 
5303
5228
  this.subscribeToData();
5304
5229
  }
5305
- async validateInitialSymbol() {
5306
- try {
5307
- const apiService = this.wsManager.getApiService();
5308
- if (!apiService) {
5309
- if (this.debug) {
5310
- console.log('[NightSessionWidget] API service not available for initial validation');
5311
- }
5312
- return;
5313
- }
5314
- const result = await apiService.quotel1(this.symbol);
5315
- if (result && result.data && result.data[0]) {
5316
- const symbolData = result.data[0];
5317
- // Extract company info
5318
- this.companyName = symbolData.comp_name || '';
5319
- this.exchangeName = symbolData.market_name || '';
5320
- this.mic = symbolData.mic || '';
5321
- if (this.debug) {
5322
- console.log('[NightSessionWidget] Initial symbol validated:', {
5323
- symbol: this.symbol,
5324
- companyName: this.companyName,
5325
- exchangeName: this.exchangeName,
5326
- mic: this.mic
5327
- });
5328
- }
5329
- }
5330
- } catch (error) {
5331
- if (this.debug) {
5332
- console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
5333
- }
5334
- // Don't throw - let the widget continue with WebSocket data
5335
- }
5336
- }
5337
5230
  subscribeToData() {
5338
5231
  let subscriptionType;
5339
5232
  if (this.source === 'bruce') {
@@ -5353,6 +5246,7 @@
5353
5246
  }
5354
5247
  handleData(message) {
5355
5248
  console.log('DEBUG NIGHT', message);
5249
+
5356
5250
  //message = message.data || message.Data
5357
5251
 
5358
5252
  if (this.loadingTimeout) {
@@ -5360,64 +5254,41 @@
5360
5254
  this.loadingTimeout = null;
5361
5255
  }
5362
5256
 
5363
- // Handle error messages from server (plain text converted to structured format)
5364
- if (message.type === 'error' || message.error == true) {
5257
+ // Check for error: false and display message if present
5258
+ if (message.error === true) {
5365
5259
  if (this.debug) {
5366
- console.log('[NightSessionWidget] Received no data message:', message.message);
5260
+ console.log('[Nigh] Received error response:', message.message);
5367
5261
  }
5368
- // Only show no data state if we don't have cached data
5369
- if (!this.data) {
5370
- this.showNoDataState({
5371
- Symbol: this.symbol,
5372
- NotFound: true,
5373
- message: message.message
5374
- });
5375
- } else {
5262
+ const errorMsg = message.message || message.Message;
5263
+
5264
+ // If there's a message field, show it as an error
5265
+ if (errorMsg) {
5376
5266
  this.hideLoading();
5377
- if (this.debug) {
5378
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5379
- }
5267
+ this.showError(errorMsg);
5268
+ return;
5380
5269
  }
5270
+ } else if (message.Data && typeof message.Data === 'string') {
5271
+ this.hideLoading();
5272
+ this.showError(message.Message);
5381
5273
  return;
5382
5274
  }
5383
5275
 
5384
- // Handle general error messages - CHECK IF IT'S ACCESS RELATED
5276
+ // Handle error messages from server (plain text converted to structured format)
5385
5277
  if (message.type === 'error') {
5386
- const errorMsg = message.message || 'Server error';
5387
-
5388
- // If it's an access/permission error, show as no-data state instead of separate error
5389
- if (errorMsg.toLowerCase().includes('access') || errorMsg.toLowerCase().includes('permission') || errorMsg.toLowerCase().includes('denied')) {
5390
- // Only show no data state if we don't have cached data
5391
- if (!this.data) {
5392
- this.showNoDataState({
5393
- Symbol: this.symbol,
5394
- NotFound: true,
5395
- isAccessError: true,
5396
- message: errorMsg
5397
- });
5398
- } else {
5399
- this.hideLoading();
5400
- if (this.debug) {
5401
- console.log('[NightSessionWidget] Access error but keeping cached data');
5402
- }
5403
- }
5278
+ if (this.debug) {
5279
+ console.log('[NightSessionWidget] Received no data message:', message.message);
5280
+ }
5281
+ // Only show no data state if we don't have cached data
5282
+ if (!this.data) {
5283
+ this.showError(errorMsg);
5404
5284
  } else {
5405
- // Only show error if we don't have cached data
5406
- if (!this.data) {
5407
- console.log('errorMsg', errorMsg);
5408
- this.showError(errorMsg);
5409
- } else {
5410
- this.hideLoading();
5411
- if (this.debug) {
5412
- console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
5413
- }
5285
+ this.hideLoading();
5286
+ if (this.debug) {
5287
+ console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
5414
5288
  }
5415
5289
  }
5416
5290
  return;
5417
5291
  }
5418
-
5419
- // Filter for night session data
5420
-
5421
5292
  if (Array.isArray(message.data)) {
5422
5293
  // First, try to find data matching our symbol regardless of MarketName
5423
5294
  const symbolData = message.data.find(item => item.Symbol === this.symbol);
@@ -6318,36 +6189,20 @@
6318
6189
  this.showLoading();
6319
6190
 
6320
6191
  // Validate initial symbol via API to get symbol info
6321
- await this.validateInitialSymbol();
6322
- this.subscribeToData();
6323
- }
6324
- async validateInitialSymbol() {
6325
- try {
6326
- const apiService = this.wsManager.getApiService();
6327
- if (!apiService) {
6328
- if (this.debug) {
6329
- console.log('[OptionsWidget] API service not available for initial validation');
6330
- }
6331
- return;
6332
- }
6333
- const result = await apiService.quoteOptionl1(this.symbol);
6334
- if (result && result[0] && !result[0].error && !result[0].not_found) {
6335
- const symbolData = result[0];
6336
- // Store for use in updateWidget
6337
- this.initialValidationData = symbolData;
6338
- if (this.debug) {
6339
- console.log('[OptionsWidget] Initial symbol validated:', {
6340
- symbol: this.symbol,
6341
- underlying: symbolData.Underlying || symbolData.RootSymbol
6342
- });
6343
- }
6192
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
6193
+ const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
6194
+ // Store for use in updateWidget
6195
+ // Note: quoteOptionl1 returns array directly, not wrapped in data field
6196
+ if (data && data[0] && !data[0].error && !data[0].not_found) {
6197
+ this.initialValidationData = data[0];
6344
6198
  }
6345
- } catch (error) {
6346
- if (this.debug) {
6347
- console.warn('[OptionsWidget] Initial symbol validation failed:', error);
6348
- }
6349
- // Don't throw - let the widget continue with WebSocket data
6199
+ });
6200
+
6201
+ // If validation failed due to access/permission issues, stop initialization
6202
+ if (validationSuccess === false) {
6203
+ return; // Error is already shown, don't continue
6350
6204
  }
6205
+ this.subscribeToData();
6351
6206
  }
6352
6207
  subscribeToData() {
6353
6208
  // Subscribe with symbol for routing
@@ -6817,6 +6672,15 @@
6817
6672
  </select>
6818
6673
  <button class="fetch-button" disabled>Search</button>
6819
6674
  </div>
6675
+ <div class="filter-section">
6676
+ <label for="strike-filter" class="filter-label">Display:</label>
6677
+ <select class="strike-filter" id="strike-filter">
6678
+ <option value="5">Near the money (±5 strikes)</option>
6679
+ <option value="10">Near the money (±10 strikes)</option>
6680
+ <option value="15">Near the money (±15 strikes)</option>
6681
+ <option value="all">All strikes</option>
6682
+ </select>
6683
+ </div>
6820
6684
  </div>
6821
6685
 
6822
6686
  <!-- Data Grid Section -->
@@ -6873,11 +6737,383 @@
6873
6737
  </div>
6874
6738
  `;
6875
6739
 
6740
+ // src/widgets/styles/BaseStyles.js
6741
+
6742
+ /**
6743
+ * Base Styles - CSS Variables and Universal Utilities
6744
+ *
6745
+ * This file contains:
6746
+ * 1. CSS Custom Properties (variables) for consistent theming
6747
+ * 2. Responsive font sizing system
6748
+ * 3. Optional utility classes
6749
+ *
6750
+ * Usage:
6751
+ * import { BaseStyles } from './styles/BaseStyles';
6752
+ * export const MyWidgetStyles = `${BaseStyles} ...widget styles...`;
6753
+ */
6754
+
6755
+ const BaseStyles = `
6756
+ /* ============================================
6757
+ MDAS WIDGET CSS VARIABLES
6758
+ ============================================ */
6759
+
6760
+ :root {
6761
+ /* --- FONT SIZE SYSTEM --- */
6762
+ /* Base font size - all other sizes are relative to this */
6763
+ --mdas-base-font-size: 14px;
6764
+
6765
+ /* Component-specific font sizes (em units scale with base) */
6766
+ --mdas-small-text-size: 0.79em; /* 11px at 14px base */
6767
+ --mdas-medium-text-size: 0.93em; /* 13px at 14px base */
6768
+ --mdas-large-text-size: 1.14em; /* 16px at 14px base */
6769
+
6770
+ /* Widget element sizes */
6771
+ --mdas-company-name-size: 1.43em; /* 20px at 14px base */
6772
+ --mdas-symbol-size: 1.79em; /* 25px at 14px base */
6773
+ --mdas-price-size: 2.29em; /* 32px at 14px base */
6774
+ --mdas-change-size: 1.14em; /* 16px at 14px base */
6775
+
6776
+ /* Data display sizes */
6777
+ --mdas-label-size: 0.86em; /* 12px at 14px base */
6778
+ --mdas-value-size: 1em; /* 14px at 14px base */
6779
+ --mdas-footer-size: 0.79em; /* 11px at 14px base */
6780
+
6781
+ /* Chart-specific sizes */
6782
+ --mdas-chart-title-size: 1.29em; /* 18px at 14px base */
6783
+ --mdas-chart-label-size: 0.93em; /* 13px at 14px base */
6784
+ --mdas-chart-value-size: 1.43em; /* 20px at 14px base */
6785
+
6786
+ /* --- COLOR PALETTE (for future theming support) --- */
6787
+ /* Primary colors */
6788
+ --mdas-primary-color: #3b82f6;
6789
+ --mdas-primary-hover: #2563eb;
6790
+
6791
+ /* Status colors */
6792
+ --mdas-color-positive: #059669;
6793
+ --mdas-color-positive-bg: #d1fae5;
6794
+ --mdas-color-negative: #dc2626;
6795
+ --mdas-color-negative-bg: #fee2e2;
6796
+ --mdas-color-neutral: #6b7280;
6797
+
6798
+ /* Background colors */
6799
+ --mdas-bg-primary: #ffffff;
6800
+ --mdas-bg-secondary: #f9fafb;
6801
+ --mdas-bg-tertiary: #f3f4f6;
6802
+
6803
+ /* Text colors */
6804
+ --mdas-text-primary: #111827;
6805
+ --mdas-text-secondary: #6b7280;
6806
+ --mdas-text-tertiary: #9ca3af;
6807
+
6808
+ /* Border colors */
6809
+ --mdas-border-primary: #e5e7eb;
6810
+ --mdas-border-secondary: #d1d5db;
6811
+
6812
+ /* --- SPACING SYSTEM --- */
6813
+ --mdas-spacing-xs: 4px;
6814
+ --mdas-spacing-sm: 8px;
6815
+ --mdas-spacing-md: 16px;
6816
+ --mdas-spacing-lg: 24px;
6817
+ --mdas-spacing-xl: 32px;
6818
+
6819
+ /* --- EFFECTS --- */
6820
+ --mdas-border-radius: 8px;
6821
+ --mdas-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
6822
+ --mdas-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
6823
+ --mdas-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
6824
+
6825
+ /* --- Z-INDEX LAYERS --- */
6826
+ --mdas-z-base: 1;
6827
+ --mdas-z-sticky: 10;
6828
+ --mdas-z-overlay: 100;
6829
+ --mdas-z-modal: 10000;
6830
+ }
6831
+
6832
+ /* ============================================
6833
+ RESPONSIVE FONT SCALING
6834
+ ============================================ */
6835
+
6836
+ /* Tablet breakpoint - slightly smaller fonts */
6837
+ @media (max-width: 768px) {
6838
+ :root {
6839
+ --mdas-base-font-size: 13px;
6840
+ }
6841
+ }
6842
+
6843
+ /* Mobile breakpoint - smaller fonts for small screens */
6844
+ @media (max-width: 480px) {
6845
+ :root {
6846
+ --mdas-base-font-size: 12px;
6847
+ }
6848
+ }
6849
+
6850
+ /* ============================================
6851
+ OPTIONAL UTILITY CLASSES
6852
+ Use these classes for consistent styling
6853
+ ============================================ */
6854
+
6855
+ /* Widget card styling - apply to widget root element */
6856
+ .mdas-card {
6857
+ background: var(--mdas-bg-primary);
6858
+ border-radius: var(--mdas-border-radius);
6859
+ padding: var(--mdas-spacing-lg);
6860
+ box-shadow: var(--mdas-shadow-md);
6861
+ border: 1px solid var(--mdas-border-primary);
6862
+ }
6863
+
6864
+ /* Responsive container */
6865
+ .mdas-container {
6866
+ width: 100%;
6867
+ max-width: 1400px;
6868
+ margin: 0 auto;
6869
+ }
6870
+
6871
+ /* Text utilities */
6872
+ .mdas-text-primary {
6873
+ color: var(--mdas-text-primary);
6874
+ }
6875
+
6876
+ .mdas-text-secondary {
6877
+ color: var(--mdas-text-secondary);
6878
+ }
6879
+
6880
+ .mdas-text-muted {
6881
+ color: var(--mdas-text-tertiary);
6882
+ }
6883
+
6884
+ /* Status colors */
6885
+ .mdas-positive {
6886
+ color: var(--mdas-color-positive);
6887
+ }
6888
+
6889
+ .mdas-negative {
6890
+ color: var(--mdas-color-negative);
6891
+ }
6892
+
6893
+ .mdas-neutral {
6894
+ color: var(--mdas-color-neutral);
6895
+ }
6896
+ `;
6897
+
6898
+ // src/widgets/styles/CommonWidgetPatterns.js
6899
+
6900
+ /**
6901
+ * Common Widget Patterns - Reusable Style Functions
6902
+ *
6903
+ * These functions generate properly scoped CSS for common widget patterns.
6904
+ * Each function takes a widget class name and returns scoped CSS.
6905
+ *
6906
+ * Benefits:
6907
+ * - Eliminates code duplication
6908
+ * - Ensures proper scoping (no global conflicts)
6909
+ * - Consistent styling across widgets
6910
+ * - Easy to maintain and update
6911
+ *
6912
+ * Usage:
6913
+ * import { getLoadingOverlayStyles } from './CommonWidgetPatterns';
6914
+ * const styles = `${getLoadingOverlayStyles('my-widget')} ...other styles...`;
6915
+ */
6916
+
6917
+ /**
6918
+ * Generate loading overlay styles for a widget
6919
+ * @param {string} widgetClass - Widget class name (e.g., 'option-chain-widget')
6920
+ * @returns {string} Scoped CSS for loading overlay
6921
+ */
6922
+ const getLoadingOverlayStyles = widgetClass => `
6923
+ /* Loading Overlay for ${widgetClass} */
6924
+ .${widgetClass} .widget-loading-overlay {
6925
+ position: absolute;
6926
+ top: 0;
6927
+ left: 0;
6928
+ right: 0;
6929
+ bottom: 0;
6930
+ background: rgba(255, 255, 255, 0.95);
6931
+ backdrop-filter: blur(2px);
6932
+ display: flex;
6933
+ flex-direction: column;
6934
+ align-items: center;
6935
+ justify-content: center;
6936
+ z-index: var(--mdas-z-overlay, 100);
6937
+ border-radius: var(--mdas-border-radius, 12px);
6938
+ }
6939
+
6940
+ .${widgetClass} .widget-loading-overlay.hidden {
6941
+ display: none;
6942
+ }
6943
+
6944
+ .${widgetClass} .loading-content {
6945
+ display: flex;
6946
+ flex-direction: column;
6947
+ align-items: center;
6948
+ gap: 12px;
6949
+ }
6950
+
6951
+ .${widgetClass} .loading-spinner {
6952
+ width: 40px;
6953
+ height: 40px;
6954
+ border: 4px solid var(--mdas-border-primary, #e5e7eb);
6955
+ border-top-color: var(--mdas-primary-color, #3b82f6);
6956
+ border-radius: 50%;
6957
+ animation: ${widgetClass}-spin 1s linear infinite;
6958
+ }
6959
+
6960
+ .${widgetClass} .loading-text {
6961
+ color: var(--mdas-text-secondary, #6b7280);
6962
+ font-size: var(--mdas-medium-text-size, 0.93em);
6963
+ font-weight: 500;
6964
+ }
6965
+
6966
+ @keyframes ${widgetClass}-spin {
6967
+ to { transform: rotate(360deg); }
6968
+ }
6969
+ `;
6970
+
6971
+ /**
6972
+ * Generate widget error display styles
6973
+ * @param {string} widgetClass - Widget class name
6974
+ * @returns {string} Scoped CSS for error display
6975
+ */
6976
+ const getErrorStyles = widgetClass => `
6977
+ /* Error Display for ${widgetClass} */
6978
+ .${widgetClass} .widget-error {
6979
+ padding: 16px 20px;
6980
+ background: var(--mdas-color-negative-bg, #fee2e2);
6981
+ border: 1px solid #fecaca;
6982
+ border-radius: var(--mdas-border-radius, 8px);
6983
+ color: var(--mdas-color-negative, #dc2626);
6984
+ font-size: var(--mdas-medium-text-size, 0.93em);
6985
+ margin: 16px;
6986
+ text-align: center;
6987
+ }
6988
+
6989
+ .${widgetClass} .widget-error-container {
6990
+ display: flex;
6991
+ flex-direction: column;
6992
+ align-items: center;
6993
+ gap: 12px;
6994
+ padding: 24px;
6995
+ }
6996
+ `;
6997
+
6998
+ /**
6999
+ * Generate widget footer styles
7000
+ * @param {string} widgetClass - Widget class name
7001
+ * @returns {string} Scoped CSS for widget footer
7002
+ */
7003
+ const getFooterStyles = widgetClass => `
7004
+ /* Footer for ${widgetClass} */
7005
+ .${widgetClass} .widget-footer {
7006
+ display: flex;
7007
+ justify-content: space-between;
7008
+ align-items: center;
7009
+ padding: 8px 16px;
7010
+ background: var(--mdas-bg-secondary, #f9fafb);
7011
+ border-top: 1px solid var(--mdas-border-primary, #e5e7eb);
7012
+ font-size: var(--mdas-footer-size, 0.79em);
7013
+ color: var(--mdas-text-secondary, #6b7280);
7014
+ }
7015
+
7016
+ .${widgetClass} .widget-footer .last-update {
7017
+ color: var(--mdas-text-tertiary, #9ca3af);
7018
+ }
7019
+
7020
+ .${widgetClass} .widget-footer .data-source {
7021
+ color: var(--mdas-text-secondary, #6b7280);
7022
+ font-weight: 500;
7023
+ }
7024
+ `;
7025
+
7026
+ /**
7027
+ * Generate no-data state styles
7028
+ * @param {string} widgetClass - Widget class name
7029
+ * @returns {string} Scoped CSS for no-data state
7030
+ */
7031
+ const getNoDataStyles = widgetClass => `
7032
+ /* No Data State for ${widgetClass} */
7033
+ .${widgetClass} .no-data-state {
7034
+ display: flex;
7035
+ flex-direction: column;
7036
+ align-items: center;
7037
+ justify-content: center;
7038
+ padding: 40px 20px;
7039
+ text-align: center;
7040
+ color: var(--mdas-text-secondary, #6b7280);
7041
+ }
7042
+
7043
+ .${widgetClass} .no-data-content {
7044
+ max-width: 400px;
7045
+ display: flex;
7046
+ flex-direction: column;
7047
+ align-items: center;
7048
+ gap: 16px;
7049
+ }
7050
+
7051
+ .${widgetClass} .no-data-icon {
7052
+ width: 64px;
7053
+ height: 64px;
7054
+ display: flex;
7055
+ align-items: center;
7056
+ justify-content: center;
7057
+ opacity: 0.5;
7058
+ }
7059
+
7060
+ .${widgetClass} .no-data-icon svg {
7061
+ width: 100%;
7062
+ height: 100%;
7063
+ }
7064
+
7065
+ .${widgetClass} .no-data-title {
7066
+ font-size: var(--mdas-large-text-size, 1.14em);
7067
+ font-weight: 600;
7068
+ color: var(--mdas-text-primary, #111827);
7069
+ margin: 0;
7070
+ }
7071
+
7072
+ .${widgetClass} .no-data-description {
7073
+ font-size: var(--mdas-medium-text-size, 0.93em);
7074
+ color: var(--mdas-text-secondary, #6b7280);
7075
+ margin: 0;
7076
+ line-height: 1.5;
7077
+ }
7078
+
7079
+ .${widgetClass} .no-data-description strong {
7080
+ color: var(--mdas-text-primary, #111827);
7081
+ font-weight: 600;
7082
+ }
7083
+
7084
+ .${widgetClass} .no-data-guidance {
7085
+ font-size: var(--mdas-small-text-size, 0.79em);
7086
+ color: var(--mdas-text-tertiary, #9ca3af);
7087
+ margin: 0;
7088
+ }
7089
+
7090
+ /* Error variant of no-data state */
7091
+ .${widgetClass} .no-data-state.error-access {
7092
+ color: var(--mdas-color-negative, #dc2626);
7093
+ }
7094
+
7095
+ .${widgetClass} .no-data-state.error-access .no-data-title {
7096
+ color: var(--mdas-color-negative, #dc2626);
7097
+ }
7098
+
7099
+ .${widgetClass} .no-data-state.error-access .no-data-icon {
7100
+ opacity: 0.8;
7101
+ }
7102
+
7103
+ .${widgetClass} .no-data-state.error-access .no-data-icon svg circle[fill] {
7104
+ fill: var(--mdas-color-negative, #dc2626);
7105
+ }
7106
+ `;
7107
+
6876
7108
  // src/widgets/styles/OptionChainStyles.js
6877
7109
  const OptionChainStyles = `
6878
- ${SharedStyles}
7110
+ ${BaseStyles}
7111
+ ${getLoadingOverlayStyles('option-chain-widget')}
7112
+ ${getErrorStyles('option-chain-widget')}
7113
+ ${getFooterStyles('option-chain-widget')}
7114
+ ${getNoDataStyles('option-chain-widget')}
6879
7115
 
6880
- /* Base styles remain the same until responsive section */
7116
+ /* Option Chain Widget Specific Styles */
6881
7117
  .option-chain-widget {
6882
7118
  border: 1px solid #e5e7eb;
6883
7119
  border-radius: 8px;
@@ -6902,8 +7138,22 @@ ${SharedStyles}
6902
7138
  flex-wrap: wrap;
6903
7139
  }
6904
7140
 
7141
+ .option-chain-widget .filter-section {
7142
+ display: flex;
7143
+ gap: 8px;
7144
+ align-items: center;
7145
+ margin-top: 8px;
7146
+ }
7147
+
7148
+ .option-chain-widget .filter-label {
7149
+ font-size: 13px;
7150
+ font-weight: 500;
7151
+ color: #374151;
7152
+ }
7153
+
6905
7154
  .option-chain-widget .symbol-input,
6906
- .option-chain-widget .date-select {
7155
+ .option-chain-widget .date-select,
7156
+ .option-chain-widget .strike-filter {
6907
7157
  padding: 8px 12px;
6908
7158
  border: 1px solid #d1d5db;
6909
7159
  border-radius: 4px;
@@ -6924,6 +7174,12 @@ ${SharedStyles}
6924
7174
  background: white;
6925
7175
  }
6926
7176
 
7177
+ .option-chain-widget .strike-filter {
7178
+ min-width: 200px;
7179
+ background: white;
7180
+ cursor: pointer;
7181
+ }
7182
+
6927
7183
  .option-chain-widget .fetch-button {
6928
7184
  padding: 8px 16px;
6929
7185
  background: #3b82f6;
@@ -7079,10 +7335,22 @@ ${SharedStyles}
7079
7335
  min-height: 32px;
7080
7336
  }
7081
7337
 
7338
+ /* In-the-money highlighting */
7339
+ .option-chain-widget .calls-data.in-the-money span,
7340
+ .option-chain-widget .puts-data.in-the-money span {
7341
+ background-color: #fffbeb;
7342
+ }
7343
+
7082
7344
  .option-chain-widget .option-row:hover {
7083
7345
  background: #f9fafb;
7084
7346
  }
7085
- .option-chain-widget .option-row:hover .calls-data span,
7347
+
7348
+ .option-chain-widget .option-row:hover .calls-data.in-the-money span,
7349
+ .option-chain-widget .option-row:hover .puts-data.in-the-money span {
7350
+ background-color: #fef3c7;
7351
+ }
7352
+
7353
+ .option-chain-widget .option-row:hover .calls-data span,
7086
7354
  .option-chain-widget .option-row:hover .puts-data span,
7087
7355
  .option-chain-widget .option-row:hover .strike-data {
7088
7356
  background: #f9fafb;
@@ -7106,14 +7374,32 @@ ${SharedStyles}
7106
7374
  .option-chain-widget .puts-data span {
7107
7375
  padding: 6px 4px;
7108
7376
  text-align: center;
7109
- font-size: 12px; /* Changed from 12px to 14px */
7110
- font-weight: 400; /* Added to match data values in other widgets */
7111
- color: #111827; /* Changed from #374151 to match other widgets */
7377
+ font-size: 12px;
7378
+ font-weight: 400;
7379
+ color: #111827;
7112
7380
  overflow: hidden;
7113
7381
  text-overflow: ellipsis;
7114
7382
  white-space: nowrap;
7115
7383
  }
7116
7384
 
7385
+ /* Override color for change values with positive/negative classes */
7386
+ .option-chain-widget .calls-data span.positive,
7387
+ .option-chain-widget .puts-data span.positive {
7388
+ color: #059669 !important;
7389
+ font-weight: 500;
7390
+ }
7391
+
7392
+ .option-chain-widget .calls-data span.negative,
7393
+ .option-chain-widget .puts-data span.negative {
7394
+ color: #dc2626 !important;
7395
+ font-weight: 500;
7396
+ }
7397
+
7398
+ .option-chain-widget .calls-data span.neutral,
7399
+ .option-chain-widget .puts-data span.neutral {
7400
+ color: #6b7280 !important;
7401
+ }
7402
+
7117
7403
  .option-chain-widget .contract-cell {
7118
7404
  font-size: 10px;
7119
7405
  color: #6b7280;
@@ -7125,78 +7411,42 @@ ${SharedStyles}
7125
7411
  white-space: nowrap;
7126
7412
  }
7127
7413
 
7128
- .option-chain-widget .positive {
7129
- color: #059669;
7130
- }
7131
-
7132
- .option-chain-widget .negative {
7133
- color: #dc2626;
7134
- }
7135
-
7136
- .option-chain-widget .neutral {
7137
- color: #6b7280;
7138
- }
7139
-
7140
- .option-chain-widget .widget-footer {
7141
- background: #f9fafb;
7142
- padding: 8px 16px;
7143
- border-top: 1px solid #e5e7eb;
7144
- text-align: center;
7145
- font-size: 11px;
7146
- color: #6b7280;
7414
+ .option-chain-widget .contract-cell.clickable {
7415
+ color: #3b82f6;
7416
+ cursor: pointer;
7417
+ text-decoration: underline;
7418
+ transition: all 0.2s ease;
7147
7419
  }
7148
7420
 
7149
- .option-chain-widget .widget-loading-overlay {
7150
- position: absolute;
7151
- top: 0;
7152
- left: 0;
7153
- right: 0;
7154
- bottom: 0;
7155
- background: rgba(255, 255, 255, 0.9);
7156
- display: flex;
7157
- align-items: center;
7158
- justify-content: center;
7159
- z-index: 20;
7421
+ .option-chain-widget .contract-cell.clickable:hover {
7422
+ color: #2563eb;
7423
+ background-color: #eff6ff;
7160
7424
  }
7161
7425
 
7162
- .option-chain-widget .widget-loading-overlay.hidden {
7163
- display: none;
7426
+ .option-chain-widget .contract-cell.clickable:focus {
7427
+ outline: 2px solid #3b82f6;
7428
+ outline-offset: 2px;
7429
+ border-radius: 2px;
7164
7430
  }
7165
7431
 
7166
- .option-chain-widget .loading-content {
7167
- text-align: center;
7432
+ .option-chain-widget .contract-cell.clickable:active {
7433
+ color: #1e40af;
7434
+ background-color: #dbeafe;
7168
7435
  }
7169
7436
 
7170
- .option-chain-widget .loading-spinner {
7171
- width: 32px;
7172
- height: 32px;
7173
- border: 3px solid #e5e7eb;
7174
- border-top: 3px solid #3b82f6;
7175
- border-radius: 50%;
7176
- animation: spin 1s linear infinite;
7177
- margin: 0 auto 12px;
7437
+ .option-chain-widget .positive {
7438
+ color: #059669;
7178
7439
  }
7179
7440
 
7180
- @keyframes spin {
7181
- 0% { transform: rotate(0deg); }
7182
- 100% { transform: rotate(360deg); }
7441
+ .option-chain-widget .negative {
7442
+ color: #dc2626;
7183
7443
  }
7184
7444
 
7185
- .option-chain-widget .no-data-state {
7186
- padding: 40px;
7187
- text-align: center;
7445
+ .option-chain-widget .neutral {
7188
7446
  color: #6b7280;
7189
7447
  }
7190
7448
 
7191
- .option-chain-widget .widget-error {
7192
- padding: 20px;
7193
- background: #fef2f2;
7194
- color: #dc2626;
7195
- border: 1px solid #fecaca;
7196
- margin: 16px;
7197
- border-radius: 4px;
7198
- text-align: center;
7199
- }
7449
+ /* Loading, Error, Footer, and No-Data styles provided by CommonWidgetPatterns */
7200
7450
 
7201
7451
  /* RESPONSIVE STYLES */
7202
7452
 
@@ -7207,13 +7457,19 @@ ${SharedStyles}
7207
7457
  align-items: stretch;
7208
7458
  gap: 8px;
7209
7459
  }
7210
-
7460
+
7461
+ .option-chain-widget .filter-section {
7462
+ flex-direction: row;
7463
+ margin-top: 0;
7464
+ }
7465
+
7211
7466
  .option-chain-widget .symbol-input,
7212
- .option-chain-widget .date-select {
7467
+ .option-chain-widget .date-select,
7468
+ .option-chain-widget .strike-filter {
7213
7469
  width: 100%;
7214
7470
  max-width: none;
7215
7471
  }
7216
-
7472
+
7217
7473
  .option-chain-widget .fetch-button {
7218
7474
  width: 100%;
7219
7475
  }
@@ -7369,6 +7625,121 @@ ${SharedStyles}
7369
7625
  padding: 2px 1px;
7370
7626
  }
7371
7627
  }
7628
+
7629
+ /* Modal Styles */
7630
+ .option-chain-modal-overlay {
7631
+ position: fixed;
7632
+ top: 0;
7633
+ left: 0;
7634
+ right: 0;
7635
+ bottom: 0;
7636
+ background: rgba(0, 0, 0, 0.6);
7637
+ display: flex;
7638
+ align-items: center;
7639
+ justify-content: center;
7640
+ z-index: 10000;
7641
+ padding: 20px;
7642
+ animation: fadeIn 0.2s ease-out;
7643
+ }
7644
+
7645
+ @keyframes fadeIn {
7646
+ from {
7647
+ opacity: 0;
7648
+ }
7649
+ to {
7650
+ opacity: 1;
7651
+ }
7652
+ }
7653
+
7654
+ .option-chain-modal {
7655
+ background: white;
7656
+ border-radius: 12px;
7657
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
7658
+ max-width: 800px;
7659
+ width: 100%;
7660
+ max-height: 90vh;
7661
+ display: flex;
7662
+ flex-direction: column;
7663
+ animation: slideUp 0.3s ease-out;
7664
+ }
7665
+
7666
+ @keyframes slideUp {
7667
+ from {
7668
+ transform: translateY(20px);
7669
+ opacity: 0;
7670
+ }
7671
+ to {
7672
+ transform: translateY(0);
7673
+ opacity: 1;
7674
+ }
7675
+ }
7676
+
7677
+ .option-chain-modal-header {
7678
+ display: flex;
7679
+ align-items: center;
7680
+ justify-content: space-between;
7681
+ padding: 20px 24px;
7682
+ border-bottom: 1px solid #e5e7eb;
7683
+ }
7684
+
7685
+ .option-chain-modal-header h3 {
7686
+ margin: 0;
7687
+ font-size: 18px;
7688
+ font-weight: 600;
7689
+ color: #111827;
7690
+ }
7691
+
7692
+ .option-chain-modal-close {
7693
+ background: none;
7694
+ border: none;
7695
+ font-size: 28px;
7696
+ line-height: 1;
7697
+ color: #6b7280;
7698
+ cursor: pointer;
7699
+ padding: 0;
7700
+ width: 32px;
7701
+ height: 32px;
7702
+ display: flex;
7703
+ align-items: center;
7704
+ justify-content: center;
7705
+ border-radius: 4px;
7706
+ transition: all 0.2s ease;
7707
+ }
7708
+
7709
+ .option-chain-modal-close:hover {
7710
+ background: #f3f4f6;
7711
+ color: #111827;
7712
+ }
7713
+
7714
+ .option-chain-modal-close:active {
7715
+ background: #e5e7eb;
7716
+ }
7717
+
7718
+ .option-chain-modal-body {
7719
+ flex: 1;
7720
+ overflow-y: auto;
7721
+ padding: 24px;
7722
+ }
7723
+
7724
+ /* Responsive modal */
7725
+ @media (max-width: 768px) {
7726
+ .option-chain-modal {
7727
+ max-width: 95%;
7728
+ max-height: 95vh;
7729
+ }
7730
+
7731
+ .option-chain-modal-header {
7732
+ padding: 16px;
7733
+ }
7734
+
7735
+ .option-chain-modal-header h3 {
7736
+ font-size: 16px;
7737
+ }
7738
+
7739
+ .option-chain-modal-body {
7740
+ padding: 16px;
7741
+ }
7742
+ }
7372
7743
  `;
7373
7744
 
7374
7745
  class OptionChainWidget extends BaseWidget {
@@ -7383,7 +7754,18 @@ ${SharedStyles}
7383
7754
  this.data = null;
7384
7755
  this.isDestroyed = false;
7385
7756
  this.unsubscribe = null;
7757
+ this.unsubscribeUnderlying = null; // For underlying stock price
7758
+ this.underlyingPrice = null; // Current price of underlying stock
7386
7759
  this.loadingTimeout = null;
7760
+ this.renderDebounceTimeout = null; // For debouncing re-renders
7761
+
7762
+ // Separate cache for option chain data and underlying price
7763
+ this.cachedOptionChainData = null;
7764
+ this.cachedUnderlyingPrice = null;
7765
+
7766
+ // Modal for Options widget
7767
+ this.optionsModal = null;
7768
+ this.optionsWidgetInstance = null;
7387
7769
 
7388
7770
  // INPUT VALIDATION: Validate initial symbol if provided
7389
7771
  if (options.symbol) {
@@ -7399,6 +7781,7 @@ ${SharedStyles}
7399
7781
  }
7400
7782
  this.date = '';
7401
7783
  this.availableDates = {};
7784
+ this.strikeFilterRange = 5; // Default: show ±10 strikes from ATM
7402
7785
 
7403
7786
  // RATE LIMITING: Create rate limiter for fetch button (1 second between fetches)
7404
7787
  this.fetchRateLimiter = createRateLimitValidator(1000);
@@ -7429,6 +7812,7 @@ ${SharedStyles}
7429
7812
  this.dateSelect = this.container.querySelector('.date-select');
7430
7813
  this.fetchButton = this.container.querySelector('.fetch-button');
7431
7814
  this.dataGrid = this.container.querySelector('.option-chain-data-grid');
7815
+ this.strikeFilterSelect = this.container.querySelector('.strike-filter');
7432
7816
 
7433
7817
  // Set initial symbol value if provided in options
7434
7818
  if (this.symbol) {
@@ -7503,6 +7887,22 @@ ${SharedStyles}
7503
7887
  }
7504
7888
  this.loadAvailableDates(symbolValidation.sanitized);
7505
7889
  });
7890
+
7891
+ // MEMORY LEAK FIX: Use BaseWidget's addEventListener
7892
+ // Strike filter dropdown
7893
+ this.addEventListener(this.strikeFilterSelect, 'change', e => {
7894
+ const value = e.target.value;
7895
+ if (value === 'all') {
7896
+ this.strikeFilterRange = 'all';
7897
+ } else {
7898
+ this.strikeFilterRange = parseInt(value, 10);
7899
+ }
7900
+
7901
+ // Re-render with new filter if we have data
7902
+ if (this.data) {
7903
+ this.displayOptionChain(this.data);
7904
+ }
7905
+ });
7506
7906
  }
7507
7907
  async loadAvailableDates(symbol) {
7508
7908
  if (!symbol) return;
@@ -7518,7 +7918,9 @@ ${SharedStyles}
7518
7918
  // Use API service from wsManager
7519
7919
  const apiService = this.wsManager.getApiService();
7520
7920
  const data = await apiService.getOptionChainDates(symbol);
7521
- console.log("Available dates:", data.dates_dictionary);
7921
+ if (this.debug) {
7922
+ console.log('[OptionChainWidget] Available dates:', data.dates_dictionary);
7923
+ }
7522
7924
  this.availableDates = data.dates_dictionary || {};
7523
7925
  this.populateDateOptions();
7524
7926
 
@@ -7612,11 +8014,15 @@ ${SharedStyles}
7612
8014
  this.showError(`No data received for ${this.symbol} on ${this.date}. Please try again.`);
7613
8015
  }, 10000); // 10 second timeout
7614
8016
 
7615
- // Unsubscribe from previous subscription if exists
8017
+ // Unsubscribe from previous subscriptions if they exist
7616
8018
  if (this.unsubscribe) {
7617
8019
  this.unsubscribe();
7618
8020
  this.unsubscribe = null;
7619
8021
  }
8022
+ if (this.unsubscribeUnderlying) {
8023
+ this.unsubscribeUnderlying();
8024
+ this.unsubscribeUnderlying = null;
8025
+ }
7620
8026
 
7621
8027
  // Subscribe to option chain data
7622
8028
  this.subscribeToData();
@@ -7626,25 +8032,62 @@ ${SharedStyles}
7626
8032
  }
7627
8033
  }
7628
8034
  subscribeToData() {
7629
- // Subscribe with symbol and date for routing
7630
- this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol,
7631
- // Pass symbol as string
7632
- {
8035
+ // Subscribe to option chain data
8036
+ this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryoptionchain'], this.handleMessage.bind(this), this.symbol, {
7633
8037
  date: this.date
7634
- } // Pass date as additional parameter
7635
- );
8038
+ });
7636
8039
 
7637
- // Send subscription message with both symbol and date
7638
- /* this.wsManager.send({
7639
- type: 'queryoptionchain',
7640
- underlying: this.symbol,
7641
- date: this.date
7642
- }); */
8040
+ // Subscribe to underlying stock price for ITM highlighting
8041
+ this.unsubscribeUnderlying = this.wsManager.subscribe(`${this.widgetId}-underlying`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8042
+ }
8043
+
8044
+ /**
8045
+ * Find the nearest strike price to the underlying price (ATM strike)
8046
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8047
+ * @param {Number} underlyingPrice - Current price of underlying stock
8048
+ * @returns {String} - The nearest strike price
8049
+ */
8050
+ findNearestStrike(sortedStrikes, underlyingPrice) {
8051
+ if (!sortedStrikes || sortedStrikes.length === 0 || !underlyingPrice) {
8052
+ return null;
8053
+ }
8054
+
8055
+ // Find strike closest to underlying price
8056
+ let nearestStrike = sortedStrikes[0];
8057
+ let minDifference = Math.abs(parseFloat(sortedStrikes[0]) - underlyingPrice);
8058
+ for (const strike of sortedStrikes) {
8059
+ const difference = Math.abs(parseFloat(strike) - underlyingPrice);
8060
+ if (difference < minDifference) {
8061
+ minDifference = difference;
8062
+ nearestStrike = strike;
8063
+ }
8064
+ }
8065
+ return nearestStrike;
8066
+ }
8067
+
8068
+ /**
8069
+ * Filter strikes to show only those near the money
8070
+ * @param {Array} sortedStrikes - Array of strike prices sorted ascending
8071
+ * @param {String} atmStrike - The at-the-money strike price
8072
+ * @param {Number} range - Number of strikes to show above and below ATM
8073
+ * @returns {Array} - Filtered array of strikes
8074
+ */
8075
+ filterNearMoneyStrikes(sortedStrikes, atmStrike, range) {
8076
+ if (!atmStrike || !sortedStrikes || sortedStrikes.length === 0) {
8077
+ return sortedStrikes;
8078
+ }
8079
+ const atmIndex = sortedStrikes.indexOf(atmStrike);
8080
+ if (atmIndex === -1) {
8081
+ return sortedStrikes;
8082
+ }
8083
+ const startIndex = Math.max(0, atmIndex - range);
8084
+ const endIndex = Math.min(sortedStrikes.length - 1, atmIndex + range);
8085
+ return sortedStrikes.slice(startIndex, endIndex + 1);
7643
8086
  }
7644
8087
  handleData(message) {
7645
- // Clear loading timeout since we received data
8088
+ // Clear loading timeout since we received data (use BaseWidget method)
7646
8089
  if (this.loadingTimeout) {
7647
- clearTimeout(this.loadingTimeout);
8090
+ this.clearTimeout(this.loadingTimeout);
7648
8091
  this.loadingTimeout = null;
7649
8092
  }
7650
8093
 
@@ -7676,7 +8119,9 @@ ${SharedStyles}
7676
8119
  }
7677
8120
  }
7678
8121
  } else {
7679
- this.data = message; // Store for caching
8122
+ // Cache the option chain data
8123
+ this.data = message;
8124
+ this.cachedOptionChainData = message;
7680
8125
  this.displayOptionChain(message);
7681
8126
  }
7682
8127
  } else if (message.type === 'queryoptionchain' && Array.isArray(message.data)) {
@@ -7691,9 +8136,35 @@ ${SharedStyles}
7691
8136
  }
7692
8137
  }
7693
8138
  } else {
7694
- this.data = message.data; // Store for caching
8139
+ // Cache the option chain data
8140
+ this.data = message.data;
8141
+ this.cachedOptionChainData = message.data;
7695
8142
  this.displayOptionChain(message.data);
7696
8143
  }
8144
+ } else if (message.type === 'queryl1' && message.Data) {
8145
+ if (this.debug) {
8146
+ console.log('[OptionChainWidget] Received underlying price update');
8147
+ }
8148
+ const data = message.Data.find(d => d.Symbol === this.symbol);
8149
+ if (data && data.LastPx) {
8150
+ // Cache the underlying price
8151
+ this.underlyingPrice = parseFloat(data.LastPx);
8152
+ this.cachedUnderlyingPrice = parseFloat(data.LastPx);
8153
+
8154
+ // Debounce re-render to prevent excessive updates
8155
+ // Clear any pending render
8156
+ if (this.renderDebounceTimeout) {
8157
+ this.clearTimeout(this.renderDebounceTimeout);
8158
+ }
8159
+
8160
+ // Schedule a debounced re-render (500ms delay)
8161
+ this.renderDebounceTimeout = this.setTimeout(() => {
8162
+ if (this.data) {
8163
+ this.displayOptionChain(this.data);
8164
+ }
8165
+ this.renderDebounceTimeout = null;
8166
+ }, 500);
8167
+ }
7697
8168
  }
7698
8169
  if (message._cached) {
7699
8170
  this.showConnectionQuality(); // Show cache indicator
@@ -7701,6 +8172,14 @@ ${SharedStyles}
7701
8172
  }
7702
8173
  displayOptionChain(data) {
7703
8174
  if (this.isDestroyed) return;
8175
+
8176
+ // Validate data
8177
+ if (!data || !Array.isArray(data) || data.length === 0) {
8178
+ if (this.debug) {
8179
+ console.warn('[OptionChainWidget] Invalid or empty data passed to displayOptionChain');
8180
+ }
8181
+ return;
8182
+ }
7704
8183
  try {
7705
8184
  // Hide loading overlay
7706
8185
  this.hideLoading();
@@ -7733,9 +8212,18 @@ ${SharedStyles}
7733
8212
  }
7734
8213
  });
7735
8214
 
7736
- // Sort strikes and display
8215
+ // Sort strikes
7737
8216
  const sortedStrikes = Object.keys(optionsByStrike).sort((a, b) => parseFloat(a) - parseFloat(b));
7738
- sortedStrikes.forEach(strike => {
8217
+
8218
+ // Apply ATM filter if not showing all
8219
+ let displayStrikes = sortedStrikes;
8220
+ if (this.strikeFilterRange !== 'all' && this.underlyingPrice) {
8221
+ const atmStrike = this.findNearestStrike(sortedStrikes, this.underlyingPrice);
8222
+ if (atmStrike) {
8223
+ displayStrikes = this.filterNearMoneyStrikes(sortedStrikes, atmStrike, this.strikeFilterRange);
8224
+ }
8225
+ }
8226
+ displayStrikes.forEach(strike => {
7739
8227
  const {
7740
8228
  call,
7741
8229
  put
@@ -7754,7 +8242,7 @@ ${SharedStyles}
7754
8242
  const formatted = parseFloat(change).toFixed(2);
7755
8243
  const className = change > 0 ? 'positive' : change < 0 ? 'negative' : 'neutral';
7756
8244
  const sign = change > 0 ? '+' : '';
7757
- span.className = `option-chain-${className}`;
8245
+ span.className = className;
7758
8246
  span.textContent = `${sign}${formatted}`;
7759
8247
  return span;
7760
8248
  };
@@ -7767,7 +8255,28 @@ ${SharedStyles}
7767
8255
  // Create calls data section
7768
8256
  const callsData = document.createElement('div');
7769
8257
  callsData.className = 'calls-data';
7770
- callsData.appendChild(createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell'));
8258
+
8259
+ // Add ITM highlighting for calls (ITM when strike < underlying price)
8260
+ if (this.underlyingPrice && parseFloat(strike) < this.underlyingPrice) {
8261
+ callsData.classList.add('in-the-money');
8262
+ }
8263
+
8264
+ // Make contract cell clickable
8265
+ const callContractCell = createElement('span', call ? sanitizeSymbol(call.symbol) : '--', 'contract-cell');
8266
+ if (call && call.symbol) {
8267
+ callContractCell.classList.add('clickable');
8268
+ callContractCell.setAttribute('role', 'button');
8269
+ callContractCell.setAttribute('tabindex', '0');
8270
+ callContractCell.setAttribute('data-symbol', call.symbol);
8271
+ this.addEventListener(callContractCell, 'click', () => this.showOptionsModal(call.symbol));
8272
+ this.addEventListener(callContractCell, 'keypress', e => {
8273
+ if (e.key === 'Enter' || e.key === ' ') {
8274
+ e.preventDefault();
8275
+ this.showOptionsModal(call.symbol);
8276
+ }
8277
+ });
8278
+ }
8279
+ callsData.appendChild(callContractCell);
7771
8280
  callsData.appendChild(createElement('span', formatPrice(call?.lastPrice)));
7772
8281
  const callChangeSpan = document.createElement('span');
7773
8282
  if (call) {
@@ -7790,7 +8299,28 @@ ${SharedStyles}
7790
8299
  // Create puts data section
7791
8300
  const putsData = document.createElement('div');
7792
8301
  putsData.className = 'puts-data';
7793
- putsData.appendChild(createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell'));
8302
+
8303
+ // Add ITM highlighting for puts (ITM when strike > underlying price)
8304
+ if (this.underlyingPrice && parseFloat(strike) > this.underlyingPrice) {
8305
+ putsData.classList.add('in-the-money');
8306
+ }
8307
+
8308
+ // Make contract cell clickable
8309
+ const putContractCell = createElement('span', put ? sanitizeSymbol(put.symbol) : '', 'contract-cell');
8310
+ if (put && put.symbol) {
8311
+ putContractCell.classList.add('clickable');
8312
+ putContractCell.setAttribute('role', 'button');
8313
+ putContractCell.setAttribute('tabindex', '0');
8314
+ putContractCell.setAttribute('data-symbol', put.symbol);
8315
+ this.addEventListener(putContractCell, 'click', () => this.showOptionsModal(put.symbol));
8316
+ this.addEventListener(putContractCell, 'keypress', e => {
8317
+ if (e.key === 'Enter' || e.key === ' ') {
8318
+ e.preventDefault();
8319
+ this.showOptionsModal(put.symbol);
8320
+ }
8321
+ });
8322
+ }
8323
+ putsData.appendChild(putContractCell);
7794
8324
  putsData.appendChild(createElement('span', formatPrice(put?.lastPrice)));
7795
8325
  const putChangeSpan = document.createElement('span');
7796
8326
  if (put) {
@@ -7814,14 +8344,22 @@ ${SharedStyles}
7814
8344
 
7815
8345
  // Add footer - with array length check
7816
8346
  if (data && data.length > 0) {
7817
- const timestamp = formatTimestampET(data[0].quoteTime || Date.now());
8347
+ const isDelayed = data[0].DataSource === 'OPRA-D';
8348
+
8349
+ // Get timestamp - subtract 20 minutes if delayed data
8350
+ let timestampValue = data[0].quoteTime || Date.now();
8351
+ if (isDelayed) {
8352
+ // Subtract 20 minutes (20 * 60 * 1000 milliseconds)
8353
+ timestampValue = timestampValue - 20 * 60 * 1000;
8354
+ }
8355
+ const timestamp = formatTimestampET(timestampValue);
7818
8356
  const lastUpdateElement = this.container.querySelector('.last-update');
7819
8357
  if (lastUpdateElement) {
7820
8358
  lastUpdateElement.textContent = `Last update: ${timestamp}`;
7821
8359
  }
7822
8360
 
7823
8361
  // Update footer
7824
- const source = data[0].DataSource === 'OPRA-D' ? '20 mins delayed' : 'Real-time';
8362
+ const source = isDelayed ? '20 mins delayed' : 'Real-time';
7825
8363
  const dataSourceElement = this.container.querySelector('.data-source');
7826
8364
  if (dataSourceElement) {
7827
8365
  dataSourceElement.textContent = `Source: ${source}`;
@@ -7844,35 +8382,6 @@ ${SharedStyles}
7844
8382
  loadingOverlay.classList.add('hidden');
7845
8383
  }
7846
8384
  }
7847
-
7848
- /* showInputError(message) {
7849
- const errorDiv = document.createElement('div');
7850
- errorDiv.className = 'symbol-error-message';
7851
- errorDiv.textContent = message;
7852
- errorDiv.style.cssText = `
7853
- color: #dc2626;
7854
- font-size: 12px;
7855
- margin-bottom: 2px;
7856
- position: absolute;
7857
- background: white;
7858
- z-index: 1000;
7859
- transform: translateY(-100%);
7860
- top: -4px;
7861
- left: 0; // Align with input box
7862
- `;
7863
-
7864
- // Ensure the parent is positioned
7865
- const parent = this.symbolInput;
7866
- if (parent) {
7867
- parent.style.position = 'relative';
7868
- // Insert the errorDiv as the first child of the parent
7869
- parent.insertBefore(errorDiv, parent.firstChild);
7870
- }
7871
-
7872
- this.symbolInput.errorMessage = errorDiv;
7873
- this.symbolInput.focus();
7874
- } */
7875
-
7876
8385
  showError(message) {
7877
8386
  this.hideLoading();
7878
8387
  this.clearError();
@@ -7945,19 +8454,135 @@ ${SharedStyles}
7945
8454
  this.container.appendChild(noDataElement);
7946
8455
  }
7947
8456
  }
8457
+
8458
+ /**
8459
+ * Show Options widget in a modal for a specific option contract
8460
+ * @param {string} optionSymbol - The option contract symbol
8461
+ */
8462
+ showOptionsModal(optionSymbol) {
8463
+ if (!optionSymbol || optionSymbol === '--') {
8464
+ return;
8465
+ }
8466
+
8467
+ // Close existing modal if any
8468
+ this.closeOptionsModal();
8469
+
8470
+ // Create modal overlay
8471
+ this.optionsModal = document.createElement('div');
8472
+ this.optionsModal.className = 'option-chain-modal-overlay';
8473
+ this.optionsModal.innerHTML = `
8474
+ <div class="option-chain-modal">
8475
+ <div class="option-chain-modal-header">
8476
+ <h3>Option Details: ${sanitizeSymbol(optionSymbol)}</h3>
8477
+ <button class="option-chain-modal-close" aria-label="Close modal">&times;</button>
8478
+ </div>
8479
+ <div class="option-chain-modal-body">
8480
+ <div id="option-chain-modal-widget"></div>
8481
+ </div>
8482
+ </div>
8483
+ `;
8484
+
8485
+ // Add to body
8486
+ document.body.appendChild(this.optionsModal);
8487
+
8488
+ // Add close event
8489
+ const closeBtn = this.optionsModal.querySelector('.option-chain-modal-close');
8490
+ this.addEventListener(closeBtn, 'click', () => this.closeOptionsModal());
8491
+
8492
+ // Close on overlay click
8493
+ this.addEventListener(this.optionsModal, 'click', e => {
8494
+ if (e.target === this.optionsModal) {
8495
+ this.closeOptionsModal();
8496
+ }
8497
+ });
8498
+
8499
+ // Close on Escape key
8500
+ const escapeHandler = e => {
8501
+ if (e.key === 'Escape') {
8502
+ this.closeOptionsModal();
8503
+ }
8504
+ };
8505
+ document.addEventListener('keydown', escapeHandler);
8506
+ this._escapeHandler = escapeHandler;
8507
+
8508
+ // Create Options widget instance
8509
+ try {
8510
+ const widgetContainer = this.optionsModal.querySelector('#option-chain-modal-widget');
8511
+ this.optionsWidgetInstance = new OptionsWidget(widgetContainer, {
8512
+ symbol: optionSymbol,
8513
+ wsManager: this.wsManager,
8514
+ debug: this.debug,
8515
+ styled: true
8516
+ }, `${this.widgetId}-options-modal`);
8517
+ if (this.debug) {
8518
+ console.log('[OptionChainWidget] Options modal opened for:', optionSymbol);
8519
+ }
8520
+ } catch (error) {
8521
+ console.error('[OptionChainWidget] Error creating Options widget:', error);
8522
+ this.closeOptionsModal();
8523
+ this.showError(`Failed to load option details: ${error.message}`);
8524
+ }
8525
+ }
8526
+
8527
+ /**
8528
+ * Close the Options modal and cleanup
8529
+ */
8530
+ closeOptionsModal() {
8531
+ // Destroy widget instance
8532
+ if (this.optionsWidgetInstance) {
8533
+ this.optionsWidgetInstance.destroy();
8534
+ this.optionsWidgetInstance = null;
8535
+ }
8536
+
8537
+ // Remove modal from DOM
8538
+ if (this.optionsModal) {
8539
+ this.optionsModal.remove();
8540
+ this.optionsModal = null;
8541
+ }
8542
+
8543
+ // Remove escape key handler
8544
+ if (this._escapeHandler) {
8545
+ document.removeEventListener('keydown', this._escapeHandler);
8546
+ this._escapeHandler = null;
8547
+ }
8548
+ if (this.debug) {
8549
+ console.log('[OptionChainWidget] Options modal closed');
8550
+ }
8551
+ }
7948
8552
  destroy() {
7949
8553
  this.isDestroyed = true;
8554
+
8555
+ // Close modal if open
8556
+ this.closeOptionsModal();
7950
8557
  if (this.unsubscribe) {
7951
8558
  this.unsubscribe();
7952
8559
  this.unsubscribe = null;
7953
8560
  }
7954
8561
 
8562
+ // Unsubscribe from underlying price
8563
+ if (this.unsubscribeUnderlying) {
8564
+ this.unsubscribeUnderlying();
8565
+ this.unsubscribeUnderlying = null;
8566
+ }
8567
+
7955
8568
  // MEMORY LEAK FIX: Clear loading timeout using BaseWidget method
7956
8569
  if (this.loadingTimeout) {
7957
8570
  this.clearTimeout(this.loadingTimeout);
7958
8571
  this.loadingTimeout = null;
7959
8572
  }
7960
8573
 
8574
+ // Clear debounce timeout
8575
+ if (this.renderDebounceTimeout) {
8576
+ this.clearTimeout(this.renderDebounceTimeout);
8577
+ this.renderDebounceTimeout = null;
8578
+ }
8579
+
8580
+ // Clear cached data
8581
+ this.cachedOptionChainData = null;
8582
+ this.cachedUnderlyingPrice = null;
8583
+ this.underlyingPrice = null;
8584
+ this.data = null;
8585
+
7961
8586
  // Call parent destroy for automatic cleanup of event listeners, timeouts, intervals
7962
8587
  super.destroy();
7963
8588
  }
@@ -8043,80 +8668,81 @@ ${SharedStyles}
8043
8668
  font-family: Arial, sans-serif;
8044
8669
  }
8045
8670
 
8046
- .widget-header {
8671
+ .data-widget .widget-header {
8047
8672
  display: flex;
8048
8673
  flex-direction: column;
8049
8674
  margin-bottom: 10px;
8050
8675
  }
8051
8676
 
8052
- .symbol {
8677
+ .data-widget .symbol {
8053
8678
  font-size: 24px;
8054
8679
  font-weight: bold;
8055
8680
  margin-right: 10px;
8056
8681
  }
8057
8682
 
8058
- .company-info {
8683
+ .data-widget .company-info {
8059
8684
  font-size: 12px;
8060
8685
  color: #666;
8061
8686
  }
8062
8687
 
8063
- .trading-info {
8688
+ .data-widget .trading-info {
8064
8689
  font-size: 12px;
8065
8690
  color: #999;
8066
8691
  }
8067
8692
 
8068
- .price-section {
8693
+ .data-widget .price-section {
8069
8694
  display: flex;
8070
8695
  align-items: baseline;
8071
8696
  margin-bottom: 10px;
8072
8697
  }
8073
8698
 
8074
- .current-price {
8699
+ .data-widget .current-price {
8075
8700
  font-size: 36px;
8076
8701
  font-weight: bold;
8077
8702
  margin-right: 10px;
8078
8703
  }
8079
8704
 
8080
- .price-change {
8705
+ .data-widget .price-change {
8081
8706
  font-size: 18px;
8082
8707
  }
8083
8708
 
8084
- .price-change.positive {
8709
+ .data-widget .price-change.positive {
8085
8710
  color: green;
8086
8711
  }
8087
8712
 
8088
- .price-change.negative {
8713
+ .data-widget .price-change.negative {
8089
8714
  color: red;
8090
8715
  }
8091
8716
 
8092
- .bid-ask-section {
8717
+ .data-widget .bid-ask-section {
8093
8718
  display: flex;
8094
8719
  justify-content: space-between;
8095
8720
  margin-bottom: 10px;
8096
8721
  }
8097
8722
 
8098
- .ask, .bid {
8723
+ .data-widget .ask,
8724
+ .data-widget .bid {
8099
8725
  display: flex;
8100
8726
  align-items: center;
8101
8727
  }
8102
8728
 
8103
- .label {
8729
+ .data-widget .label {
8104
8730
  font-size: 14px;
8105
8731
  margin-right: 5px;
8106
8732
  }
8107
8733
 
8108
- .value {
8734
+ .data-widget .value {
8109
8735
  font-size: 16px;
8110
8736
  font-weight: bold;
8111
8737
  margin-right: 5px;
8112
8738
  }
8113
8739
 
8114
- .size {
8740
+ .data-widget .size {
8115
8741
  font-size: 14px;
8116
8742
  color: red;
8117
8743
  }
8118
8744
 
8119
- .widget-footer {
8745
+ .data-widget .widget-footer {
8120
8746
  font-size: 12px;
8121
8747
  color: #333;
8122
8748
  }
@@ -8527,76 +9153,50 @@ ${SharedStyles}
8527
9153
 
8528
9154
  // Subscribe to regular market data (Level 1)
8529
9155
  // Create a wrapper to add data type context
8530
- this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], messageWrapper => {
8531
- const {
8532
- event,
8533
- data
8534
- } = messageWrapper;
8535
-
8536
- // Handle connection events
8537
- if (event === 'connection') {
8538
- this.handleConnectionStatus(data);
8539
- return;
8540
- }
8541
-
8542
- // For data events, add type context and use base handleMessage pattern
8543
- if (event === 'data') {
8544
- data._dataType = 'market';
8545
- this.handleMessage({
8546
- event,
8547
- data
8548
- });
8549
- }
8550
- }, this.symbol);
9156
+ this.unsubscribeMarket = this.wsManager.subscribe(`${this.widgetId}-market`, ['queryl1'], this.handleMessage.bind(this), this.symbol);
8551
9157
 
8552
9158
  // Subscribe to night session data (BlueOcean or Bruce)
8553
- this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], messageWrapper => {
8554
- const {
8555
- event,
8556
- data
8557
- } = messageWrapper;
8558
-
8559
- // Connection already handled by market subscription
8560
- if (event === 'connection') {
8561
- return;
8562
- }
8563
-
8564
- // For data events, add type context and use base handleMessage pattern
8565
- if (event === 'data') {
8566
- data._dataType = 'night';
8567
- this.handleMessage({
8568
- event,
8569
- data
8570
- });
8571
- }
8572
- }, this.symbol);
9159
+ this.unsubscribeNight = this.wsManager.subscribe(`${this.widgetId}-night`, ['queryblueoceanl1'], this.handleMessage.bind(this), this.symbol);
8573
9160
  }
8574
9161
  handleData(message) {
8575
9162
  // Extract data type from metadata
8576
- const dataType = message._dataType;
9163
+ const dataType = message.type;
8577
9164
  if (this.debug) {
8578
9165
  console.log(`[CombinedMarketWidget] handleData called with type: ${dataType}`, message);
8579
9166
  }
8580
9167
 
8581
- // Safety check - if no data type, try to infer from structure
8582
- if (!dataType) {
8583
- if (this.debug) {
8584
- console.warn('[CombinedMarketWidget] No data type specified, attempting to infer from structure');
9168
+ // Clear loading timeout since we received data (use BaseWidget method)
9169
+ if (this.loadingTimeout) {
9170
+ this.clearTimeout(this.loadingTimeout);
9171
+ this.loadingTimeout = null;
9172
+ }
9173
+
9174
+ // Handle option chain data
9175
+ if (Array.isArray(message)) {
9176
+ if (message.length === 0) {
9177
+ // Only show no data state if we don't have cached data
9178
+ if (!this.data) {
9179
+ this.showNoDataState();
9180
+ } else {
9181
+ this.hideLoading();
9182
+ if (this.debug) {
9183
+ console.log('[OptionChainWidget] No new data, keeping cached data visible');
9184
+ }
9185
+ }
8585
9186
  }
8586
- return;
8587
9187
  }
8588
9188
 
8589
9189
  // Handle error messages from server
8590
- if (message.type === 'error' && message.noData) {
9190
+ if (message.type === 'error' || message.error == true) {
8591
9191
  if (this.debug) {
8592
9192
  console.log(`[CombinedMarketWidget] Received no data message for ${dataType}:`, message.message);
8593
9193
  }
8594
9194
  // Keep existing cached data visible, just hide loading
8595
- if (dataType === 'market' && !this.marketData) {
9195
+ if (dataType === 'queryl1' && !this.marketData) {
8596
9196
  if (this.debug) {
8597
9197
  console.log('[CombinedMarketWidget] No market data available');
8598
9198
  }
8599
- } else if (dataType === 'night' && !this.nightSessionData) {
9199
+ } else if (dataType === 'blueoceanl1' && !this.nightSessionData) {
8600
9200
  if (this.debug) {
8601
9201
  console.log('[CombinedMarketWidget] No night session data available');
8602
9202
  }
@@ -9049,7 +9649,7 @@ ${SharedStyles}
9049
9649
  margin: 0 auto;
9050
9650
  }
9051
9651
 
9052
- .chart-header {
9652
+ .intraday-chart-widget .chart-header {
9053
9653
  display: flex;
9054
9654
  justify-content: space-between;
9055
9655
  align-items: center;
@@ -9058,13 +9658,13 @@ ${SharedStyles}
9058
9658
  border-bottom: 1px solid #e5e7eb;
9059
9659
  }
9060
9660
 
9061
- .chart-title-section {
9661
+ .intraday-chart-widget .chart-title-section {
9062
9662
  display: flex;
9063
9663
  flex-direction: column;
9064
9664
  gap: 4px;
9065
9665
  }
9066
9666
 
9067
- .company-market-info {
9667
+ .intraday-chart-widget .company-market-info {
9068
9668
  display: flex;
9069
9669
  gap: 8px;
9070
9670
  align-items: center;
@@ -9072,18 +9672,18 @@ ${SharedStyles}
9072
9672
  color: #6b7280;
9073
9673
  }
9074
9674
 
9075
- .intraday-company-name {
9675
+ .intraday-chart-widget .intraday-company-name {
9076
9676
  font-weight: 500;
9077
9677
  }
9078
9678
 
9079
- .intraday-chart-symbol {
9679
+ .intraday-chart-widget .intraday-chart-symbol {
9080
9680
  font-size: 1.5em;
9081
9681
  font-weight: 700;
9082
9682
  color: #1f2937;
9083
9683
  margin: 0;
9084
9684
  }
9085
9685
 
9086
- .intraday-chart-source {
9686
+ .intraday-chart-widget .intraday-chart-source {
9087
9687
  font-size: 0.75em;
9088
9688
  padding: 4px 8px;
9089
9689
  border-radius: 4px;
@@ -9092,36 +9692,36 @@ ${SharedStyles}
9092
9692
  font-weight: 600;
9093
9693
  }
9094
9694
 
9095
- .chart-change {
9695
+ .intraday-chart-widget .chart-change {
9096
9696
  font-size: 1.1em;
9097
9697
  font-weight: 600;
9098
9698
  padding: 6px 12px;
9099
9699
  border-radius: 6px;
9100
9700
  }
9101
9701
 
9102
- .chart-change.positive {
9702
+ .intraday-chart-widget .chart-change.positive {
9103
9703
  color: #059669;
9104
9704
  background: #d1fae5;
9105
9705
  }
9106
9706
 
9107
- .chart-change.negative {
9707
+ .intraday-chart-widget .chart-change.negative {
9108
9708
  color: #dc2626;
9109
9709
  background: #fee2e2;
9110
9710
  }
9111
9711
 
9112
- .chart-controls {
9712
+ .intraday-chart-widget .chart-controls {
9113
9713
  display: flex;
9114
9714
  justify-content: space-between;
9115
9715
  align-items: center;
9116
9716
  margin-bottom: 15px;
9117
9717
  }
9118
9718
 
9119
- .chart-range-selector {
9719
+ .intraday-chart-widget .chart-range-selector {
9120
9720
  display: flex;
9121
9721
  gap: 8px;
9122
9722
  }
9123
9723
 
9124
- .range-btn {
9724
+ .intraday-chart-widget .range-btn {
9125
9725
  padding: 8px 16px;
9126
9726
  border: 1px solid #e5e7eb;
9127
9727
  background: white;
@@ -9133,28 +9733,28 @@ ${SharedStyles}
9133
9733
  transition: all 0.2s ease;
9134
9734
  }
9135
9735
 
9136
- .range-btn:hover {
9736
+ .intraday-chart-widget .range-btn:hover {
9137
9737
  background: #f9fafb;
9138
9738
  border-color: #d1d5db;
9139
9739
  }
9140
9740
 
9141
- .range-btn.active {
9741
+ .intraday-chart-widget .range-btn.active {
9142
9742
  background: #667eea;
9143
9743
  color: white;
9144
9744
  border-color: #667eea;
9145
9745
  }
9146
9746
 
9147
- .range-btn:focus {
9747
+ .intraday-chart-widget .range-btn:focus {
9148
9748
  outline: none;
9149
9749
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
9150
9750
  }
9151
9751
 
9152
- .chart-type-selector {
9752
+ .intraday-chart-widget .chart-type-selector {
9153
9753
  display: flex;
9154
9754
  gap: 8px;
9155
9755
  }
9156
9756
 
9157
- .type-btn {
9757
+ .intraday-chart-widget .type-btn {
9158
9758
  padding: 8px 16px;
9159
9759
  border: 1px solid #e5e7eb;
9160
9760
  background: white;
@@ -9166,23 +9766,23 @@ ${SharedStyles}
9166
9766
  transition: all 0.2s ease;
9167
9767
  }
9168
9768
 
9169
- .type-btn:hover {
9769
+ .intraday-chart-widget .type-btn:hover {
9170
9770
  background: #f9fafb;
9171
9771
  border-color: #d1d5db;
9172
9772
  }
9173
9773
 
9174
- .type-btn.active {
9774
+ .intraday-chart-widget .type-btn.active {
9175
9775
  background: #10b981;
9176
9776
  color: white;
9177
9777
  border-color: #10b981;
9178
9778
  }
9179
9779
 
9180
- .type-btn:focus {
9780
+ .intraday-chart-widget .type-btn:focus {
9181
9781
  outline: none;
9182
9782
  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
9183
9783
  }
9184
9784
 
9185
- .zoom-reset-btn {
9785
+ .intraday-chart-widget .zoom-reset-btn {
9186
9786
  display: flex;
9187
9787
  align-items: center;
9188
9788
  gap: 6px;
@@ -9197,34 +9797,34 @@ ${SharedStyles}
9197
9797
  transition: all 0.2s ease;
9198
9798
  }
9199
9799
 
9200
- .zoom-reset-btn:hover {
9800
+ .intraday-chart-widget .zoom-reset-btn:hover {
9201
9801
  background: #f9fafb;
9202
9802
  border-color: #667eea;
9203
9803
  color: #667eea;
9204
9804
  }
9205
9805
 
9206
- .zoom-reset-btn:focus {
9806
+ .intraday-chart-widget .zoom-reset-btn:focus {
9207
9807
  outline: none;
9208
9808
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
9209
9809
  }
9210
9810
 
9211
- .zoom-reset-btn svg {
9811
+ .intraday-chart-widget .zoom-reset-btn svg {
9212
9812
  flex-shrink: 0;
9213
9813
  }
9214
9814
 
9215
- .chart-container {
9815
+ .intraday-chart-widget .chart-container {
9216
9816
  height: 500px;
9217
9817
  margin-bottom: 20px;
9218
9818
  position: relative;
9219
9819
  }
9220
9820
 
9221
- .chart-stats {
9821
+ .intraday-chart-widget .chart-stats {
9222
9822
  padding: 15px;
9223
9823
  background: #f9fafb;
9224
9824
  border-radius: 8px;
9225
9825
  }
9226
9826
 
9227
- .stats-header {
9827
+ .intraday-chart-widget .stats-header {
9228
9828
  font-size: 0.875em;
9229
9829
  font-weight: 700;
9230
9830
  color: #374151;
@@ -9235,19 +9835,19 @@ ${SharedStyles}
9235
9835
  border-bottom: 2px solid #e5e7eb;
9236
9836
  }
9237
9837
 
9238
- .stats-grid {
9838
+ .intraday-chart-widget .stats-grid {
9239
9839
  display: grid;
9240
9840
  grid-template-columns: repeat(5, 1fr);
9241
9841
  gap: 15px;
9242
9842
  }
9243
9843
 
9244
- .stat-item {
9844
+ .intraday-chart-widget .stat-item {
9245
9845
  display: flex;
9246
9846
  flex-direction: column;
9247
9847
  gap: 4px;
9248
9848
  }
9249
9849
 
9250
- .stat-label {
9850
+ .intraday-chart-widget .stat-label {
9251
9851
  font-size: 0.75em;
9252
9852
  color: #6b7280;
9253
9853
  font-weight: 600;
@@ -9255,13 +9855,13 @@ ${SharedStyles}
9255
9855
  letter-spacing: 0.5px;
9256
9856
  }
9257
9857
 
9258
- .stat-value {
9858
+ .intraday-chart-widget .stat-value {
9259
9859
  font-size: 1.1em;
9260
9860
  font-weight: 700;
9261
9861
  color: #1f2937;
9262
9862
  }
9263
9863
 
9264
- .widget-loading-overlay {
9864
+ .intraday-chart-widget .widget-loading-overlay {
9265
9865
  position: absolute;
9266
9866
  top: 0;
9267
9867
  left: 0;
@@ -9280,26 +9880,26 @@ ${SharedStyles}
9280
9880
  backdrop-filter: blur(1px);
9281
9881
  }
9282
9882
 
9283
- .widget-loading-overlay.hidden {
9883
+ .intraday-chart-widget .widget-loading-overlay.hidden {
9284
9884
  display: none;
9285
9885
  }
9286
9886
 
9287
- .loading-spinner {
9887
+ .intraday-chart-widget .loading-spinner {
9288
9888
  width: 20px;
9289
9889
  height: 20px;
9290
9890
  border: 3px solid #e5e7eb;
9291
9891
  border-top-color: #667eea;
9292
9892
  border-radius: 50%;
9293
- animation: spin 0.8s linear infinite;
9893
+ animation: intraday-spin 0.8s linear infinite;
9294
9894
  }
9295
9895
 
9296
- .loading-text {
9896
+ .intraday-chart-widget .loading-text {
9297
9897
  color: #6b7280;
9298
9898
  font-size: 0.875em;
9299
9899
  font-weight: 500;
9300
9900
  }
9301
9901
 
9302
- @keyframes spin {
9902
+ @keyframes intraday-spin {
9303
9903
  to { transform: rotate(360deg); }
9304
9904
  }
9305
9905
 
@@ -9309,26 +9909,26 @@ ${SharedStyles}
9309
9909
  padding: 15px;
9310
9910
  }
9311
9911
 
9312
- .stats-grid {
9912
+ .intraday-chart-widget .stats-grid {
9313
9913
  grid-template-columns: repeat(3, 1fr);
9314
9914
  gap: 10px;
9315
9915
  }
9316
9916
 
9317
- .stats-header {
9917
+ .intraday-chart-widget .stats-header {
9318
9918
  font-size: 0.8em;
9319
9919
  margin-bottom: 10px;
9320
9920
  }
9321
9921
 
9322
- .chart-container {
9922
+ .intraday-chart-widget .chart-container {
9323
9923
  height: 350px;
9324
9924
  }
9325
9925
 
9326
- .intraday-chart-symbol {
9926
+ .intraday-chart-widget .intraday-chart-symbol {
9327
9927
  font-size: 1.2em;
9328
9928
  }
9329
9929
  }
9330
9930
 
9331
- .widget-error {
9931
+ .intraday-chart-widget .widget-error {
9332
9932
  padding: 15px;
9333
9933
  background: #fee2e2;
9334
9934
  border: 1px solid #fecaca;
@@ -38704,7 +39304,20 @@ ${SharedStyles}
38704
39304
  }
38705
39305
  async initialize() {
38706
39306
  // Validate initial symbol via API to get company info
38707
- await this.validateInitialSymbol();
39307
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
39308
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
39309
+ // Extract company info from first data item
39310
+ if (data && data[0]) {
39311
+ this.companyName = data[0].comp_name || '';
39312
+ this.exchangeName = data[0].market_name || '';
39313
+ this.mic = data[0].mic || '';
39314
+ }
39315
+ });
39316
+
39317
+ // If validation failed due to access/permission issues, stop initialization
39318
+ if (validationSuccess === false) {
39319
+ return; // Error is already shown, don't continue
39320
+ }
38708
39321
 
38709
39322
  // Update company name in the header
38710
39323
  this.updateCompanyName();
@@ -38712,38 +39325,6 @@ ${SharedStyles}
38712
39325
  // Load chart data
38713
39326
  await this.loadChartData();
38714
39327
  }
38715
- async validateInitialSymbol() {
38716
- try {
38717
- const apiService = this.wsManager.getApiService();
38718
- if (!apiService) {
38719
- if (this.debug) {
38720
- console.log('[IntradayChartWidget] API service not available for initial validation');
38721
- }
38722
- return;
38723
- }
38724
- const result = await apiService.quotel1(this.symbol);
38725
- if (result && result.data && result.data[0]) {
38726
- const symbolData = result.data[0];
38727
- // Extract company info
38728
- this.companyName = symbolData.comp_name || '';
38729
- this.exchangeName = symbolData.market_name || '';
38730
- this.mic = symbolData.mic || '';
38731
- if (this.debug) {
38732
- console.log('[IntradayChartWidget] Initial symbol validated:', {
38733
- symbol: this.symbol,
38734
- companyName: this.companyName,
38735
- exchangeName: this.exchangeName,
38736
- mic: this.mic
38737
- });
38738
- }
38739
- }
38740
- } catch (error) {
38741
- if (this.debug) {
38742
- console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
38743
- }
38744
- // Don't throw - let the widget continue with chart data
38745
- }
38746
- }
38747
39328
  updateCompanyName() {
38748
39329
  const companyNameElement = this.container.querySelector('.intraday-company-name');
38749
39330
  if (companyNameElement) {
@@ -39167,19 +39748,12 @@ ${SharedStyles}
39167
39748
  // Handle array format (standard night session format)
39168
39749
  if (Array.isArray(message.data)) {
39169
39750
  console.log('[IntradayChartWidget] Processing array format, length:', message.length);
39170
- const symbolData = message.find(item => item.Symbol === this.symbol);
39751
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
39171
39752
  console.log('[IntradayChartWidget] Found symbol data:', symbolData);
39172
39753
  if (symbolData && !symbolData.NotFound) {
39173
39754
  priceData = symbolData;
39174
39755
  }
39175
39756
  }
39176
- // Handle wrapped format
39177
- else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
39178
- console.log('[IntradayChartWidget] Processing wrapped format');
39179
- if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
39180
- priceData = message['0'];
39181
- }
39182
- }
39183
39757
  // Handle direct data format
39184
39758
  else if (message.Symbol === this.symbol && !message.NotFound) {
39185
39759
  console.log('[IntradayChartWidget] Processing direct format');
@@ -40282,7 +40856,19 @@ ${SharedStyles}
40282
40856
  this.showLoading();
40283
40857
 
40284
40858
  // Fetch company info for initial symbol
40285
- await this.validateInitialSymbol();
40859
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40860
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40861
+ // Extract company info from first data item
40862
+ if (data && data[0]) {
40863
+ this.companyName = data[0].comp_name || '';
40864
+ this.exchangeName = data[0].market_name || '';
40865
+ }
40866
+ });
40867
+
40868
+ // If validation failed due to access/permission issues, stop initialization
40869
+ if (validationSuccess === false) {
40870
+ return; // Error is already shown, don't continue
40871
+ }
40286
40872
 
40287
40873
  // Set timeout to detect no data on initial load
40288
40874
  this.loadingTimeout = setTimeout(() => {
@@ -40294,36 +40880,6 @@ ${SharedStyles}
40294
40880
  }, 10000);
40295
40881
  this.subscribeToData();
40296
40882
  }
40297
- async validateInitialSymbol() {
40298
- try {
40299
- const apiService = this.wsManager.getApiService();
40300
- if (!apiService) {
40301
- if (this.debug) {
40302
- console.log('[ONBBO L2] API service not available for initial validation');
40303
- }
40304
- return;
40305
- }
40306
- const result = await apiService.quotel1(this.symbol);
40307
- if (result && result.data && result.data[0]) {
40308
- const symbolData = result.data[0];
40309
- // Extract company info
40310
- this.companyName = symbolData.comp_name || '';
40311
- this.exchangeName = symbolData.market_name || '';
40312
- if (this.debug) {
40313
- console.log('[ONBBO L2] Initial symbol validated:', {
40314
- symbol: this.symbol,
40315
- companyName: this.companyName,
40316
- exchangeName: this.exchangeName
40317
- });
40318
- }
40319
- }
40320
- } catch (error) {
40321
- if (this.debug) {
40322
- console.warn('[ONBBO L2] Initial symbol validation failed:', error);
40323
- }
40324
- // Don't throw - let the widget continue with WebSocket data
40325
- }
40326
- }
40327
40883
  subscribeToData() {
40328
40884
  // Subscribe to ONBBO Level 2 data (order book)
40329
40885
  // Use separate widget IDs to avoid "already active" duplicate detection
@@ -40842,6 +41398,7 @@ ${SharedStyles}
40842
41398
  this.styled = options.styled !== undefined ? options.styled : true;
40843
41399
  this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
40844
41400
  this.data = new TimeSalesModel([], this.maxTrades);
41401
+ this.data.symbol = this.symbol; // Set the symbol in the data model
40845
41402
  this.isDestroyed = false;
40846
41403
  this.unsubscribe = null;
40847
41404
  this.symbolEditor = null;
@@ -40853,6 +41410,7 @@ ${SharedStyles}
40853
41410
 
40854
41411
  // Create widget structure
40855
41412
  this.createWidgetStructure();
41413
+ this.initializeSymbolEditor();
40856
41414
 
40857
41415
  // Initialize the widget
40858
41416
  this.initialize();
@@ -40869,7 +41427,26 @@ ${SharedStyles}
40869
41427
  }
40870
41428
  }
40871
41429
  this.addStyles();
40872
- this.setupSymbolEditor();
41430
+ //this.setupSymbolEditor();
41431
+ }
41432
+ initializeSymbolEditor() {
41433
+ // Initialize symbol editor with stock symbol validation
41434
+ this.symbolEditor = new SymbolEditor(this, {
41435
+ maxLength: 10,
41436
+ placeholder: 'Enter symbol...',
41437
+ //validator: this.validateStockSymbol.bind(this),
41438
+ onSymbolChange: this.handleSymbolChange.bind(this),
41439
+ debug: this.debug,
41440
+ autoUppercase: true,
41441
+ symbolType: 'quotel1'
41442
+ });
41443
+
41444
+ // Set initial symbol
41445
+ const symbolElement = this.container.querySelector('.symbol');
41446
+ if (symbolElement) {
41447
+ symbolElement.textContent = this.symbol;
41448
+ symbolElement.dataset.originalSymbol = this.symbol;
41449
+ }
40873
41450
  }
40874
41451
  addStyles() {
40875
41452
  // Inject styles from the styles file into document head
@@ -40892,6 +41469,8 @@ ${SharedStyles}
40892
41469
  // Keep editor open when clicking buttons
40893
41470
  symbolType: 'quotel1' // Use standard quote validation
40894
41471
  });
41472
+
41473
+ //console.log('SETUP COMPLETE')
40895
41474
  }
40896
41475
  async handleSymbolChange(newSymbol, oldSymbol) {
40897
41476
  let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
@@ -40994,7 +41573,19 @@ ${SharedStyles}
40994
41573
  this.showLoading();
40995
41574
 
40996
41575
  // Validate initial symbol via API to get company info
40997
- await this.validateInitialSymbol();
41576
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
41577
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
41578
+ // Extract company info from first data item
41579
+ if (data && data[0]) {
41580
+ this.companyName = data[0].comp_name || '';
41581
+ this.exchangeName = data[0].market_name || '';
41582
+ }
41583
+ });
41584
+
41585
+ // If validation failed due to access/permission issues, stop initialization
41586
+ if (validationSuccess === false) {
41587
+ return; // Error is already shown, don't continue
41588
+ }
40998
41589
 
40999
41590
  // Set timeout to detect no data on initial load
41000
41591
  this.loadingTimeout = setTimeout(() => {
@@ -41007,36 +41598,6 @@ ${SharedStyles}
41007
41598
 
41008
41599
  this.subscribeToData();
41009
41600
  }
41010
- async validateInitialSymbol() {
41011
- try {
41012
- const apiService = this.wsManager.getApiService();
41013
- if (!apiService) {
41014
- if (this.debug) {
41015
- console.log('[TimeSalesWidget] API service not available for initial validation');
41016
- }
41017
- return;
41018
- }
41019
- const result = await apiService.quotel1(this.symbol);
41020
- if (result && result.data && result.data[0]) {
41021
- const symbolData = result.data[0];
41022
- // Extract company info
41023
- this.companyName = symbolData.comp_name || '';
41024
- this.exchangeName = symbolData.market_name || '';
41025
- if (this.debug) {
41026
- console.log('[TimeSalesWidget] Initial symbol validated:', {
41027
- symbol: this.symbol,
41028
- companyName: this.companyName,
41029
- exchangeName: this.exchangeName
41030
- });
41031
- }
41032
- }
41033
- } catch (error) {
41034
- if (this.debug) {
41035
- console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
41036
- }
41037
- // Don't throw - let the widget continue with WebSocket data
41038
- }
41039
- }
41040
41601
  subscribeToData() {
41041
41602
  // Subscribe to Time & Sales data (using queryonbbotimesale)
41042
41603
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
@@ -41052,6 +41613,9 @@ ${SharedStyles}
41052
41613
  clearTimeout(this.loadingTimeout);
41053
41614
  this.loadingTimeout = null;
41054
41615
  }
41616
+ if (message.type != 'queryonbbotimesale') {
41617
+ return;
41618
+ }
41055
41619
 
41056
41620
  // Handle error messages
41057
41621
  if (message.type === 'error') {
@@ -41064,30 +41628,16 @@ ${SharedStyles}
41064
41628
  }
41065
41629
 
41066
41630
  // Handle Time & Sales data - Array of trades
41067
- if (Array.isArray(message)) {
41631
+ if (Array.isArray(message.data)) {
41068
41632
  if (this.debug) {
41069
41633
  console.log('[TimeSalesWidget] Received trades array:', message);
41070
41634
  }
41071
41635
 
41072
41636
  // Add new trades to existing data
41073
- this.data.addTrades(message);
41637
+ this.data.addTrades(message.data);
41074
41638
  this.updateWidget();
41075
41639
  return;
41076
41640
  }
41077
-
41078
- // Handle wrapped format
41079
- if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
41080
- if (Array.isArray(message['0'])) {
41081
- if (this.debug) {
41082
- console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
41083
- }
41084
-
41085
- // Add new trades to existing data
41086
- this.data.addTrades(message['0']);
41087
- this.updateWidget();
41088
- return;
41089
- }
41090
- }
41091
41641
  if (this.debug) {
41092
41642
  console.log('[TimeSalesWidget] Unexpected message format:', message);
41093
41643
  }
@@ -41314,7 +41864,15 @@ ${SharedStyles}
41314
41864
  try {
41315
41865
  const response = await fetch(url, config);
41316
41866
  if (!response.ok) {
41317
- throw new Error(`HTTP error! status: ${response.status}`);
41867
+ // Try to get response body for error details
41868
+ let errorBody = '';
41869
+ try {
41870
+ errorBody = await response.text();
41871
+ console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
41872
+ } catch (e) {
41873
+ console.error(`[ApiService] Could not read error response body:`, e);
41874
+ }
41875
+ throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
41318
41876
  }
41319
41877
  return await response.json();
41320
41878
  } catch (error) {
@@ -42074,8 +42632,8 @@ ${SharedStyles}
42074
42632
  */
42075
42633
  _normalizeMessage(message) {
42076
42634
  // Check for error field (case-insensitive)
42077
- const errorField = message.error || message.Error;
42078
- const typeField = message.type || message.Type;
42635
+ const errorField = message.error;
42636
+ const typeField = message.type;
42079
42637
  const messageField = message.message || message.Message;
42080
42638
 
42081
42639
  // Handle explicit error messages - route based on type field
@@ -42115,54 +42673,16 @@ ${SharedStyles}
42115
42673
  }
42116
42674
  }
42117
42675
 
42118
- // Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
42119
- const dataField = message.data || message.Data;
42120
- if (typeField && dataField !== undefined) {
42121
- if (this.config.debug) {
42122
- console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
42123
- }
42124
-
42125
- // Extract the actual data and add the type to it for routing
42126
- let normalizedData;
42127
- if (Array.isArray(dataField)) {
42128
- // If data is an array, process each item
42129
- normalizedData = dataField;
42130
- // Add type info to the structure for routing
42131
- if (normalizedData.length > 0) {
42132
- normalizedData._messageType = typeField;
42133
- }
42134
- } else if (typeof dataField === 'object' && dataField !== null) {
42135
- // If data is an object, use it directly
42136
- normalizedData = dataField;
42137
- // Add type info for routing
42138
- normalizedData._messageType = typeField;
42139
- } else {
42140
- // Primitive value, wrap it
42141
- normalizedData = {
42142
- value: dataField,
42143
- _messageType: typeField
42144
- };
42145
- }
42146
-
42147
- // Only add type field if the normalized data doesn't already have one
42148
- if (typeof normalizedData === 'object' && !normalizedData.type && !normalizedData.Type) {
42149
- normalizedData.type = typeField;
42150
- }
42151
- return normalizedData;
42152
- }
42153
-
42154
42676
  // Old format or no type/data structure, return as is
42155
42677
  return message;
42156
42678
  }
42157
42679
  _routeMessage(message) {
42158
42680
  //console.log('message', message);
42159
42681
 
42160
- if (Array.isArray(message) && message.length === 0) {
42161
- if (this.config.debug) {
42162
- console.log('[WebSocketManager] Received empty array, ignoring');
42163
- }
42164
- return; // Don't route empty arrays
42165
- }
42682
+ // IMPORTANT: Don't filter out empty arrays here - route them based on message.type
42683
+ // Widgets need to receive empty arrays to clear loading states and show "no data" messages
42684
+ // The message structure is: { type: 'queryoptionchain', data: [] }
42685
+ // We need to route based on the 'type' field, not the data content
42166
42686
 
42167
42687
  // Cache the message for later use by new subscribers
42168
42688
  this._cacheMessage(message);
@@ -42261,27 +42781,34 @@ ${SharedStyles}
42261
42781
  _getRelevantWidgets(message) {
42262
42782
  const relevantWidgets = new Set();
42263
42783
 
42784
+ // OPTIMIZATION: Extract message type once from the entire message, not from each data item
42785
+ // Message structure: { type: 'queryoptionchain', data: [] }
42786
+ let dataField = message.Data || message.data;
42787
+ const messageType = message.type || this._extractMessageType(dataField);
42788
+
42264
42789
  // Handle array messages
42265
- this._addRelevantWidgetsForItem(message, relevantWidgets);
42790
+ this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
42266
42791
  return relevantWidgets;
42267
42792
  }
42268
- _addRelevantWidgetsForItem(item, relevantWidgets) {
42793
+ _addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
42269
42794
  // If item has Data array, process it
42270
42795
  if (item.Data && Array.isArray(item.Data)) {
42271
42796
  //console.log('Data array found with length:', item.Data.length);
42272
42797
  item.Data.forEach(dataItem => {
42273
- this._processDataItem(dataItem, relevantWidgets);
42798
+ // Pass messageType from parent message, don't extract from each data item
42799
+ this._processDataItem(dataItem, relevantWidgets, messageType);
42274
42800
  });
42275
- } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42276
- this._processOptionChainData(item, relevantWidgets);
42801
+ } else if (item.data && item.data[0] && item.data[0].Strike !== undefined && item.data[0].Expire && !item.data[0].underlyingSymbol) {
42802
+ this._processOptionChainData(item.data, relevantWidgets);
42277
42803
  } else {
42278
- // Process single item
42279
- this._processDataItem(item, relevantWidgets);
42804
+ // Process single item - messageType already extracted from message
42805
+ this._processDataItem(item, relevantWidgets, messageType);
42280
42806
  }
42281
42807
  }
42282
42808
 
42283
42809
  // Simplified option chain processing - handle entire array at once
42284
42810
  _processOptionChainData(optionChainArray, relevantWidgets) {
42811
+ //console.log('PROCESSING DATA OPTIONS')
42285
42812
  if (!optionChainArray || optionChainArray.length === 0) return;
42286
42813
  // Extract underlying symbol and date from first option contract
42287
42814
  const firstOption = optionChainArray[0];
@@ -42320,12 +42847,11 @@ ${SharedStyles}
42320
42847
  }
42321
42848
  }
42322
42849
  }
42323
- _processDataItem(dataItem, relevantWidgets) {
42850
+ _processDataItem(dataItem, relevantWidgets, messageType) {
42324
42851
  // Process individual data items and route to appropriate widgets
42325
42852
  // Option chain arrays are handled separately by _processOptionChainData
42326
42853
 
42327
42854
  const symbol = this._extractSymbol(dataItem);
42328
- const messageType = this._extractMessageType(dataItem);
42329
42855
  if (this.config.debug) {
42330
42856
  console.log('[WebSocketManager] Processing data item:', {
42331
42857
  symbol,
@@ -42485,25 +43011,15 @@ ${SharedStyles}
42485
43011
  return symbolMappings[extracted] || extracted;
42486
43012
  }
42487
43013
  _extractMessageType(item) {
42488
- // Check for _messageType field added by normalization (from new format)
42489
- if (item._messageType) {
42490
- return item._messageType;
42491
- }
42492
-
42493
- // Determine message type based on content
42494
- if (item.type) return item.type;
42495
-
42496
- // Infer type from data structure
43014
+ // FALLBACK: Infer type from data structure (for legacy/malformed messages)
42497
43015
 
42498
43016
  // IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
42499
43017
  // Level 2 ONBBO data - array of MMID objects or object with MMID field
42500
43018
  if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
42501
43019
  return 'queryonbbol2';
42502
43020
  }
42503
-
42504
- // Check for single MMID object
42505
- if (item.MMID !== undefined) {
42506
- return 'queryonbbol2';
43021
+ if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
43022
+ return 'queryoptionchain';
42507
43023
  }
42508
43024
 
42509
43025
  // Check for Source field AFTER MMID check