Spaces:
Configuration error
Configuration error
Download commodities-alerts.html from fabrit/assets-tracker-api: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/fabrit/assets-tracker-api/resolve/main/commodities-alerts.html
- Command line
-
hf download hf://spaces/fabrit/assets-tracker-api/commodities-alerts.html
-
curl -L -o commodities-alerts.html https://huggingface.co/spaces/fabrit/assets-tracker-api/resolve/main/commodities-alerts.html
14 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Commodity Price Alerts</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #1a1a1a; color: #f8fafc; min-height: 100vh; padding: 1rem; } | |
| .container { max-width: 800px; margin: 0 auto; } | |
| header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #333; } | |
| h1 { font-size: 1.5rem; background: linear-gradient(135deg, #f59e0b, #d97706); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .debug { background: #111; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-family: monospace; font-size: 0.75rem; white-space: pre-wrap; max-height: 80px; overflow-y: auto; color: #94a3b8; } | |
| .card { background: #222; border: 1px solid #333; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; transition: all 0.3s; } | |
| .card:hover { border-color: #f59e0b; } | |
| .card.flash { animation: flash 0.5s ease 3; } | |
| @keyframes flash { 0%,100% { border-color: #333; box-shadow: none; } 50% { border-color: #f59e0b; box-shadow: 0 0 30px #f59e0b50; } } | |
| .card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem; } | |
| .asset-name { font-size: 1.25rem; font-weight: 600; display: flex; align-items: center; gap: 0.5rem; } | |
| .asset-icon { font-size: 1.5rem; } | |
| .price { font-size: 1.75rem; font-weight: 700; color: #f59e0b; margin-bottom: 0.5rem; } | |
| .unit { font-size: 0.875rem; color: #94a3b8; margin-bottom: 0.5rem; } | |
| .range { color: #94a3b8; font-size: 0.875rem; margin-bottom: 0.75rem; } | |
| .state { padding: 0.4rem 0.8rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; display: inline-block; } | |
| .state.inside { background: #22c55e20; color: #22c55e; } | |
| .state.outside { background: #374151; color: #94a3b8; } | |
| .btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 0.875rem; } | |
| .btn-primary { background: #f59e0b; color: #1a1a1a; } | |
| .btn-primary:hover { background: #fbbf24; } | |
| .btn-danger { background: transparent; color: #ef4444; border: 1px solid #ef4444; } | |
| .btn-danger:hover { background: #ef4444; color: white; } | |
| .modal { position: fixed; inset: 0; background: rgba(0,0,0,0.85); display: none; align-items: center; justify-content: center; z-index: 100; } | |
| .modal.active { display: flex; } | |
| .modal-content { background: #222; border: 1px solid #333; border-radius: 16px; padding: 2rem; width: 90%; max-width: 400px; } | |
| .modal h2 { margin-bottom: 1.5rem; } | |
| .form-group { margin-bottom: 1rem; } | |
| .form-group label { display: block; margin-bottom: 0.5rem; color: #94a3b8; font-size: 0.875rem; } | |
| .form-group input, .form-group select { width: 100%; padding: 0.75rem; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #f8fafc; font-size: 1rem; } | |
| .form-group input:focus, .form-group select:focus { outline: none; border-color: #f59e0b; } | |
| .btn-block { width: 100%; padding: 1rem; margin-top: 0.5rem; } | |
| .toast { position: fixed; top: 20px; right: 20px; background: #222; border: 2px solid #22c55e; border-radius: 12px; padding: 1rem 1.5rem; z-index: 1000; animation: slideIn 0.3s; } | |
| @keyframes slideIn { from { opacity: 0; transform: translateX(120%); } to { opacity: 1; transform: translateX(0); } } | |
| .toast-title { font-weight: 700; margin-bottom: 0.25rem; display: flex; align-items: center; gap: 0.5rem; } | |
| .toast-msg { font-size: 0.875rem; color: #94a3b8; } | |
| .empty { text-align: center; padding: 3rem; color: #94a3b8; } | |
| .section-title { font-size: 1.1rem; margin: 1.5rem 0 1rem; color: #94a3b8; } | |
| .alert-item { background: #222; border: 1px solid #333; border-radius: 8px; padding: 1rem; margin-bottom: 0.5rem; display: flex; gap: 1rem; align-items: center; } | |
| .status { display: flex; gap: 1rem; margin-bottom: 1rem; font-size: 0.875rem; color: #94a3b8; } | |
| .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; } | |
| .note { background: #333; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; font-size: 0.875rem; color: #94a3b8; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="toasts"></div> | |
| <div class="container"> | |
| <header> | |
| <h1>π₯ Commodity Alerts</h1> | |
| <button class="btn btn-primary" onclick="showModal()">+ Add Alert</button> | |
| </header> | |
| <div class="note">Prices from Yahoo Finance (via proxy)</div> | |
| <div class="status"><span class="status-dot"></span> Live commodity prices</div> | |
| <div id="debug" class="debug">Initializing...</div> | |
| <div id="alertsContainer"></div> | |
| <div class="section-title">π Alert History</div> | |
| <div id="historyContainer"></div> | |
| </div> | |
| <div id="modal" class="modal" onclick="if(event.target===this)hideModal()"> | |
| <div class="modal-content"> | |
| <h2>Add Commodity Alert</h2> | |
| <div class="form-group"> | |
| <label>Commodity</label> | |
| <select id="assetSelect"> | |
| <option value="">Select commodity...</option> | |
| <option value="GC=F">Gold ($/oz)</option> | |
| <option value="SI=F">Silver ($/oz)</option> | |
| <option value="PL=F">Platinum ($/oz)</option> | |
| <option value="PA=F">Palladium ($/oz)</option> | |
| <option value="CL=F">Crude Oil ($/bbl)</option> | |
| <option value="NG=F">Natural Gas ($/MMBtu)</option> | |
| <option value="HG=F">Copper ($/lb)</option> | |
| <option value="ZC=F">Corn ($/bu)</option> | |
| <option value="ZS=F">Soybeans ($/bu)</option> | |
| <option value="ZW=F">Wheat ($/bu)</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Min Price</label> | |
| <input type="number" id="minPrice" step="any" placeholder="1900"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Max Price</label> | |
| <input type="number" id="maxPrice" step="any" placeholder="2000"> | |
| </div> | |
| <button class="btn btn-primary btn-block" onclick="addAlert()">Add Alert</button> | |
| <button class="btn btn-danger btn-block" onclick="hideModal()">Cancel</button> | |
| </div> | |
| </div> | |
| <script> | |
| const COMMODITY_ICONS = { | |
| 'GC=F': 'π₯', 'SI=F': 'π₯', 'PL=F': 'π₯', 'PA=F': 'βͺ', | |
| 'CL=F': 'π’οΈ', 'NG=F': 'π₯', 'HG=F': 'πΆ', 'ZC=F': 'π½', | |
| 'ZS=F': 'π’', 'ZW=F': 'π' | |
| }; | |
| const COMMODITY_NAMES = { | |
| 'GC=F': 'Gold', 'SI=F': 'Silver', 'PL=F': 'Platinum', 'PA=F': 'Palladium', | |
| 'CL=F': 'Crude Oil', 'NG=F': 'Natural Gas', 'HG=F': 'Copper', 'ZC=F': 'Corn', | |
| 'ZS=F': 'Soybeans', 'ZW=F': 'Wheat' | |
| }; | |
| const COMMODITY_UNITS = { | |
| 'GC=F': 'per oz', 'SI=F': 'per oz', 'PL=F': 'per oz', 'PA=F': 'per oz', | |
| 'CL=F': 'per bbl', 'NG=F': 'per MMBtu', 'HG=F': 'per lb', 'ZC=F': 'per bu', | |
| 'ZS=F': 'per bu', 'ZW=F': 'per bu' | |
| }; | |
| let alerts = JSON.parse(localStorage.getItem('commodity_alerts') || '[]'); | |
| let history = JSON.parse(localStorage.getItem('commodity_history') || '[]'); | |
| let prices = {}; | |
| let triggered = new Set(); | |
| const debug = document.getElementById('debug'); | |
| function log(msg) { | |
| debug.textContent = new Date().toLocaleTimeString() + ' | ' + msg + '\n' + debug.textContent.slice(0, 300); | |
| } | |
| function save() { | |
| localStorage.setItem('commodity_alerts', JSON.stringify(alerts)); | |
| localStorage.setItem('commodity_history', JSON.stringify(history.slice(0, 30))); | |
| } | |
| function showModal() { | |
| document.getElementById('modal').classList.add('active'); | |
| } | |
| function hideModal() { | |
| document.getElementById('modal').classList.remove('active'); | |
| } | |
| function addAlert() { | |
| const symbol = document.getElementById('assetSelect').value; | |
| const min = parseFloat(document.getElementById('minPrice').value); | |
| const max = parseFloat(document.getElementById('maxPrice').value); | |
| if (!symbol || !min || !max || min >= max) { | |
| alert('Please fill all fields correctly.\nMin must be less than Max.'); | |
| return; | |
| } | |
| alerts = alerts.filter(a => a.symbol !== symbol); | |
| alerts.push({ symbol, min, max, lastInRange: false }); | |
| save(); | |
| render(); | |
| hideModal(); | |
| log('Added: ' + COMMODITY_NAMES[symbol] + ' | $' + min + ' - $' + max); | |
| } | |
| function removeAlert(symbol) { | |
| alerts = alerts.filter(a => a.symbol !== symbol); | |
| triggered.clear(); | |
| save(); | |
| render(); | |
| log('Removed: ' + symbol); | |
| } | |
| function triggerAlert(symbol, price, min, max) { | |
| const key = symbol + '-' + min + '-' + max; | |
| if (triggered.has(key)) return; | |
| triggered.add(key); | |
| history.unshift({ symbol, price, min, max, time: Date.now() }); | |
| save(); | |
| renderHistory(); | |
| showToast(symbol, price, min, max); | |
| if (Notification.permission === 'granted') { | |
| new Notification(COMMODITY_NAMES[symbol] + ' Alert!', { | |
| body: 'Price $' + price.toLocaleString() + ' is in range ($' + min + ' - $' + max + ')', | |
| }); | |
| } | |
| if ('vibrate' in navigator) navigator.vibrate([200, 100, 200]); | |
| const card = document.getElementById('card-' + symbol.replace(/[^a-z]/g, '')); | |
| if (card) { | |
| card.classList.add('flash'); | |
| setTimeout(() => card.classList.remove('flash'), 2000); | |
| } | |
| } | |
| function showToast(symbol, price, min, max) { | |
| const div = document.createElement('div'); | |
| div.className = 'toast'; | |
| div.innerHTML = ` | |
| <div class="toast-title">π ${COMMODITY_NAMES[symbol]} Alert!</div> | |
| <div class="toast-msg">$${price.toLocaleString()} in range ($${min.toLocaleString()} - $${max.toLocaleString()})</div> | |
| `; | |
| document.getElementById('toasts').appendChild(div); | |
| setTimeout(() => div.remove(), 5000); | |
| } | |
| function render() { | |
| const container = document.getElementById('alertsContainer'); | |
| if (alerts.length === 0) { | |
| container.innerHTML = '<div class="empty">No alerts set. Click "+ Add Alert" to start.</div>'; | |
| return; | |
| } | |
| container.innerHTML = alerts.map(a => { | |
| const price = prices[a.symbol]; | |
| const inRange = price >= a.min && price <= a.max; | |
| const icon = COMMODITY_ICONS[a.symbol] || 'π¦'; | |
| const name = COMMODITY_NAMES[a.symbol] || a.symbol; | |
| const unit = COMMODITY_UNITS[a.symbol] || ''; | |
| return ` | |
| <div class="card" id="card-${a.symbol.replace(/[^a-z]/g, '')}"> | |
| <div class="card-header"> | |
| <span class="asset-name"><span class="asset-icon">${icon}</span>${name}</span> | |
| <button class="btn btn-danger" onclick="removeAlert('${a.symbol}')">Remove</button> | |
| </div> | |
| <div class="price">${price ? '$' + price.toLocaleString() : 'Loading...'}</div> | |
| <div class="unit">${unit}</div> | |
| <div class="range">Target: $${a.min.toLocaleString()} - $${a.max.toLocaleString()}</div> | |
| <span class="state ${inRange ? 'inside' : 'outside'}">${inRange ? 'β In Range' : 'β Waiting'}</span> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function renderHistory() { | |
| const container = document.getElementById('historyContainer'); | |
| if (history.length === 0) { | |
| container.innerHTML = '<div class="empty" style="padding:1rem">No alerts triggered yet</div>'; | |
| return; | |
| } | |
| container.innerHTML = history.map(h => ` | |
| <div class="alert-item"> | |
| <span>π</span> | |
| <div> | |
| <strong>${COMMODITY_NAMES[h.symbol] || h.symbol}</strong> - $${h.price.toLocaleString()} | |
| <div style="font-size:0.75rem;color:#64748b;margin-top:2px"> | |
| Range: $${h.min.toLocaleString()} - $${h.max.toLocaleString()} | ${new Date(h.time).toLocaleTimeString()} | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| async function fetchPrices() { | |
| if (alerts.length === 0) return; | |
| const symbols = alerts.map(a => a.symbol); | |
| const symbolsParam = symbols.join(','); | |
| try { | |
| const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(symbolsParam)}`; | |
| log('Fetching commodities...'); | |
| const res = await fetch(url); | |
| if (!res.ok) { | |
| log('API error: ' + res.status); | |
| return; | |
| } | |
| const data = await res.json(); | |
| if (data.chart && data.chart.result) { | |
| for (const result of data.chart.result) { | |
| const symbol = result.meta.symbol; | |
| const price = result.meta.regularMarketPrice; | |
| if (price) { | |
| prices[symbol] = price; | |
| log(symbol + ': $' + price); | |
| } | |
| } | |
| } | |
| log('Price update complete'); | |
| } catch (e) { | |
| log('Fetch error: ' + e.message); | |
| } | |
| } | |
| function checkAlerts() { | |
| alerts.forEach(a => { | |
| const price = prices[a.symbol]; | |
| if (!price) return; | |
| const wasIn = a.lastInRange === true; | |
| const isIn = price >= a.min && price <= a.max; | |
| if (isIn && !wasIn) { | |
| log('TRIGGER: ' + a.symbol + ' at $' + price); | |
| triggerAlert(a.symbol, price, a.min, a.max); | |
| } | |
| a.lastInRange = isIn; | |
| }); | |
| save(); | |
| render(); | |
| } | |
| async function init() { | |
| log('Starting Commodity Alerts...'); | |
| render(); | |
| renderHistory(); | |
| await fetchPrices(); | |
| checkAlerts(); | |
| setInterval(async () => { | |
| await fetchPrices(); | |
| checkAlerts(); | |
| }, 30000); | |
| if ('Notification' in window && Notification.permission === 'default') { | |
| Notification.requestPermission(); | |
| } | |
| } | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |