diff --git a/app/routes/investments.py b/app/routes/investments.py index e41c609..2c5ca1a 100644 --- a/app/routes/investments.py +++ b/app/routes/investments.py @@ -257,6 +257,19 @@ def refresh_prices(): @investments_bp.route('/api/price/') @login_required def api_price(ticker): - """Live price lookup for a ticker — used in the add transaction form.""" - price = fetch_price(ticker) - return jsonify({'ticker': ticker.upper(), 'price': price}) + """Live price lookup for a ticker — used in the add/edit investment form.""" + ticker = ticker.upper().strip() + error = None + price = None + try: + price = fetch_price(ticker) + if price is None: + error = f'No data returned for {ticker}. Check the ticker format.' + except Exception as e: + error = str(e) + + return jsonify({ + 'ticker': ticker, + 'price': price, + 'error': error, + }) diff --git a/app/templates/investments/form.html b/app/templates/investments/form.html index 04d4d72..293c942 100644 --- a/app/templates/investments/form.html +++ b/app/templates/investments/form.html @@ -80,18 +80,38 @@ document.getElementById('checkTicker').addEventListener('click', function() { const ticker = document.getElementById('tickerInput').value.trim().toUpperCase(); const result = document.getElementById('tickerResult'); + const btn = document.getElementById('checkTicker'); if (!ticker) { result.textContent = ''; return; } - result.innerHTML = 'Fetching…'; - fetch('/investments/api/price/' + encodeURIComponent(ticker)) + + result.innerHTML = 'Fetching…'; + btn.disabled = true; + + // AbortController for 15s timeout + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 15000); + + fetch('/investments/api/price/' + encodeURIComponent(ticker), { signal: controller.signal }) .then(r => r.json()) .then(data => { - if (data.price) { - result.innerHTML = '' + ticker + ' → {{ current_user.currency_symbol }}' + data.price.toFixed(4) + ''; + clearTimeout(timer); + btn.disabled = false; + if (data.price != null) { + result.innerHTML = '' + + ticker + ' → {{ current_user.currency_symbol }}' + data.price.toFixed(4) + ''; } else { - result.innerHTML = 'Ticker not found or no data'; + const msg = data.error || 'Ticker not found or no data'; + result.innerHTML = '' + msg + ''; } }) - .catch(() => { result.innerHTML = 'Could not fetch price'; }); + .catch(err => { + clearTimeout(timer); + btn.disabled = false; + if (err.name === 'AbortError') { + result.innerHTML = 'Request timed out — Yahoo Finance may be slow. Try again.'; + } else { + result.innerHTML = 'Network error. Check server connectivity.'; + } + }); }); {% endblock %} diff --git a/app/templates/investments/transaction_form.html b/app/templates/investments/transaction_form.html index 6527894..bcd8d27 100644 --- a/app/templates/investments/transaction_form.html +++ b/app/templates/investments/transaction_form.html @@ -105,17 +105,33 @@ priceInput.addEventListener('input', updateTotal); const fillBtn = document.getElementById('fillPrice'); if (fillBtn) { fillBtn.addEventListener('click', function() { + fillBtn.disabled = true; fillBtn.textContent = '…'; - fetch('/investments/api/price/{{ inv.ticker }}') + + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 15000); + + fetch('/investments/api/price/{{ inv.ticker }}', { signal: controller.signal }) .then(r => r.json()) .then(data => { - if (data.price) { + clearTimeout(timer); + fillBtn.disabled = false; + fillBtn.textContent = '↓'; + if (data.price != null) { priceInput.value = data.price.toFixed(4); updateTotal(); + } else { + fillBtn.title = data.error || 'No price available'; + fillBtn.style.color = '#ef4444'; + setTimeout(() => { fillBtn.style.color = ''; fillBtn.title = ''; }, 3000); } - fillBtn.textContent = '↓'; }) - .catch(() => { fillBtn.textContent = '↓'; }); + .catch(err => { + clearTimeout(timer); + fillBtn.disabled = false; + fillBtn.textContent = '↓'; + fillBtn.title = err.name === 'AbortError' ? 'Timed out' : 'Network error'; + }); }); } {% endif %}