diff --git a/templates/index.html b/templates/index.html index d0df754..4834125 100644 --- a/templates/index.html +++ b/templates/index.html @@ -325,112 +325,114 @@ if (loading) loading.style.display = 'inline'; - try { + try { // <--- THIS WAS MISSING const response = await fetch('/api/summary'); const result = await response.json(); - // 1. Update Sync Time - if (result.last_sync) { - // Ensure format is ISO compliant for mobile browsers - const dateStr = result.last_sync.replace(' ', 'T'); - const dateObj = new Date(dateStr + (dateStr.includes('Z') ? '' : 'Z')); - - document.getElementById('lastSyncTime').innerText = dateObj.toLocaleTimeString([], { - hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false - }); - } - // --- FIX: Extract data from result --- - const data = result.data; - - if (!data || data.length === 0) { - tbody.innerHTML = 'No data found.'; - return; - } + // --- 1. DATE & TIMEZONE FIX --- + if (syncDisplay && result.last_sync) { + const dateStr = result.last_sync.trim().replace(' ', 'T'); + const dateObj = new Date(dateStr + (dateStr.includes('Z') ? '' : 'Z')); + + syncDisplay.innerText = dateObj.toLocaleString([], { + month: 'short', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', + hour12: false + }); + + syncDisplay.classList.add('text-success'); + setTimeout(() => syncDisplay.classList.remove('text-success'), 2000); + } + // --- 2. Extract data from result --- + const data = result.data; let htmlContent = ''; const todayStr = new Date().toISOString().split('T')[0]; - data.forEach(item => { - // --- A. Error Handling --- - if (item.error || !item.last_close || item.last_close === 'N/A') { + if (data && data.length > 0) { + data.forEach(item => { + // --- A. Error Handling --- + if (item.error || !item.last_close || item.last_close === 'N/A') { + htmlContent += ` + +
${item.name || item.symbol}
+ + Needs Sync + Data not found or corrupted. + + `; + return; + } + + // --- B. Preparation & Math --- + let displayDate = item.last_date || "N/A"; + let dateColor = '#dc3545'; + if (item.last_date && item.last_date.includes('-')) { + const parts = item.last_date.split('-'); + displayDate = `${parts[2]}/${parts[1]}`; + dateColor = (item.last_date === todayStr) ? '#28a745' : '#dc3545'; + } + + const current = parseFloat(item.last_close) || 0; + const low = parseFloat(item.low_52) || 0; + const high = parseFloat(item.high_52) || 0; + + let rangePct = high > low ? Math.min(Math.max(((current - low) / (high - low)) * 100, 0), 100) : 0; + const rangeColor = rangePct > 80 ? 'text-danger' : (rangePct < 20 ? 'text-success' : 'text-muted'); + + const rsiVal = item.rsi !== null ? item.rsi : "N/A"; + const bbRaw = parseFloat(item.bb_pct); + const bbDisplay = !isNaN(bbRaw) ? (bbRaw * 100).toFixed(0) + '%' : 'N/A'; + const z60Val = item.z60 !== null ? item.z60 : "N/A"; + const z120Val = item.z120 !== null ? item.z120 : "N/A"; + + const isFresh = item.last_date === todayStr; + const dateBadgeClass = isFresh ? 'badge bg-success-subtle text-success border border-success-subtle' : 'text-muted'; + + // --- C. Construct Row --- htmlContent += ` -
${item.name || item.symbol}
- - Needs Sync - Data not found or corrupted. +
${item.name || item.symbol}
+ ${displayDate} + ${item.last_close} + + ${item.change_pct >= 0 ? '+' : ''}${item.change_pct}% + +
+ ${item.low_52}${item.high_52} +
+
+
+
+ + ${rsiVal} + ${bbDisplay} + ${z60Val} + ${z120Val} + ${typeof formatEma === 'function' ? formatEma(item.last_ema20) : item.last_ema20} + ${typeof formatEma === 'function' ? formatEma(item.last_ema50) : item.last_ema50} + ${typeof formatEma === 'function' ? formatEma(item.last_ema100) : item.last_ema100} + ${typeof formatEma === 'function' ? formatEma(item.last_ema200) : item.last_ema200} + ${typeof formatKD === 'function' ? formatKD(item.kd_values) : 'N/A'} `; - return; - } - - // --- B. Preparation & Math --- - let displayDate = item.last_date || "N/A"; - let dateColor = '#dc3545'; - if (item.last_date && item.last_date.includes('-')) { - const parts = item.last_date.split('-'); - displayDate = `${parts[2]}/${parts[1]}`; - dateColor = (item.last_date === todayStr) ? '#28a745' : '#dc3545'; - } - - const current = parseFloat(item.last_close) || 0; - const low = parseFloat(item.low_52) || 0; - const high = parseFloat(item.high_52) || 0; - - // 52W Range calculation - let rangePct = high > low ? Math.min(Math.max(((current - low) / (high - low)) * 100, 0), 100) : 0; - const rangeColor = rangePct > 80 ? 'text-danger' : (rangePct < 20 ? 'text-success' : 'text-muted'); - - // Indicators - const rsiVal = item.rsi !== null ? item.rsi : "N/A"; - const bbRaw = parseFloat(item.bb_pct); - const bbDisplay = !isNaN(bbRaw) ? (bbRaw * 100).toFixed(0) + '%' : 'N/A'; - const z60Val = item.z60 !== null ? item.z60 : "N/A"; - const z120Val = item.z120 !== null ? item.z120 : "N/A"; - - const isFresh = item.last_date === todayStr; - const dateBadgeClass = isFresh ? 'badge bg-success-subtle text-success border border-success-subtle' : 'text-muted'; - - // --- C. Construct Row (14 Columns) --- - htmlContent += ` - -
${item.name || item.symbol}
- ${displayDate} - ${item.last_close} - - ${item.change_pct >= 0 ? '+' : ''}${item.change_pct}% - - -
- ${item.low_52}${item.high_52} -
-
-
-
- - ${rsiVal} - ${bbDisplay} - ${z60Val} - ${z120Val} - ${typeof formatEma === 'function' ? formatEma(item.last_ema20) : item.last_ema20} - ${typeof formatEma === 'function' ? formatEma(item.last_ema50) : item.last_ema50} - ${typeof formatEma === 'function' ? formatEma(item.last_ema100) : item.last_ema100} - ${typeof formatEma === 'function' ? formatEma(item.last_ema200) : item.last_ema200} - ${typeof formatKD === 'function' ? formatKD(item.kd_values) : 'N/A'} - `; - }); + }); + } else { + htmlContent = 'No data available'; + } tbody.innerHTML = htmlContent; - } catch (error) { + } catch (error) { console.error("Fetch error:", error); - if (tbody) tbody.innerHTML = 'Error loading data.'; + if (tbody) tbody.innerHTML = 'API Error: Check Console'; } finally { if (loading) loading.style.display = 'none'; } } // --- 4. Global Sync --- async function runGlobalSync() { + console.log("Sync triggered..."); const syncBtn = document.getElementById('syncBtn'); const loading = document.getElementById('loading'); if (!syncBtn) return; @@ -457,17 +459,24 @@ // This checks if the Flask server is responding every 30 seconds async function checkStatus() { const indicator = document.getElementById('statusIndicator'); + + // GUARD: If the element is missing from HTML, exit immediately + if (!indicator) return; + try { - const response = await fetch('/api/summary'); // Or a dedicated /health endpoint + const response = await fetch('/api/summary'); if (response.ok) { - indicator.innerHTML = '● Online'; - indicator.className = 'text-success'; + // Using a span for the dot allows us to animate it separately if desired + indicator.innerHTML = ' Online'; + indicator.className = 'text-success fw-bold'; } else { throw new Error(); } } catch (e) { - indicator.innerHTML = '● Offline'; - indicator.className = 'text-danger'; + if (indicator) { + indicator.innerHTML = '● Offline'; + indicator.className = 'text-danger fw-bold'; + } } }