Papaflessas's picture
Deploy Signal Generator app
4856b42
<!DOCTYPE html>
<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>