Brajmovech commited on
Commit
fa65b59
·
1 Parent(s): 82e7fcd

Preserve dashboard state and SEC verification UI

Browse files
static/app.js CHANGED
@@ -86,6 +86,7 @@ document.addEventListener('DOMContentLoaded', () => {
86
  let latestTrajectoryLower = [];
87
  let cachedHorizons = {};
88
  let cachedLlmByHorizon = {};
 
89
 
90
  const HORIZON_LABELS = {
91
  '1D': '1 Day',
@@ -96,6 +97,8 @@ document.addEventListener('DOMContentLoaded', () => {
96
  '5Y': '5 Years',
97
  };
98
 
 
 
99
  const predictedPriceLabelEl = document.getElementById('predicted-price-label');
100
  const accuracyValueEl = document.getElementById('accuracy-value');
101
  const accuracyRowEl = document.getElementById('accuracy-row');
@@ -372,6 +375,16 @@ document.addEventListener('DOMContentLoaded', () => {
372
  });
373
  }
374
 
 
 
 
 
 
 
 
 
 
 
375
  // Route a failed remote-validation result to the right visual treatment
376
  function _routeValidationError(result, val) {
377
  const code = result.code || '';
@@ -446,10 +459,11 @@ document.addEventListener('DOMContentLoaded', () => {
446
  if (result.valid) {
447
  _validatedTicker = val;
448
  analyzeBtn.disabled = false;
449
- const hasWarning = !!(result.warning);
 
450
  _setInputState(hasWarning ? 'warn' : 'valid');
451
  _showValidationHint(
452
- hasWarning ? `\u26A0 ${result.warning}` : `\u2713 ${result.company_name || val}`,
453
  hasWarning ? 'warn' : 'success'
454
  );
455
  _renderSuggestions([]);
@@ -1251,6 +1265,7 @@ document.addEventListener('DOMContentLoaded', () => {
1251
  currentTicker = String(data?.meta?.symbol || normalizedTicker).toUpperCase();
1252
  input.value = currentTicker;
1253
  _validatedTicker = currentTicker;
 
1254
  cachedLlmByHorizon = {};
1255
  const initialLlm = data?.llm_insights;
1256
  const initialHorizon = String(data?.meta?.risk_horizon || currentHorizon || '1D').trim().toUpperCase();
@@ -1275,6 +1290,7 @@ document.addEventListener('DOMContentLoaded', () => {
1275
  }
1276
  await refreshChartForTimeframe(currentTicker, getActiveTimeframe(), false);
1277
  if (typeof window._irisLoadRecommendations === 'function') { window._irisLoadRecommendations(currentTicker); }
 
1278
 
1279
  } catch (error) {
1280
  clearTimeout(timeoutId);
@@ -1326,6 +1342,172 @@ document.addEventListener('DOMContentLoaded', () => {
1326
  }
1327
  }
1328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  timeframeButtons.forEach((btn) => {
1330
  btn.addEventListener('click', async () => {
1331
  const timeframeKey = String(btn.dataset.timeframe || '').toUpperCase();
@@ -1349,29 +1531,7 @@ document.addEventListener('DOMContentLoaded', () => {
1349
  setActiveHorizon(newHorizon);
1350
  const cached = cachedHorizons[newHorizon];
1351
  if (cached && typeof cached === 'object') {
1352
- latestPredictedPrice = Number(cached.predicted_price);
1353
- latestTrajectory = Array.isArray(cached.prediction_trajectory) ? cached.prediction_trajectory : [];
1354
- latestTrajectoryUpper = Array.isArray(cached.prediction_trajectory_upper) ? cached.prediction_trajectory_upper : [];
1355
- latestTrajectoryLower = Array.isArray(cached.prediction_trajectory_lower) ? cached.prediction_trajectory_lower : [];
1356
-
1357
- if (predictedPriceEl && Number.isFinite(latestPredictedPrice)) {
1358
- predictedPriceEl.textContent = usdFormatter.format(latestPredictedPrice);
1359
- }
1360
-
1361
- const trend = String(cached.trend_label || '').replace(/[^\x20-\x7E]/g, '').trim();
1362
- applyTrendBadge(trend);
1363
- predictedPriceEl.classList.remove('price-up', 'price-down');
1364
- if (trend.includes('UPTREND')) {
1365
- predictedPriceEl.classList.add('price-up');
1366
- } else if (trend.includes('DOWNTREND')) {
1367
- predictedPriceEl.classList.add('price-down');
1368
- }
1369
-
1370
- renderInvestmentSignalBadge(cached.investment_signal || '');
1371
- updateAccuracyDisplay(cached.model_confidence ?? null);
1372
- if (priceCard) {
1373
- priceCard._irisReasoning = String(cached?.iris_reasoning?.summary || '');
1374
- }
1375
  } else {
1376
  // Fallback for older reports without all_horizons.
1377
  if (priceCard) {
@@ -1417,6 +1577,7 @@ document.addEventListener('DOMContentLoaded', () => {
1417
  iris_reasoning: pred?.iris_reasoning || {},
1418
  model_confidence: pred?.model_confidence ?? null,
1419
  };
 
1420
  }
1421
  } catch (err) {
1422
  console.warn('Prediction update failed:', err);
@@ -1451,11 +1612,17 @@ document.addEventListener('DOMContentLoaded', () => {
1451
  btn.classList.remove('is-loading');
1452
  hideChartLoading();
1453
  timeframeButtons.forEach((b) => { b.disabled = false; });
 
1454
  }
1455
  });
1456
  });
1457
  setActiveTimeframe(getActiveTimeframe());
1458
  setActiveHorizon(currentHorizon);
 
 
 
 
 
1459
 
1460
  // Mobile: tap to toggle prediction tooltips.
1461
  document.addEventListener('touchstart', (e) => {
 
86
  let latestTrajectoryLower = [];
87
  let cachedHorizons = {};
88
  let cachedLlmByHorizon = {};
89
+ let latestAnalyzeResponse = null;
90
 
91
  const HORIZON_LABELS = {
92
  '1D': '1 Day',
 
97
  '5Y': '5 Years',
98
  };
99
 
100
+ const DASHBOARD_STATE_STORAGE_KEY = 'iris-dashboard-state-v1';
101
+
102
  const predictedPriceLabelEl = document.getElementById('predicted-price-label');
103
  const accuracyValueEl = document.getElementById('accuracy-value');
104
  const accuracyRowEl = document.getElementById('accuracy-row');
 
375
  });
376
  }
377
 
378
+ function _buildValidationSuccessMessage(result, fallbackTicker) {
379
+ const companyLabel = String(result?.company_name || fallbackTicker || '').trim();
380
+ if (result?.source === 'local_db' && !result?.warning) {
381
+ return companyLabel
382
+ ? `✓ Verified in SEC database: ${companyLabel}`
383
+ : '✓ Verified in SEC database';
384
+ }
385
+ return `✓ ${companyLabel || fallbackTicker || 'Ticker verified'}`;
386
+ }
387
+
388
  // Route a failed remote-validation result to the right visual treatment
389
  function _routeValidationError(result, val) {
390
  const code = result.code || '';
 
459
  if (result.valid) {
460
  _validatedTicker = val;
461
  analyzeBtn.disabled = false;
462
+ const isSecVerified = result.source === 'local_db' && !result.warning;
463
+ const hasWarning = !!(result.warning && !isSecVerified);
464
  _setInputState(hasWarning ? 'warn' : 'valid');
465
  _showValidationHint(
466
+ hasWarning ? `\u26A0 ${result.warning}` : _buildValidationSuccessMessage(result, val),
467
  hasWarning ? 'warn' : 'success'
468
  );
469
  _renderSuggestions([]);
 
1265
  currentTicker = String(data?.meta?.symbol || normalizedTicker).toUpperCase();
1266
  input.value = currentTicker;
1267
  _validatedTicker = currentTicker;
1268
+ latestAnalyzeResponse = data;
1269
  cachedLlmByHorizon = {};
1270
  const initialLlm = data?.llm_insights;
1271
  const initialHorizon = String(data?.meta?.risk_horizon || currentHorizon || '1D').trim().toUpperCase();
 
1290
  }
1291
  await refreshChartForTimeframe(currentTicker, getActiveTimeframe(), false);
1292
  if (typeof window._irisLoadRecommendations === 'function') { window._irisLoadRecommendations(currentTicker); }
1293
+ _saveDashboardState();
1294
 
1295
  } catch (error) {
1296
  clearTimeout(timeoutId);
 
1342
  }
1343
  }
1344
 
1345
+ function applyPredictionState(snapshot) {
1346
+ if (!snapshot || typeof snapshot !== 'object') {
1347
+ return false;
1348
+ }
1349
+
1350
+ latestPredictedPrice = Number(snapshot.predicted_price);
1351
+ latestTrajectory = Array.isArray(snapshot.prediction_trajectory) ? snapshot.prediction_trajectory : [];
1352
+ latestTrajectoryUpper = Array.isArray(snapshot.prediction_trajectory_upper) ? snapshot.prediction_trajectory_upper : [];
1353
+ latestTrajectoryLower = Array.isArray(snapshot.prediction_trajectory_lower) ? snapshot.prediction_trajectory_lower : [];
1354
+
1355
+ if (predictedPriceEl) {
1356
+ predictedPriceEl.textContent = Number.isFinite(latestPredictedPrice)
1357
+ ? usdFormatter.format(latestPredictedPrice)
1358
+ : 'N/A';
1359
+ }
1360
+
1361
+ const trend = String(snapshot.trend_label || '').replace(/[^\x20-\x7E]/g, '').trim();
1362
+ applyTrendBadge(trend);
1363
+ predictedPriceEl.classList.remove('price-up', 'price-down');
1364
+ if (trend.includes('UPTREND')) {
1365
+ predictedPriceEl.classList.add('price-up');
1366
+ } else if (trend.includes('DOWNTREND')) {
1367
+ predictedPriceEl.classList.add('price-down');
1368
+ }
1369
+
1370
+ renderInvestmentSignalBadge(snapshot.investment_signal || '');
1371
+ updateAccuracyDisplay(snapshot.model_confidence ?? null);
1372
+
1373
+ const priceCard = document.querySelector('.price-card');
1374
+ if (priceCard) {
1375
+ priceCard._irisReasoning = String(snapshot?.iris_reasoning?.summary || '');
1376
+ }
1377
+
1378
+ return true;
1379
+ }
1380
+
1381
+ function _saveDashboardState() {
1382
+ if (!currentTicker || !latestAnalyzeResponse) {
1383
+ return;
1384
+ }
1385
+
1386
+ const snapshot = {
1387
+ currentTicker,
1388
+ validatedTicker: _validatedTicker,
1389
+ activeTimeframe: getActiveTimeframe(),
1390
+ currentHorizon,
1391
+ analyzeData: latestAnalyzeResponse,
1392
+ latestPredictedPrice: Number.isFinite(latestPredictedPrice) ? latestPredictedPrice : null,
1393
+ latestTrajectory: Array.isArray(latestTrajectory) ? latestTrajectory : [],
1394
+ latestTrajectoryUpper: Array.isArray(latestTrajectoryUpper) ? latestTrajectoryUpper : [],
1395
+ latestTrajectoryLower: Array.isArray(latestTrajectoryLower) ? latestTrajectoryLower : [],
1396
+ latestAnalyzeHistory: Array.isArray(latestAnalyzeHistory) ? latestAnalyzeHistory : [],
1397
+ latestAnalyzeTimeframe,
1398
+ cachedHorizons: cachedHorizons && typeof cachedHorizons === 'object' ? cachedHorizons : {},
1399
+ cachedLlmByHorizon: cachedLlmByHorizon && typeof cachedLlmByHorizon === 'object' ? cachedLlmByHorizon : {},
1400
+ savedAt: Date.now(),
1401
+ };
1402
+
1403
+ try {
1404
+ sessionStorage.setItem(DASHBOARD_STATE_STORAGE_KEY, JSON.stringify(snapshot));
1405
+ } catch (error) {
1406
+ console.debug('[IRIS] Unable to persist dashboard state:', error);
1407
+ }
1408
+ }
1409
+
1410
+ function _restoreDashboardState() {
1411
+ let rawSnapshot = null;
1412
+ try {
1413
+ rawSnapshot = sessionStorage.getItem(DASHBOARD_STATE_STORAGE_KEY);
1414
+ } catch (error) {
1415
+ rawSnapshot = null;
1416
+ }
1417
+
1418
+ if (!rawSnapshot) {
1419
+ return;
1420
+ }
1421
+
1422
+ let snapshot = null;
1423
+ try {
1424
+ snapshot = JSON.parse(rawSnapshot);
1425
+ } catch (error) {
1426
+ try {
1427
+ sessionStorage.removeItem(DASHBOARD_STATE_STORAGE_KEY);
1428
+ } catch (_) {}
1429
+ return;
1430
+ }
1431
+
1432
+ const analyzeData = snapshot && typeof snapshot === 'object' ? snapshot.analyzeData : null;
1433
+ const restoredTicker = String(snapshot?.currentTicker || analyzeData?.meta?.symbol || '').trim().toUpperCase();
1434
+ if (!analyzeData || !restoredTicker) {
1435
+ return;
1436
+ }
1437
+
1438
+ latestAnalyzeResponse = analyzeData;
1439
+ currentTicker = restoredTicker;
1440
+ _validatedTicker = String(snapshot?.validatedTicker || restoredTicker).trim().toUpperCase();
1441
+ cachedHorizons = snapshot?.cachedHorizons && typeof snapshot.cachedHorizons === 'object'
1442
+ ? snapshot.cachedHorizons
1443
+ : (analyzeData?.all_horizons && typeof analyzeData.all_horizons === 'object' ? analyzeData.all_horizons : {});
1444
+ cachedLlmByHorizon = snapshot?.cachedLlmByHorizon && typeof snapshot.cachedLlmByHorizon === 'object'
1445
+ ? snapshot.cachedLlmByHorizon
1446
+ : {};
1447
+
1448
+ latestPredictedPrice = Number(snapshot?.latestPredictedPrice);
1449
+ if (!Number.isFinite(latestPredictedPrice)) {
1450
+ latestPredictedPrice = Number(analyzeData?.market?.predicted_price_horizon ?? analyzeData?.market?.predicted_price_next_session);
1451
+ }
1452
+ latestTrajectory = Array.isArray(snapshot?.latestTrajectory)
1453
+ ? snapshot.latestTrajectory
1454
+ : (Array.isArray(analyzeData?.market?.prediction_trajectory) ? analyzeData.market.prediction_trajectory : []);
1455
+ latestTrajectoryUpper = Array.isArray(snapshot?.latestTrajectoryUpper)
1456
+ ? snapshot.latestTrajectoryUpper
1457
+ : (Array.isArray(analyzeData?.market?.prediction_trajectory_upper) ? analyzeData.market.prediction_trajectory_upper : []);
1458
+ latestTrajectoryLower = Array.isArray(snapshot?.latestTrajectoryLower)
1459
+ ? snapshot.latestTrajectoryLower
1460
+ : (Array.isArray(analyzeData?.market?.prediction_trajectory_lower) ? analyzeData.market.prediction_trajectory_lower : []);
1461
+ latestAnalyzeHistory = normalizeHistoryPoints(
1462
+ Array.isArray(snapshot?.latestAnalyzeHistory) ? snapshot.latestAnalyzeHistory : analyzeData?.market?.history
1463
+ );
1464
+ latestAnalyzeTimeframe = String(snapshot?.latestAnalyzeTimeframe || '').trim().toUpperCase()
1465
+ || resolveTimeframeFromMeta(analyzeData?.meta || {});
1466
+
1467
+ updateDashboard(analyzeData);
1468
+
1469
+ const restoredTimeframe = TIMEFRAME_TO_QUERY[String(snapshot?.activeTimeframe || '').trim().toUpperCase()]
1470
+ ? String(snapshot.activeTimeframe).trim().toUpperCase()
1471
+ : resolveTimeframeFromMeta(analyzeData?.meta || {});
1472
+ setActiveTimeframe(restoredTimeframe);
1473
+
1474
+ const restoredHorizon = HORIZON_LABELS[String(snapshot?.currentHorizon || '').trim().toUpperCase()]
1475
+ ? String(snapshot.currentHorizon).trim().toUpperCase()
1476
+ : String(analyzeData?.meta?.risk_horizon || currentHorizon).trim().toUpperCase();
1477
+ setActiveHorizon(restoredHorizon);
1478
+
1479
+ const restoredPrediction = cachedHorizons[restoredHorizon];
1480
+ if (restoredPrediction) {
1481
+ applyPredictionState(restoredPrediction);
1482
+ }
1483
+
1484
+ input.value = currentTicker;
1485
+ analyzeBtn.disabled = false;
1486
+ if (clearBtn) clearBtn.classList.remove('hidden');
1487
+ _setInputState('valid');
1488
+ _clearValidationHint();
1489
+ dashboard.classList.remove('hidden');
1490
+
1491
+ if (Array.isArray(latestAnalyzeHistory) && latestAnalyzeHistory.length > 0) {
1492
+ renderChart(
1493
+ latestAnalyzeHistory,
1494
+ latestPredictedPrice,
1495
+ latestTrajectory,
1496
+ latestTrajectoryUpper,
1497
+ latestTrajectoryLower,
1498
+ );
1499
+ }
1500
+ if (typeof window._irisLoadRecommendations === 'function') {
1501
+ window._irisLoadRecommendations(currentTicker);
1502
+ }
1503
+
1504
+ requestAnimationFrame(() => {
1505
+ requestAnimationFrame(() => {
1506
+ syncHeadlinesCardHeight();
1507
+ });
1508
+ });
1509
+ }
1510
+
1511
  timeframeButtons.forEach((btn) => {
1512
  btn.addEventListener('click', async () => {
1513
  const timeframeKey = String(btn.dataset.timeframe || '').toUpperCase();
 
1531
  setActiveHorizon(newHorizon);
1532
  const cached = cachedHorizons[newHorizon];
1533
  if (cached && typeof cached === 'object') {
1534
+ applyPredictionState(cached);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  } else {
1536
  // Fallback for older reports without all_horizons.
1537
  if (priceCard) {
 
1577
  iris_reasoning: pred?.iris_reasoning || {},
1578
  model_confidence: pred?.model_confidence ?? null,
1579
  };
1580
+ applyPredictionState(cachedHorizons[newHorizon]);
1581
  }
1582
  } catch (err) {
1583
  console.warn('Prediction update failed:', err);
 
1612
  btn.classList.remove('is-loading');
1613
  hideChartLoading();
1614
  timeframeButtons.forEach((b) => { b.disabled = false; });
1615
+ _saveDashboardState();
1616
  }
1617
  });
1618
  });
1619
  setActiveTimeframe(getActiveTimeframe());
1620
  setActiveHorizon(currentHorizon);
1621
+ _restoreDashboardState();
1622
+
1623
+ window.addEventListener('pagehide', () => {
1624
+ _saveDashboardState();
1625
+ });
1626
 
1627
  // Mobile: tap to toggle prediction tooltips.
1628
  document.addEventListener('touchstart', (e) => {
templates/index.html CHANGED
@@ -253,7 +253,7 @@
253
  <script
254
  src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js?v=4"></script>
255
  <script src="/static/tickerValidation.js?v=1"></script>
256
- <script src="/static/app.js?v=24"></script>
257
  </body>
258
 
259
  </html>
 
253
  <script
254
  src="https://unpkg.com/lightweight-charts@4.1.1/dist/lightweight-charts.standalone.production.js?v=4"></script>
255
  <script src="/static/tickerValidation.js?v=1"></script>
256
+ <script src="/static/app.js?v=25"></script>
257
  </body>
258
 
259
  </html>
tests/test_ticker_validator.py CHANGED
@@ -92,6 +92,7 @@ class TestLocalDbFastPath(unittest.TestCase):
92
  self.assertTrue(result.valid)
93
  self.assertEqual(result.company_name, "Apple Inc.")
94
  self.assertEqual(result.source, "local_db")
 
95
  mock_ticker.assert_not_called()
96
 
97
 
 
92
  self.assertTrue(result.valid)
93
  self.assertEqual(result.company_name, "Apple Inc.")
94
  self.assertEqual(result.source, "local_db")
95
+ self.assertFalse(result.warning)
96
  mock_ticker.assert_not_called()
97
 
98
 
tests/test_validation_edge_cases.py CHANGED
@@ -150,14 +150,14 @@ class TestGracefulDegradation(unittest.TestCase):
150
 
151
  # 8 ---
152
  def test_graceful_degradation_api_down(self):
153
- """When yfinance times out but ticker IS in local DB, return valid with warning."""
154
  with patch(
155
  "ticker_validator.yf.Ticker", side_effect=TimeoutError("Connection timed out")
156
  ), patch("ticker_validator.is_known_ticker", return_value=True):
157
  result = validate_ticker("AAPL")
158
 
159
- self.assertTrue(result.valid, "Should degrade gracefully to local DB")
160
- self.assertIn("local database", result.warning.lower())
161
  self.assertEqual(result.source, "local_db")
162
 
163
  # 9 ---
 
150
 
151
  # 8 ---
152
  def test_graceful_degradation_api_down(self):
153
+ """Known SEC tickers should validate locally even if yfinance is unavailable."""
154
  with patch(
155
  "ticker_validator.yf.Ticker", side_effect=TimeoutError("Connection timed out")
156
  ), patch("ticker_validator.is_known_ticker", return_value=True):
157
  result = validate_ticker("AAPL")
158
 
159
+ self.assertTrue(result.valid, "Known SEC tickers should validate offline")
160
+ self.assertFalse(result.warning)
161
  self.assertEqual(result.source, "local_db")
162
 
163
  # 9 ---
ticker_validator.py CHANGED
@@ -248,7 +248,6 @@ def validate_ticker_exists(ticker: str) -> TickerValidationResult:
248
  ticker=normalized,
249
  company_name=get_company_name(normalized) or "(verified offline)",
250
  source="local_db",
251
- warning="Ticker verified from local database (SEC snapshot).",
252
  )
253
 
254
  return _cached_api_lookup(normalized)
 
248
  ticker=normalized,
249
  company_name=get_company_name(normalized) or "(verified offline)",
250
  source="local_db",
 
251
  )
252
 
253
  return _cached_api_lookup(normalized)