Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Stock Alchemist | Signal Generator</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-color: #0F172A; | |
| --card-bg: #1E293B; | |
| --text-primary: #F8FAFC; | |
| --text-secondary: #94A3B8; | |
| --accent-green: #10B981; | |
| --accent-red: #EF4444; | |
| --accent-yellow: #F59E0B; | |
| --accent-blue: #3B82F6; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| padding: 2rem; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 2rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| } | |
| h1 { font-weight: 700; font-size: 1.8rem; background: linear-gradient(to right, var(--accent-blue), var(--accent-green)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .status-badge { | |
| padding: 0.5rem 1rem; | |
| border-radius: 20px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| background: rgba(16, 185, 129, 0.2); | |
| color: var(--accent-green); | |
| } | |
| .actions { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 1rem; | |
| margin-bottom: 2rem; | |
| } | |
| .card { | |
| background: var(--card-bg); | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| border: 1px solid rgba(255,255,255,0.05); | |
| } | |
| h2 { font-size: 1.1rem; margin-bottom: 1rem; color: var(--text-secondary); } | |
| .btn { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| background: var(--accent-blue); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-decoration: none; | |
| font-size: 0.95rem; | |
| } | |
| .btn:hover { filter: brightness(1.1); transform: translateY(-1px); } | |
| .btn:disabled { opacity: 0.7; cursor: not-allowed; } | |
| .btn-green { background: var(--accent-green); } | |
| .btn-purple { background: #8B5CF6; } | |
| input[type="text"] { | |
| width: 100%; | |
| padding: 0.75rem; | |
| border-radius: 8px; | |
| border: 1px solid #334155; | |
| background: #0F172A; | |
| color: white; | |
| margin-bottom: 1rem; | |
| font-family: inherit; | |
| } | |
| input:focus { outline: 2px solid var(--accent-blue); border-color: transparent; } | |
| .signals-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.95rem; | |
| } | |
| .signals-table th { | |
| text-align: left; | |
| padding: 1rem; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .signals-table td { | |
| padding: 1rem; | |
| border-bottom: 1px solid rgba(255,255,255,0.05); | |
| } | |
| .tag { | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 12px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| display: inline-block; | |
| } | |
| .tag-buy { background: rgba(16, 185, 129, 0.2); color: var(--accent-green); } | |
| .tag-sell { background: rgba(239, 68, 68, 0.2); color: var(--accent-red); } | |
| .tag-hold { background: rgba(245, 158, 11, 0.2); color: var(--accent-yellow); } | |
| .empty-state { | |
| text-align: center; | |
| padding: 3rem; | |
| color: var(--text-secondary); | |
| } | |
| /* Scale down for mobile */ | |
| @media (max-width: 600px) { | |
| .actions { grid-template-columns: 1fr; } | |
| h1 { font-size: 1.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container" id="mainContent"> | |
| <header> | |
| <h1>🧪 Stock Alchemist</h1> | |
| <div class="status-badge" id="systemStatus">● System Online</div> | |
| </header> | |
| <div class="actions"> | |
| <!-- Generate Signal Card --> | |
| <div class="card"> | |
| <h2>⚡ Generate Signal</h2> | |
| <input type="text" id="tickerInput" placeholder="Enter Ticker (e.g. AAPL)" /> | |
| <button class="btn btn-green" onclick="generateSignal()" id="genBtn"> | |
| Generate AI Signal | |
| </button> | |
| </div> | |
| <button class="btn" onclick="window.location.href='/logs'" style="flex: 1; background: #334155;"> | |
| View Logs | |
| </button> | |
| </div> | |
| </div> | |
| <!-- System Stats --> | |
| <div class="card"> | |
| <h2>📊 System Status</h2> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;"> | |
| <span style="color:var(--text-secondary)">Database</span> | |
| <span id="dbStatus">Checking...</span> | |
| </div> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 1.5rem;"> | |
| <span style="color:var(--text-secondary)">Ollama</span> | |
| <span id="ollamaStatus">Checking...</span> | |
| </div> | |
| <div style="display: flex; gap: 0.5rem;"> | |
| <button class="btn btn-purple" onclick="runAnalysis()" style="flex: 1;"> | |
| Run Analysis | |
| </button> | |
| <button class="btn" onclick="testDb()" style="flex: 1; background: #475569;"> | |
| Test DB | |
| </button> | |
| <button class="btn" onclick="testOllama()" style="flex: 1; background: #ea580c;"> | |
| Test LLM | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Recent Signals --> | |
| <div class="card"> | |
| <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;"> | |
| <h2>📡 Recent Signals</h2> | |
| <button class="btn" style="width:auto; padding: 0.5rem 1rem; font-size: 0.8rem; background: #334155;" onclick="loadSignals()">Refresh</button> | |
| </div> | |
| <table class="signals-table"> | |
| <thead> | |
| <tr> | |
| <th>Ticker</th> | |
| <th>Date</th> | |
| <th>Signal</th> | |
| <th>Sentiment</th> | |
| <th>Time (UTC)</th> | |
| </tr> | |
| </thead> | |
| <tbody id="signalsBody"> | |
| <tr><td colspan="5" class="empty-state">Loading signals...</td></tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <script> | |
| // Try to get secret from storage, but don't block UI if missing | |
| let API_SECRET = localStorage.getItem('api_secret'); | |
| // Initialize immediately | |
| init(); | |
| async function init() { | |
| checkHealth(); | |
| loadSignals(); | |
| } | |
| function getSecretOrPrompt() { | |
| if (!API_SECRET) { | |
| const input = prompt("🔐 Enter API Secret to perform this action:"); | |
| if (input) { | |
| API_SECRET = input; | |
| localStorage.setItem('api_secret', input); | |
| } else { | |
| return null; | |
| } | |
| } | |
| return API_SECRET; | |
| } | |
| async function checkHealth() { | |
| try { | |
| const res = await fetch('/health'); | |
| const data = await res.json(); | |
| const dbEl = document.getElementById('dbStatus'); | |
| const ollamaEl = document.getElementById('ollamaStatus'); | |
| dbEl.innerText = data.database === 'connected' ? 'Connected' : 'Disconnected'; | |
| dbEl.style.color = data.database === 'connected' ? '#10B981' : '#EF4444'; | |
| ollamaEl.innerText = data.ollama === 'running' ? 'Active' : 'Offline'; | |
| ollamaEl.style.color = data.ollama === 'running' ? '#10B981' : '#EF4444'; | |
| } catch (e) { | |
| console.error(e); | |
| } | |
| } | |
| async function loadSignals() { | |
| const tbody = document.getElementById('signalsBody'); | |
| try { | |
| // No secret needed for viewing now | |
| const res = await fetch('/api/signals'); | |
| if (!res.ok) throw new Error('Failed to fetch'); | |
| const signals = await res.json(); | |
| if (signals.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No signals generated yet.</td></tr>'; | |
| return; | |
| } | |
| tbody.innerHTML = signals.map(s => { | |
| const pos = s.signal_position || 'HOLD'; | |
| const cls = pos === 'BUY' ? 'tag-buy' : (pos === 'SELL' ? 'tag-sell' : 'tag-hold'); | |
| const sentiment = s.sentiment ? (s.sentiment.confidence ? `${(s.sentiment.confidence * 100).toFixed(0)}%` : '-') : '-'; | |
| // Format date safely | |
| let dateStr = s.created_at; | |
| try { | |
| dateStr = new Date(s.created_at).toLocaleString(); | |
| } catch(e) {} | |
| return ` | |
| <tr> | |
| <td style="font-weight:600; color:white;">${s.ticker}</td> | |
| <td>${s.signal_date}</td> | |
| <td><span class="tag ${cls}">${pos}</span></td> | |
| <td>${sentiment}</td> | |
| <td style="color:var(--text-secondary); font-size: 0.85rem;">${dateStr}</td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } catch (e) { | |
| tbody.innerHTML = '<tr><td colspan="5" class="empty-state" style="color:#EF4444">Failed to load signals.</td></tr>'; | |
| } | |
| } | |
| async function generateSignal() { | |
| const secret = getSecretOrPrompt(); | |
| if (!secret) return; | |
| const ticker = document.getElementById('tickerInput').value.toUpperCase(); | |
| const btn = document.getElementById('genBtn'); | |
| if (!ticker) return alert('Please enter a ticker'); | |
| btn.disabled = true; | |
| btn.innerText = 'Generating...'; | |
| try { | |
| const res = await fetch('/generate-signal', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-API-Secret': secret | |
| }, | |
| body: JSON.stringify({ ticker: ticker }) | |
| }); | |
| if (res.ok) { | |
| alert(`Signal generation started for ${ticker}. Check back in a few seconds.`); | |
| setTimeout(loadSignals, 2000); | |
| } else { | |
| const err = await res.json(); | |
| if (res.status === 403) { | |
| alert("Invalid API Secret. Please try again."); | |
| localStorage.removeItem('api_secret'); | |
| API_SECRET = null; | |
| } else { | |
| alert('Error: ' + (err.detail || 'Request failed')); | |
| } | |
| } | |
| } catch (e) { | |
| alert('Request failed'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerText = 'Generate AI Signal'; | |
| } | |
| } | |
| async function runAnalysis() { | |
| const secret = getSecretOrPrompt(); | |
| if (!secret) return; | |
| if (!confirm('Run Saturday Analysis? This is a heavy background task.')) return; | |
| try { | |
| const res = await fetch('/saturday-analysis', { | |
| method: 'POST', | |
| headers: { 'X-API-Secret': secret } | |
| }); | |
| if (res.ok) { | |
| alert('Analysis started in background.'); | |
| } else { | |
| if (res.status === 403) { | |
| alert("Invalid API Secret."); | |
| localStorage.removeItem('api_secret'); | |
| API_SECRET = null; | |
| } else { | |
| alert('Failed to start analysis'); | |
| } | |
| } | |
| } catch (e) { | |
| alert('Failed to start analysis'); | |
| } | |
| } | |
| async function testDb() { | |
| const secret = getSecretOrPrompt(); | |
| if (!secret) return; | |
| const btn = event.target; | |
| const ogText = btn.innerText; | |
| btn.innerText = 'Testing...'; | |
| btn.disabled = true; | |
| try { | |
| const res = await fetch('/test-db', { | |
| method: 'POST', | |
| headers: { 'X-API-Secret': secret } | |
| }); | |
| const data = await res.json(); | |
| if (res.status === 403) { | |
| alert("Invalid API Secret."); | |
| localStorage.removeItem('api_secret'); | |
| API_SECRET = null; | |
| return; | |
| } | |
| let msg = `Status: ${data.status.toUpperCase()}\n\n`; | |
| if (data.details) msg += data.details.join('\n'); | |
| if (data.config) msg += `\n\nHost: ${data.config.host}\nSSL Set: ${data.config.ssl_ca_set}`; | |
| alert(msg); | |
| // Refresh health check UI | |
| checkHealth(); | |
| } catch (e) { | |
| alert('Test request failed: ' + e); | |
| } finally { | |
| btn.innerText = ogText; | |
| btn.disabled = false; | |
| } | |
| } | |
| async function testOllama() { | |
| const secret = getSecretOrPrompt(); | |
| if (!secret) return; | |
| const btn = event.target; | |
| const ogText = btn.innerText; | |
| btn.innerText = 'Testing...'; | |
| btn.disabled = true; | |
| try { | |
| const res = await fetch('/test-ollama', { | |
| method: 'POST', | |
| headers: { 'X-API-Secret': secret } | |
| }); | |
| const data = await res.json(); | |
| if (res.status === 403) { | |
| alert("Invalid API Secret."); | |
| localStorage.removeItem('api_secret'); | |
| API_SECRET = null; | |
| return; | |
| } | |
| let msg = `Status: ${data.status.toUpperCase()}\n\n`; | |
| if (data.details) msg += data.details.join('\n'); | |
| alert(msg); | |
| checkHealth(); | |
| } catch (e) { | |
| alert('Test request failed: ' + e); | |
| } finally { | |
| btn.innerText = ogText; | |
| btn.disabled = false; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |