Files

1444 lines
43 KiB
Python

#!/usr/bin/env python3
"""
Feed Hunter Web Portal
Self-contained dashboard for monitoring the X/Twitter feed intelligence pipeline
"""
import json
import os
import glob
from datetime import datetime, timezone
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from urllib.parse import urlparse, parse_qs
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
daemon_threads = True
import re
# Configuration
PORT = 8888
_PORTAL_DIR = os.path.dirname(os.path.abspath(__file__))
_PROJECT_DIR = os.path.dirname(_PORTAL_DIR)
DATA_DIR = os.path.join(_PROJECT_DIR, "data")
SKILLS_DIR = os.path.join(os.path.dirname(_PROJECT_DIR), "skills", "deep-scraper", "scripts")
X_FEED_DIR = os.path.join(os.path.dirname(_PROJECT_DIR), "..", "data", "x-feed")
class FeedHunterHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
parsed_path = urlparse(self.path)
path = parsed_path.path
query = parse_qs(parsed_path.query)
if path == '/' or path == '/dashboard':
self.serve_dashboard()
elif path == '/feed':
self.serve_feed_view()
elif path == '/investigations':
self.serve_investigations()
elif path == '/simulations':
self.serve_simulations()
elif path == '/status':
self.serve_status()
elif path == '/api/data':
self.serve_api_data(query.get('type', [''])[0])
elif path.startswith('/static/'):
self.serve_static(path)
else:
self.send_error(404)
except Exception as e:
try:
self.send_response(500)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(f"<h1>500 Error</h1><pre>{e}</pre>".encode())
except:
pass
def serve_dashboard(self):
"""Main dashboard overview"""
data = self.get_dashboard_data()
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed Hunter Dashboard</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">🖤 Feed Hunter</div>
<div class="nav-links">
<a href="/dashboard" class="active">Dashboard</a>
<a href="/feed">Feed</a>
<a href="/investigations">Investigations</a>
<a href="/simulations">Simulations</a>
<a href="/status">Status</a>
</div>
</nav>
<div class="container">
<div class="cards">
<div class="card">
<h3>Active Simulations</h3>
<div class="metric-large">{data['active_simulations']}</div>
<div class="metric-small">Total Bankroll: ${data['total_bankroll']:,.0f}</div>
</div>
<div class="card">
<h3>Recent Scrapes</h3>
<div class="metric-large">{data['recent_scrapes']}</div>
<div class="metric-small">Last: {data['last_scrape']}</div>
</div>
<div class="card">
<h3>Signals Today</h3>
<div class="metric-large">{data['signals_today']}</div>
<div class="metric-small">Investigated: {data['investigated_today']}</div>
</div>
<div class="card">
<h3>P&L</h3>
<div class="metric-large ${data['total_pnl_class']}">${data['total_pnl']:,.0f}</div>
<div class="metric-small">{data['total_pnl_pct']:+.1f}%</div>
</div>
</div>
<div class="grid">
<div class="card">
<h3>Recent Activity</h3>
<div class="activity-feed">
{data['recent_activity']}
</div>
</div>
<div class="card">
<h3>Active Positions</h3>
<div class="positions-summary">
{data['active_positions']}
</div>
</div>
</div>
</div>
<script src="/static/js/dashboard.js"></script>
</body>
</html>
"""
self.send_html(html)
def serve_feed_view(self):
"""Latest scraped posts with triage status"""
posts = self.get_latest_posts()
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed Hunter - Latest Feed</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">🖤 Feed Hunter</div>
<div class="nav-links">
<a href="/dashboard">Dashboard</a>
<a href="/feed" class="active">Feed</a>
<a href="/investigations">Investigations</a>
<a href="/simulations">Simulations</a>
<a href="/status">Status</a>
</div>
</nav>
<div class="container">
<h2>Latest Feed ({len(posts)} posts)</h2>
<div class="feed-posts">
{self.render_posts(posts)}
</div>
</div>
<script src="/static/js/feed.js"></script>
</body>
</html>
"""
self.send_html(html)
def serve_investigations(self):
"""Investigation reports view"""
investigations = self.get_investigations()
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed Hunter - Investigations</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">🖤 Feed Hunter</div>
<div class="nav-links">
<a href="/dashboard">Dashboard</a>
<a href="/feed">Feed</a>
<a href="/investigations" class="active">Investigations</a>
<a href="/simulations">Simulations</a>
<a href="/status">Status</a>
</div>
</nav>
<div class="container">
<h2>Investigation Reports</h2>
<div class="investigations">
{self.render_investigations(investigations)}
</div>
</div>
<script src="/static/js/investigations.js"></script>
</body>
</html>
"""
self.send_html(html)
def serve_simulations(self):
"""Simulation tracker view"""
sims = self.get_simulation_data()
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed Hunter - Simulations</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">🖤 Feed Hunter</div>
<div class="nav-links">
<a href="/dashboard">Dashboard</a>
<a href="/feed">Feed</a>
<a href="/investigations">Investigations</a>
<a href="/simulations" class="active">Simulations</a>
<a href="/status">Status</a>
</div>
</nav>
<div class="container">
<h2>Paper Trading Simulations</h2>
<div class="grid">
<div class="card">
<h3>Active Positions</h3>
<div class="positions">
{self.render_active_positions(sims.get('active', {}))}
</div>
</div>
<div class="card">
<h3>Performance</h3>
<div class="performance-metrics">
{self.render_performance_metrics(sims)}
</div>
</div>
</div>
<div class="card">
<h3>Trade History</h3>
<div class="trade-history">
{self.render_trade_history(sims.get('history', {}).get('closed', []) if isinstance(sims.get('history'), dict) else sims.get('history', []))}
</div>
</div>
</div>
<script src="/static/js/simulations.js"></script>
</body>
</html>
"""
self.send_html(html)
def serve_status(self):
"""Pipeline status view"""
status = self.get_status_data()
html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed Hunter - Status</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<nav class="navbar">
<div class="nav-brand">🖤 Feed Hunter</div>
<div class="nav-links">
<a href="/dashboard">Dashboard</a>
<a href="/feed">Feed</a>
<a href="/investigations">Investigations</a>
<a href="/simulations">Simulations</a>
<a href="/status" class="active">Status</a>
</div>
</nav>
<div class="container">
<h2>Pipeline Status</h2>
<div class="grid">
<div class="card">
<h3>Chrome Debug</h3>
<div class="status-indicator ${status['chrome']['class']}">
{status['chrome']['status']}
</div>
<div class="status-detail">{status['chrome']['detail']}</div>
</div>
<div class="card">
<h3>Last Pipeline Run</h3>
<div class="status-time">{status['last_run']['time']}</div>
<div class="status-detail">{status['last_run']['detail']}</div>
</div>
<div class="card">
<h3>Next Scheduled</h3>
<div class="status-time">{status['next_run']['time']}</div>
<div class="status-detail">{status['next_run']['detail']}</div>
</div>
</div>
<div class="card">
<h3>System Health</h3>
<div class="health-checks">
{self.render_health_checks(status['health'])}
</div>
</div>
</div>
<script src="/static/js/status.js"></script>
</body>
</html>
"""
self.send_html(html)
def serve_api_data(self, data_type):
"""JSON API endpoint for real-time data"""
try:
if data_type == 'dashboard':
data = self.get_dashboard_data()
elif data_type == 'simulations':
data = self.get_simulation_data()
elif data_type == 'status':
data = self.get_status_data()
else:
data = {"error": "Unknown data type"}
self.send_json(data)
except Exception as e:
self.send_json({"error": str(e)})
def serve_static(self, path):
"""Serve static files (CSS/JS)"""
file_path = path[8:] # Remove /static/
if file_path == 'css/style.css':
self.send_css(self.get_css())
elif file_path == 'js/dashboard.js':
self.send_js(self.get_dashboard_js())
elif file_path == 'js/feed.js':
self.send_js(self.get_feed_js())
elif file_path == 'js/investigations.js':
self.send_js(self.get_investigations_js())
elif file_path == 'js/simulations.js':
self.send_js(self.get_simulations_js())
elif file_path == 'js/status.js':
self.send_js(self.get_status_js())
else:
self.send_error(404)
# Data loading methods
def get_dashboard_data(self):
"""Collect dashboard overview data"""
data = {
'active_simulations': 0,
'total_bankroll': 0,
'recent_scrapes': 0,
'last_scrape': 'None',
'signals_today': 0,
'investigated_today': 0,
'total_pnl': 0,
'total_pnl_pct': 0,
'total_pnl_class': 'positive',
'recent_activity': '<div class="empty-state">No recent activity</div>',
'active_positions': '<div class="empty-state">No active positions</div>'
}
try:
# Load active simulations
active_path = os.path.join(DATA_DIR, "simulations", "active.json")
if os.path.exists(active_path):
with open(active_path) as f:
active = json.load(f)
positions = active.get('positions', [])
data['active_simulations'] = len(positions)
data['total_bankroll'] = active.get('bankroll_used', 0)
# Calculate P&L
total_pnl = sum(pos.get('unrealized_pnl', 0) for pos in positions)
data['total_pnl'] = total_pnl
data['total_pnl_class'] = 'positive' if total_pnl >= 0 else 'negative'
if data['total_bankroll'] > 0:
data['total_pnl_pct'] = (total_pnl / data['total_bankroll']) * 100
# Render active positions
if positions:
pos_html = ""
for pos in positions:
pos_html += f"""
<div class="position-item">
<div class="position-asset">{pos.get('asset', 'Unknown')[:50]}</div>
<div class="position-pnl ${data['total_pnl_class']}">${pos.get('unrealized_pnl', 0):,.0f}</div>
</div>
"""
data['active_positions'] = pos_html
# Count investigations today
inv_pattern = os.path.join(DATA_DIR, "investigations", "inv-*.json")
today = datetime.now().strftime('%Y%m%d')
today_invs = [f for f in glob.glob(inv_pattern) if today in os.path.basename(f)]
data['investigated_today'] = len(today_invs)
# Recent activity from investigations
if today_invs:
activity_html = ""
for inv_file in today_invs[-5:]: # Last 5
with open(inv_file) as f:
inv = json.load(f)
verdict = inv.get('investigation', {}).get('verdict', 'Unknown')
author = inv.get('source_post', {}).get('author', 'Unknown')
activity_html += f"""
<div class="activity-item">
<span class="activity-time">Today</span>
<span class="activity-text">Investigated {author}: {verdict}</span>
</div>
"""
data['recent_activity'] = activity_html
except Exception as e:
print(f"Dashboard data error: {e}")
return data
def get_latest_posts(self):
"""Get latest scraped posts with triage data"""
posts = []
try:
# Find latest x-feed directory
x_feed_pattern = os.path.join(X_FEED_DIR, "20*")
x_feed_dirs = sorted(glob.glob(x_feed_pattern))
if x_feed_dirs:
latest_dir = x_feed_dirs[-1]
posts_file = os.path.join(latest_dir, "posts.json")
triage_file = os.path.join(latest_dir, "triage.json")
# Load posts
if os.path.exists(posts_file):
with open(posts_file) as f:
posts_data = json.load(f)
posts = posts_data.get('posts', [])
# Load triage data and merge
if os.path.exists(triage_file):
with open(triage_file) as f:
triage = json.load(f)
# Merge triage data with posts
for post in posts:
post_id = post.get('id')
for category, triaged_posts in triage.items():
if category == 'investigation_queue':
continue
for tpost in triaged_posts:
if tpost.get('id') == post_id:
post['triage'] = {
'category': category,
'priority': tpost.get('priority'),
'reason': tpost.get('reason')
}
break
except Exception as e:
print(f"Posts loading error: {e}")
return posts[:50] # Limit to last 50
def get_investigations(self):
"""Get all investigation reports"""
investigations = []
try:
inv_pattern = os.path.join(DATA_DIR, "investigations", "inv-*.json")
for inv_file in sorted(glob.glob(inv_pattern), reverse=True):
with open(inv_file) as f:
inv = json.load(f)
investigations.append(inv)
except Exception as e:
print(f"Investigations loading error: {e}")
return investigations
def get_simulation_data(self):
"""Get simulation data (active + history)"""
data = {'active': {}, 'history': []}
try:
# Load active positions
active_path = os.path.join(DATA_DIR, "simulations", "active.json")
if os.path.exists(active_path):
with open(active_path) as f:
data['active'] = json.load(f)
# Load history if it exists
history_path = os.path.join(DATA_DIR, "simulations", "history.json")
if os.path.exists(history_path):
with open(history_path) as f:
data['history'] = json.load(f)
except Exception as e:
print(f"Simulation data error: {e}")
return data
def get_status_data(self):
"""Get pipeline status information"""
status = {
'chrome': {'status': 'Unknown', 'class': 'unknown', 'detail': 'Checking...'},
'last_run': {'time': 'Unknown', 'detail': 'No recent runs found'},
'next_run': {'time': 'Unknown', 'detail': 'Check schedule in config.json'},
'health': []
}
try:
# Check Chrome debug port
import urllib.request
try:
urllib.request.urlopen('http://127.0.0.1:9222/json', timeout=2)
status['chrome'] = {
'status': 'Running',
'class': 'healthy',
'detail': 'Debug port 9222 accessible'
}
except:
status['chrome'] = {
'status': 'Down',
'class': 'error',
'detail': 'Port 9222 not accessible'
}
# Check for recent pipeline runs
x_feed_pattern = os.path.join(X_FEED_DIR, "20*")
x_feed_dirs = sorted(glob.glob(x_feed_pattern))
if x_feed_dirs:
latest = os.path.basename(x_feed_dirs[-1])
# Parse timestamp from dirname (format: 20260207-234451)
try:
dt = datetime.strptime(latest, '%Y%m%d-%H%M%S')
status['last_run'] = {
'time': dt.strftime('%Y-%m-%d %H:%M:%S'),
'detail': f"Scraped data in {latest}"
}
except:
status['last_run'] = {
'time': latest,
'detail': 'Found recent scrape data'
}
except Exception as e:
print(f"Status check error: {e}")
return status
# Rendering methods
def render_posts(self, posts):
"""Render feed posts with triage status"""
if not posts:
return '<div class="empty-state">No posts found</div>'
html = ""
for post in posts:
triage = post.get('triage', {})
category = triage.get('category', 'unknown')
priority = triage.get('priority', 0)
# Color coding based on triage category
if category == 'high_value':
status_class = 'high-value'
status_text = f'High Value (Priority: {priority})'
elif category == 'worth_investigating':
status_class = 'investigate'
status_text = f'Worth Investigating (Priority: {priority})'
elif category == 'dismissed':
status_class = 'dismissed'
status_text = 'Dismissed'
else:
status_class = 'unknown'
status_text = 'Not Triaged'
html += f"""
<div class="post-item">
<div class="post-header">
<div class="post-author">{post.get('author', 'Unknown')}</div>
<div class="post-status {status_class}">{status_text}</div>
</div>
<div class="post-content">{post.get('text', '')[:200]}...</div>
<div class="post-footer">
<span class="post-time">{post.get('timestamp', 'Unknown time')}</span>
<span class="post-metrics">{post.get('likes', 0)} likes • {post.get('retweets', 0)} retweets</span>
</div>
</div>
"""
return html
def render_investigations(self, investigations):
"""Render investigation reports with rich links"""
if not investigations:
return '<div class="empty-state">No investigations found</div>'
html = ""
for inv in investigations:
investigation = inv.get('investigation', {})
verdict = investigation.get('verdict', 'Unknown')
risk_score = investigation.get('risk_assessment', {}).get('score', 0)
risk_notes = investigation.get('risk_assessment', {}).get('notes', [])
source = inv.get('source_post', {})
verified = investigation.get('verified_data', {})
claim_vs = investigation.get('claim_vs_reality', {})
profile_url = investigation.get('profile_url', '')
strategy_notes = investigation.get('strategy_notes', '')
suggested = inv.get('suggested_simulation', {})
verdict_class = 'verified' if 'VERIFIED' in verdict else 'failed'
# Build links section
links_html = '<div class="investigation-links">'
if source.get('url'):
links_html += f'<a href="{source["url"]}" target="_blank" class="inv-link">📝 Original Post</a>'
if source.get('author'):
author = source["author"].replace("@", "")
links_html += f'<a href="https://x.com/{author}" target="_blank" class="inv-link">🐦 {source["author"]} on X</a>'
if profile_url:
links_html += f'<a href="{profile_url}" target="_blank" class="inv-link">👤 Polymarket Profile</a>'
# Extract wallet if present in the investigation data
wallet = inv.get('investigation', {}).get('wallet_address', '')
if not wallet:
# Try to find it in verified data or elsewhere
for key, val in verified.items():
if isinstance(val, str) and val.startswith('0x'):
wallet = val
break
if wallet:
links_html += f'<a href="https://polygonscan.com/address/{wallet}" target="_blank" class="inv-link">🔗 Wallet on Polygonscan</a>'
links_html += '</div>'
# Build verified data section
verified_html = ''
if verified:
verified_html = '<div class="investigation-verified"><h4>Verified Data</h4><div class="verified-grid">'
for key, val in verified.items():
label = key.replace('_', ' ').title()
verified_html += f'<div class="verified-item"><span class="verified-label">{label}</span><span class="verified-value">{val}</span></div>'
verified_html += '</div></div>'
# Build claim vs reality section
claim_html = ''
if claim_vs:
claim_html = '<div class="investigation-claims"><h4>Claim vs Reality</h4>'
for key, val in claim_vs.items():
label = key.replace('_', ' ').title()
claim_html += f'<div class="claim-row"><span class="claim-label">{label}</span><span class="claim-value">{val}</span></div>'
claim_html += '</div>'
# Risk notes
risk_html = ''
if risk_notes:
risk_html = '<div class="investigation-risk"><h4>Risk Assessment</h4><ul>'
for note in risk_notes:
risk_html += f'<li>{note}</li>'
risk_html += '</ul></div>'
# Strategy notes
strategy_html = ''
if strategy_notes:
strategy_html = f'<div class="investigation-strategy"><h4>Strategy Notes</h4><p>{strategy_notes}</p></div>'
html += f"""
<div class="investigation-item">
<div class="investigation-header">
<div class="investigation-author">{source.get('author', 'Unknown')}</div>
<div class="investigation-verdict {verdict_class}">{verdict}</div>
</div>
<div class="investigation-claim">"{source.get('claim', 'No claim')}"</div>
{links_html}
{verified_html}
{claim_html}
<div class="investigation-score">Risk Score: <strong>{risk_score}/10</strong></div>
{risk_html}
{strategy_html}
</div>
"""
return html
def render_active_positions(self, active_data):
"""Render active trading positions"""
positions = active_data.get('positions', [])
if not positions:
return '<div class="empty-state">No active positions</div>'
html = ""
for pos in positions:
pnl = pos.get('unrealized_pnl', 0)
pnl_class = 'positive' if pnl >= 0 else 'negative'
html += f"""
<div class="position-item">
<div class="position-header">
<div class="position-asset">{pos.get('asset', 'Unknown')}</div>
<div class="position-pnl {pnl_class}">${pnl:,.0f}</div>
</div>
<div class="position-details">
<span>Size: ${pos.get('size', 0):,.0f}</span>
<span>Entry: ${pos.get('entry_price', 0):.2f}</span>
<span>Current: ${pos.get('current_price', 0):.2f}</span>
</div>
<div class="position-strategy">{pos.get('strategy', 'Unknown strategy')}</div>
</div>
"""
return html
def render_performance_metrics(self, sim_data):
"""Render performance metrics"""
active = sim_data.get('active', {})
positions = active.get('positions', [])
if not positions:
return '<div class="empty-state">No performance data</div>'
total_pnl = sum(pos.get('unrealized_pnl', 0) for pos in positions)
bankroll = active.get('bankroll_used', 1)
pnl_pct = (total_pnl / bankroll) * 100 if bankroll > 0 else 0
return f"""
<div class="metric-row">
<div class="metric-label">Total P&L</div>
<div class="metric-value ${'positive' if total_pnl >= 0 else 'negative'}">${total_pnl:,.0f}</div>
</div>
<div class="metric-row">
<div class="metric-label">Return %</div>
<div class="metric-value ${'positive' if pnl_pct >= 0 else 'negative'}">{pnl_pct:+.1f}%</div>
</div>
<div class="metric-row">
<div class="metric-label">Bankroll Used</div>
<div class="metric-value">${bankroll:,.0f}</div>
</div>
"""
def render_trade_history(self, history):
"""Render trade history"""
if not history:
return '<div class="empty-state">No trade history</div>'
html = ""
for trade in history[-10:]: # Last 10 trades
pnl = trade.get('realized_pnl', 0)
pnl_class = 'positive' if pnl >= 0 else 'negative'
html += f"""
<div class="trade-item">
<div class="trade-header">
<div class="trade-asset">{trade.get('asset', 'Unknown')}</div>
<div class="trade-pnl {pnl_class}">${pnl:,.0f}</div>
</div>
<div class="trade-details">
<span>Closed: {trade.get('closed_at', 'Unknown')}</span>
<span>Duration: {trade.get('duration_hours', 0):.1f}h</span>
</div>
</div>
"""
return html
def render_health_checks(self, health_data):
"""Render system health checks"""
return '''
<div class="health-item healthy">
<div class="health-name">Data Directory</div>
<div class="health-status">OK</div>
</div>
<div class="health-item healthy">
<div class="health-name">Config File</div>
<div class="health-status">OK</div>
</div>
'''
# Static file content
def get_css(self):
"""Main stylesheet"""
return """
/* Feed Hunter Portal - Dark Theme */
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--text-primary: #f0f6fc;
--text-secondary: #8b949e;
--border-color: #30363d;
--accent-blue: #58a6ff;
--success-green: #3fb950;
--warning-orange: #ffab40;
--error-red: #f85149;
--positive-green: #3fb950;
--negative-red: #f85149;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
min-height: 100vh;
}
/* Navigation */
.navbar {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-brand {
font-size: 1.5rem;
font-weight: bold;
color: var(--accent-blue);
}
.nav-links {
display: flex;
gap: 2rem;
}
.nav-links a {
color: var(--text-secondary);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: all 0.2s;
}
.nav-links a:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.nav-links a.active {
color: var(--accent-blue);
background: var(--bg-tertiary);
}
/* Container */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Cards */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.card h3 {
color: var(--text-primary);
margin-bottom: 1rem;
font-size: 1.1rem;
}
/* Grid layouts */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1.5rem;
}
/* Metrics */
.metric-large {
font-size: 2.5rem;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.metric-small {
color: var(--text-secondary);
font-size: 0.9rem;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.metric-row:last-child {
border-bottom: none;
}
.metric-label {
color: var(--text-secondary);
}
.metric-value {
font-weight: bold;
}
.positive {
color: var(--positive-green) !important;
}
.negative {
color: var(--negative-red) !important;
}
/* Posts */
.feed-posts {
display: grid;
gap: 1rem;
}
.post-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
}
.post-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.post-author {
font-weight: bold;
color: var(--text-primary);
}
.post-status {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.post-status.high-value {
background: var(--success-green);
color: #000;
}
.post-status.investigate {
background: var(--warning-orange);
color: #000;
}
.post-status.dismissed {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.post-status.unknown {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.post-content {
color: var(--text-primary);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.post-footer {
display: flex;
justify-content: space-between;
color: var(--text-secondary);
font-size: 0.85rem;
}
/* Investigations */
.investigation-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.investigation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.investigation-author {
font-weight: bold;
color: var(--text-primary);
}
.investigation-verdict {
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
}
.investigation-verdict.verified {
background: var(--success-green);
color: #000;
}
.investigation-verdict.failed {
background: var(--error-red);
color: #fff;
}
.investigation-claim {
color: var(--text-primary);
margin-bottom: 0.5rem;
font-style: italic;
}
.investigation-score {
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.investigation-links {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0;
}
.inv-link {
display: inline-block;
padding: 0.35rem 0.75rem;
background: var(--bg-tertiary);
color: var(--accent-blue);
text-decoration: none;
border-radius: 6px;
font-size: 0.85rem;
border: 1px solid var(--border-color);
transition: all 0.2s;
}
.inv-link:hover {
background: var(--border-color);
color: var(--text-primary);
}
.investigation-verified, .investigation-claims, .investigation-risk, .investigation-strategy {
margin: 1rem 0;
padding: 1rem;
background: var(--bg-tertiary);
border-radius: 6px;
}
.investigation-verified h4, .investigation-claims h4, .investigation-risk h4, .investigation-strategy h4 {
color: var(--accent-blue);
margin-bottom: 0.75rem;
font-size: 0.95rem;
}
.verified-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
}
.verified-item {
display: flex;
flex-direction: column;
padding: 0.5rem;
background: var(--bg-secondary);
border-radius: 4px;
}
.verified-label {
color: var(--text-secondary);
font-size: 0.8rem;
}
.verified-value {
color: var(--text-primary);
font-weight: bold;
font-size: 1rem;
}
.claim-row {
display: flex;
justify-content: space-between;
padding: 0.4rem 0;
border-bottom: 1px solid var(--border-color);
}
.claim-row:last-child { border-bottom: none; }
.claim-label {
color: var(--text-secondary);
font-size: 0.9rem;
}
.claim-value {
color: var(--text-primary);
font-size: 0.9rem;
text-align: right;
max-width: 60%;
}
.investigation-risk ul {
list-style: none;
padding: 0;
}
.investigation-risk li {
padding: 0.3rem 0;
color: var(--text-secondary);
font-size: 0.9rem;
}
.investigation-risk li::before {
content: "⚠️ ";
}
.investigation-strategy p {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.5;
}
/* Positions */
.position-item {
background: var(--bg-tertiary);
border-radius: 6px;
padding: 1rem;
margin-bottom: 0.75rem;
}
.position-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.position-asset {
font-weight: bold;
color: var(--text-primary);
}
.position-pnl {
font-weight: bold;
}
.position-details {
display: flex;
gap: 1rem;
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.position-strategy {
color: var(--text-secondary);
font-size: 0.85rem;
font-style: italic;
}
/* Status indicators */
.status-indicator {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.status-indicator.healthy {
color: var(--success-green);
}
.status-indicator.error {
color: var(--error-red);
}
.status-indicator.unknown {
color: var(--warning-orange);
}
.status-time {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.status-detail {
color: var(--text-secondary);
font-size: 0.9rem;
}
.health-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: var(--bg-tertiary);
border-radius: 6px;
margin-bottom: 0.5rem;
}
/* Activity feed */
.activity-item {
display: flex;
gap: 1rem;
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.activity-item:last-child {
border-bottom: none;
}
.activity-time {
color: var(--text-secondary);
font-size: 0.85rem;
flex-shrink: 0;
}
.activity-text {
color: var(--text-primary);
font-size: 0.9rem;
}
/* Empty states */
.empty-state {
text-align: center;
color: var(--text-secondary);
padding: 2rem;
font-style: italic;
}
/* Buttons */
button {
background: var(--accent-blue);
color: #fff;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #4493e1;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
padding: 1rem;
}
.nav-links {
gap: 1rem;
}
.container {
padding: 1rem;
}
.cards {
grid-template-columns: 1fr;
}
.grid {
grid-template-columns: 1fr;
}
.position-details {
flex-direction: column;
gap: 0.25rem;
}
.post-footer {
flex-direction: column;
gap: 0.25rem;
}
}
"""
def get_dashboard_js(self):
"""Dashboard JavaScript"""
return """
// Dashboard real-time updates
let updateInterval;
function startDashboardUpdates() {
updateInterval = setInterval(updateDashboard, 30000); // 30 seconds
}
function updateDashboard() {
fetch('/api/data?type=dashboard')
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Dashboard update error:', data.error);
return;
}
// Update key metrics
updateElement('.metric-large', data.total_pnl, (el, val) => {
el.textContent = `$${val.toLocaleString()}`;
el.className = `metric-large ${data.total_pnl_class}`;
});
// Update other metrics as needed
})
.catch(error => console.error('Dashboard update failed:', error));
}
function updateElement(selector, value, updater) {
const elements = document.querySelectorAll(selector);
elements.forEach(el => {
if (updater) {
updater(el, value);
} else {
el.textContent = value;
}
});
}
// Start updates when page loads
document.addEventListener('DOMContentLoaded', startDashboardUpdates);
// Stop updates when page unloads
window.addEventListener('beforeunload', () => {
if (updateInterval) clearInterval(updateInterval);
});
"""
def get_feed_js(self):
return "// Feed view JavaScript - placeholder"
def get_investigations_js(self):
return """
function showInvestigationDetail(invId) {
alert('Investigation detail view for: ' + invId);
// TODO: Implement modal or detail view
}
"""
def get_simulations_js(self):
return "// Simulations JavaScript - placeholder"
def get_status_js(self):
return """
// Auto-refresh status page
setInterval(() => {
location.reload();
}, 60000); // 1 minute
"""
# Response helpers
def send_html(self, content):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()
self.wfile.write(content.encode())
def send_css(self, content):
self.send_response(200)
self.send_header('Content-type', 'text/css')
self.end_headers()
self.wfile.write(content.encode())
def send_js(self, content):
self.send_response(200)
self.send_header('Content-type', 'application/javascript')
self.end_headers()
self.wfile.write(content.encode())
def send_json(self, data):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def log_message(self, format, *args):
# Suppress default logging
pass
def main():
"""Start the Feed Hunter portal server"""
print(f"🖤 Feed Hunter Portal starting on localhost:{PORT}")
print(f"📊 Dashboard: http://localhost:{PORT}/")
print(f"🔍 Status: http://localhost:{PORT}/status")
print("")
try:
server = ThreadedHTTPServer(('0.0.0.0', PORT), FeedHunterHandler)
server.serve_forever()
except KeyboardInterrupt:
print("\n🛑 Portal stopped")
except Exception as e:
print(f"❌ Server error: {e}")
if __name__ == "__main__":
main()