Spaces:
Running
Running
Commit ·
fa65b59
1
Parent(s): 82e7fcd
Preserve dashboard state and SEC verification UI
Browse files- static/app.js +192 -25
- templates/index.html +1 -1
- tests/test_ticker_validator.py +1 -0
- tests/test_validation_edge_cases.py +3 -3
- ticker_validator.py +0 -1
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
|
|
|
|
| 450 |
_setInputState(hasWarning ? 'warn' : 'valid');
|
| 451 |
_showValidationHint(
|
| 452 |
-
hasWarning ? `\u26A0 ${result.warning}` :
|
| 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 |
-
|
| 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=
|
| 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 |
-
"""
|
| 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, "
|
| 160 |
-
self.
|
| 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)
|