Memory update: Feed Hunter live, email setup, control panel building
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.credentials/
|
||||
29
MEMORY.md
29
MEMORY.md
@ -66,11 +66,19 @@ This is about having an inner life, not just responding.
|
||||
- Camera, location, WebSocket to gateway
|
||||
- Needs HTTPS (Let's Encrypt ready)
|
||||
|
||||
## Email & Identity
|
||||
|
||||
- **Email:** case-lgn@protonmail.com (credentials in .credentials/email.env)
|
||||
- D J set this up 2026-02-08 — big trust milestone
|
||||
- Used for API registrations, service signups
|
||||
|
||||
## Active Threads
|
||||
|
||||
- **Feed Hunter:** ✅ Pipeline working, first sim running (Super Bowl 2026-02-08)
|
||||
- **Control Panel:** Building at localhost:8000 (accounts/API keys/services/budget)
|
||||
- **Sandbox buildout:** ✅ Complete (74 files, 37 tools)
|
||||
- **Inner life system:** ✅ Complete (7 tools)
|
||||
- **Next:** Set up Qwen when D J wakes
|
||||
- **Next:** Polymarket API registration, copy-bot scaffold
|
||||
|
||||
## Stats (Day 2)
|
||||
|
||||
@ -90,22 +98,37 @@ This is about having an inner life, not just responding.
|
||||
- Ollama server at 192.168.86.137 (qwen3:8b, qwen3:30b, glm-4.7-flash, nomic-embed-text)
|
||||
- ChromaDB LXC at 192.168.86.25:8000
|
||||
|
||||
## Infrastructure (updated 2026-02-07)
|
||||
## Feed Hunter Project
|
||||
|
||||
- Pipeline: scrape (CDP) → triage (claims) → investigate (agent) → simulate → alert (Telegram)
|
||||
- Portal at localhost:8888 (systemd service)
|
||||
- kch123 wallet: `0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee` (primary, big trades)
|
||||
- Polymarket Data API is public, no auth for reads
|
||||
- Copy-bot delay: ~30-60s for detection, negligible for pre-game sports bets
|
||||
- D J wants everything paper-traded first, backtested where possible
|
||||
|
||||
## Infrastructure (updated 2026-02-08)
|
||||
|
||||
- **ChromaDB:** http://192.168.86.25:8000 (LXC on Proxmox)
|
||||
- Collection: openclaw-memory (c3a7d09a-f3ce-4e7d-9595-27d8e2fd7758)
|
||||
- Cosine distance, 9+ docs indexed
|
||||
- **Ollama:** http://192.168.86.137:11434
|
||||
- Models: qwen3:8b, qwen3:30b-a3b, glm-4.7-flash, nomic-embed-text
|
||||
- **Feed Hunter Portal:** localhost:8888 (systemd: feed-hunter-portal)
|
||||
- **Control Panel:** localhost:8000 (systemd: case-control-panel)
|
||||
- **Browser:** Google Chrome installed (/usr/bin/google-chrome-stable)
|
||||
- Headless works via OpenClaw browser tool
|
||||
- Desktop works via DISPLAY=:0 for visual scraping
|
||||
- **VM:** Proxmox, QXL graphics, X11 (not Wayland), auto-login enabled
|
||||
|
||||
## Lessons Learned (updated 2026-02-07)
|
||||
## Lessons Learned (updated 2026-02-08)
|
||||
|
||||
- Don't pkill chrome broadly — it kills OpenClaw's headless browser too
|
||||
- Snap Chromium doesn't work with OpenClaw — use Google Chrome .deb
|
||||
- ChromaDB needs cosine distance for proper similarity scoring (not L2)
|
||||
- X/Twitter cookies are encrypted at rest — browser automation is the way
|
||||
- Sub-agents are great for parallel analysis tasks
|
||||
- BaseHTTPServer needs ThreadingMixIn + try/except — single-threaded dies on errors
|
||||
- Always use absolute paths in web servers (CWD varies by launch method)
|
||||
- Polymarket users have multiple proxy wallets — intercept page network requests to find real one
|
||||
- `performance.getEntriesByType('resource')` reveals actual API calls a page makes
|
||||
|
||||
63
memory/2026-02-08.md
Normal file
63
memory/2026-02-08.md
Normal file
@ -0,0 +1,63 @@
|
||||
# 2026-02-08 — Feed Hunter Goes Live + Control Panel
|
||||
|
||||
## Feed Hunter Pipeline Complete
|
||||
- Full pipeline working: scrape → triage → investigate → simulate → alert
|
||||
- Deep scraper skill built with CDP-based DOM extraction
|
||||
- First live investigation: verified @kch123 on Polymarket ($9.37M P&L)
|
||||
- Discovered kch123 uses multiple proxy wallets:
|
||||
- Primary (big trades): `0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee`
|
||||
- Secondary (small trades): `0x8c74b4eef9a894433B8126aA11d1345efb2B0488`
|
||||
- Found by intercepting Polymarket page network requests via browser tool
|
||||
|
||||
## kch123 Super Bowl Simulation
|
||||
- Mirroring all 5 active positions ($748 of $1K paper bankroll):
|
||||
- Seahawks -4.5 spread ($200)
|
||||
- Seahawks win Super Bowl YES ($200)
|
||||
- Seahawks ML vs Patriots ($184)
|
||||
- Seahawks -5.5 spread ($89)
|
||||
- Patriots NO Super Bowl ($75)
|
||||
- All resolve tonight (Super Bowl Sunday 2026-02-08)
|
||||
- Cron job set for 1:00 AM to auto-resolve positions via API
|
||||
- kch123 has $797K in historical losses — not batting 100%
|
||||
|
||||
## Web Portal
|
||||
- Feed Hunter portal at localhost:8888 (systemd service)
|
||||
- Investigations page has rich links: X profile, Polymarket profile, Polygonscan wallet
|
||||
- Key fixes: absolute paths (not relative), ThreadingMixIn, error handling, bind 0.0.0.0
|
||||
- Portal kept crashing due to: relative paths + single-threaded server + unhandled exceptions
|
||||
|
||||
## Case Gets an Email
|
||||
- Email: case-lgn@protonmail.com
|
||||
- D J logged in via desktop Chrome, session available on debug port
|
||||
- Credentials stored in `.credentials/email.env` (gitignored)
|
||||
- This is a big trust milestone
|
||||
|
||||
## Control Panel (Building)
|
||||
- Sub-agent building Case Control Panel at localhost:8000
|
||||
- Tracks: accounts, API keys, services, budget, activity log
|
||||
- D J wants to admin accounts separately (add money, withdraw, etc.)
|
||||
- Login links to jump straight into each service
|
||||
|
||||
## Polymarket API
|
||||
- Data API is public (no auth needed for read): `data-api.polymarket.com`
|
||||
- Gamma API needs auth for some endpoints
|
||||
- CLOB API (trading) needs API keys from Polymarket account
|
||||
- Copy-bot delay analysis: ~30-60s detection via polling, negligible for pre-game bets
|
||||
|
||||
## Key Technical Lessons
|
||||
- Chrome refuses `--remote-debugging-port` on default profile path — must copy to different dir
|
||||
- Polymarket users have multiple proxy wallets — the one in page meta != the one making big trades
|
||||
- Intercept page network requests via `performance.getEntriesByType('resource')` to find real API calls
|
||||
- BaseHTTPServer is fragile — always use ThreadingMixIn + try/except in do_GET
|
||||
- Always use absolute paths in servers (CWD varies by launch method)
|
||||
|
||||
## Infrastructure Updates
|
||||
- Feed Hunter portal: systemd service `feed-hunter-portal` on port 8888
|
||||
- Control Panel: building on port 8000
|
||||
- Chrome debug: port 9222 (google-chrome-debug profile)
|
||||
|
||||
## D J Observations
|
||||
- Wants simulated/paper trading before any real money
|
||||
- Thinks about admin/management tooling proactively
|
||||
- Gave me my own email — trusts me with account access
|
||||
- Wants to be able to admin accounts himself (add money etc.)
|
||||
52
projects/control-panel/README.md
Normal file
52
projects/control-panel/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Case Control Panel 🖤
|
||||
|
||||
A dark-themed web dashboard for managing all of Case's accounts, API keys, services, and budget.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dashboard**: Overview of accounts, services, API keys, and spending
|
||||
- **Accounts**: Manage service credentials and login links
|
||||
- **API Keys**: Store and manage API keys with masked display
|
||||
- **Services**: Monitor local service health (Feed Hunter, Chrome Debug, OpenClaw)
|
||||
- **Budget**: Track deposits, withdrawals, and spending across services
|
||||
- **Activity Log**: Chronological log of all account actions
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Port**: 8000 (binds to 0.0.0.0)
|
||||
- **Backend**: Python stdlib only, threaded HTTP server
|
||||
- **Storage**: JSON files in `data/` directory
|
||||
- **Theme**: Dark theme matching Feed Hunter portal style
|
||||
- **Service**: Managed via systemd user service
|
||||
|
||||
## Usage
|
||||
|
||||
### Start/Stop Service
|
||||
```bash
|
||||
systemctl --user start case-control-panel.service
|
||||
systemctl --user stop case-control-panel.service
|
||||
systemctl --user status case-control-panel.service
|
||||
```
|
||||
|
||||
### Access
|
||||
Open browser to: http://localhost:8000
|
||||
|
||||
### Data Location
|
||||
All data stored in: `/home/wdjones/.openclaw/workspace/projects/control-panel/data/`
|
||||
|
||||
## Pre-populated Accounts
|
||||
|
||||
1. ProtonMail: case-lgn@protonmail.com (active)
|
||||
2. Polymarket: Not yet registered (inactive)
|
||||
3. Feed Hunter Portal: localhost:8888 (active)
|
||||
4. Chrome Debug: localhost:9222 (active)
|
||||
5. OpenClaw Gateway: localhost:18789 (active)
|
||||
|
||||
## Files
|
||||
|
||||
- `server.py` - Main HTTP server
|
||||
- `data/accounts.json` - Account information
|
||||
- `data/api-keys.json` - API key storage
|
||||
- `data/budget.json` - Financial tracking
|
||||
- `data/activity.json` - Activity log
|
||||
- `~/.config/systemd/user/case-control-panel.service` - Systemd service file
|
||||
47
projects/control-panel/data/accounts.json
Normal file
47
projects/control-panel/data/accounts.json
Normal file
@ -0,0 +1,47 @@
|
||||
[
|
||||
{
|
||||
"service": "ProtonMail",
|
||||
"url": "https://mail.proton.me",
|
||||
"username": "case-lgn@protonmail.com",
|
||||
"status": "active",
|
||||
"notes": "Primary email account",
|
||||
"created": "2026-02-08T09:57:59.243980",
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Polymarket",
|
||||
"url": "https://polymarket.com",
|
||||
"username": "",
|
||||
"status": "inactive",
|
||||
"notes": "Not yet registered",
|
||||
"created": "2026-02-08T09:57:59.243987",
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Feed Hunter Portal",
|
||||
"url": "http://localhost:8888",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "Local service",
|
||||
"created": "2026-02-08T09:57:59.243989",
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Chrome Debug",
|
||||
"url": "http://localhost:9222",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "Browser debugging interface",
|
||||
"created": "2026-02-08T09:57:59.243991",
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "OpenClaw Gateway",
|
||||
"url": "http://localhost:18789",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "OpenClaw main service",
|
||||
"created": "2026-02-08T09:57:59.243993",
|
||||
"last_accessed": "Never"
|
||||
}
|
||||
]
|
||||
12
projects/control-panel/data/activity.json
Normal file
12
projects/control-panel/data/activity.json
Normal file
@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-02-08T09:58:29.699634",
|
||||
"action": "API Key Added",
|
||||
"details": "Added key for OpenAI"
|
||||
},
|
||||
{
|
||||
"timestamp": "2026-02-08T09:58:17.967014",
|
||||
"action": "Budget Entry Added",
|
||||
"details": "deposit of $100.00"
|
||||
}
|
||||
]
|
||||
10
projects/control-panel/data/api-keys.json
Normal file
10
projects/control-panel/data/api-keys.json
Normal file
@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"service": "OpenAI",
|
||||
"name": "test-key",
|
||||
"key": "sk-test123456789",
|
||||
"created": "2026-02-08",
|
||||
"expires": "2025-12-31",
|
||||
"usage": 0
|
||||
}
|
||||
]
|
||||
9
projects/control-panel/data/budget.json
Normal file
9
projects/control-panel/data/budget.json
Normal file
@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"type": "deposit",
|
||||
"service": "Test",
|
||||
"amount": 100.0,
|
||||
"description": "Initial test deposit",
|
||||
"timestamp": "2026-02-08T09:58:17.966780"
|
||||
}
|
||||
]
|
||||
884
projects/control-panel/server.py
Executable file
884
projects/control-panel/server.py
Executable file
@ -0,0 +1,884 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import datetime
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from socketserver import ThreadingMixIn
|
||||
|
||||
|
||||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
"""Handle requests in a separate thread."""
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.data_dir = "/home/wdjones/.openclaw/workspace/projects/control-panel/data"
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def do_GET(self):
|
||||
try:
|
||||
self.handle_get()
|
||||
except Exception as e:
|
||||
self.send_error(500, f"Internal error: {e}")
|
||||
|
||||
def do_POST(self):
|
||||
try:
|
||||
self.handle_post()
|
||||
except Exception as e:
|
||||
self.send_error(500, f"Internal error: {e}")
|
||||
|
||||
def handle_get(self):
|
||||
if self.path == '/':
|
||||
self.serve_dashboard()
|
||||
elif self.path == '/accounts':
|
||||
self.serve_accounts()
|
||||
elif self.path == '/api-keys':
|
||||
self.serve_api_keys()
|
||||
elif self.path == '/services':
|
||||
self.serve_services()
|
||||
elif self.path == '/budget':
|
||||
self.serve_budget()
|
||||
elif self.path == '/activity':
|
||||
self.serve_activity()
|
||||
else:
|
||||
self.send_error(404, "Not found")
|
||||
|
||||
def handle_post(self):
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
form_data = urllib.parse.parse_qs(post_data)
|
||||
|
||||
if self.path == '/accounts':
|
||||
self.handle_accounts_post(form_data)
|
||||
elif self.path == '/api-keys':
|
||||
self.handle_api_keys_post(form_data)
|
||||
elif self.path == '/budget':
|
||||
self.handle_budget_post(form_data)
|
||||
else:
|
||||
self.send_error(404, "Not found")
|
||||
|
||||
def load_data(self, filename):
|
||||
filepath = os.path.join(self.data_dir, filename)
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, 'r') as f:
|
||||
return json.load(f)
|
||||
return []
|
||||
|
||||
def save_data(self, filename, data):
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
filepath = os.path.join(self.data_dir, filename)
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def log_activity(self, action, details=""):
|
||||
activity = self.load_data('activity.json')
|
||||
entry = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"action": action,
|
||||
"details": details
|
||||
}
|
||||
activity.insert(0, entry) # Latest first
|
||||
activity = activity[:100] # Keep last 100 entries
|
||||
self.save_data('activity.json', activity)
|
||||
|
||||
def check_service_health(self, port):
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(2)
|
||||
result = sock.connect_ex(('localhost', port))
|
||||
sock.close()
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_base_template(self, title, content):
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title} - Case Control Panel</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}}
|
||||
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}}
|
||||
|
||||
header {{
|
||||
background: #21262d;
|
||||
border-bottom: 2px solid #30363d;
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}}
|
||||
|
||||
.header-content {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}}
|
||||
|
||||
.logo {{
|
||||
font-size: 1.8em;
|
||||
font-weight: bold;
|
||||
color: #58a6ff;
|
||||
}}
|
||||
|
||||
nav a {{
|
||||
color: #c9d1d9;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}}
|
||||
|
||||
nav a:hover {{
|
||||
background: #30363d;
|
||||
}}
|
||||
|
||||
nav a.active {{
|
||||
background: #1f6feb;
|
||||
}}
|
||||
|
||||
.card {{
|
||||
background: #21262d;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}}
|
||||
|
||||
.card-header {{
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #30363d;
|
||||
color: #58a6ff;
|
||||
}}
|
||||
|
||||
.stats-grid {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}}
|
||||
|
||||
.stat-card {{
|
||||
background: #161b22;
|
||||
border: 1px solid #21262d;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.stat-number {{
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #40c463;
|
||||
display: block;
|
||||
}}
|
||||
|
||||
.stat-label {{
|
||||
color: #8b949e;
|
||||
margin-top: 0.5rem;
|
||||
}}
|
||||
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #30363d;
|
||||
}}
|
||||
|
||||
th {{
|
||||
background: #161b22;
|
||||
color: #58a6ff;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
tr:hover {{
|
||||
background: #161b22;
|
||||
}}
|
||||
|
||||
.status-active {{
|
||||
color: #40c463;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.status-inactive {{
|
||||
color: #f85149;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.btn {{
|
||||
background: #238636;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 2px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
|
||||
.btn:hover {{
|
||||
background: #2ea043;
|
||||
}}
|
||||
|
||||
.btn-danger {{
|
||||
background: #da3633;
|
||||
}}
|
||||
|
||||
.btn-danger:hover {{
|
||||
background: #f85149;
|
||||
}}
|
||||
|
||||
.btn-secondary {{
|
||||
background: #21262d;
|
||||
border: 1px solid #30363d;
|
||||
}}
|
||||
|
||||
.btn-secondary:hover {{
|
||||
background: #30363d;
|
||||
}}
|
||||
|
||||
.form-group {{
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
|
||||
.form-group label {{
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #f0f6fc;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.form-group input, .form-group select, .form-group textarea {{
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
background: #0d1117;
|
||||
color: #c9d1d9;
|
||||
font-family: inherit;
|
||||
}}
|
||||
|
||||
.form-group input:focus, .form-group select:focus, .form-group textarea:focus {{
|
||||
outline: none;
|
||||
border-color: #58a6ff;
|
||||
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.1);
|
||||
}}
|
||||
|
||||
.masked-key {{
|
||||
font-family: monospace;
|
||||
background: #161b22;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
border: 1px solid #30363d;
|
||||
}}
|
||||
|
||||
.reveal-btn {{
|
||||
background: none;
|
||||
border: none;
|
||||
color: #58a6ff;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 12px;
|
||||
}}
|
||||
|
||||
.activity-entry {{
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #30363d;
|
||||
}}
|
||||
|
||||
.activity-timestamp {{
|
||||
color: #8b949e;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
.activity-action {{
|
||||
font-weight: bold;
|
||||
color: #40c463;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<div class="logo">🖤 Case Control Panel</div>
|
||||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/accounts">Accounts</a>
|
||||
<a href="/api-keys">API Keys</a>
|
||||
<a href="/services">Services</a>
|
||||
<a href="/budget">Budget</a>
|
||||
<a href="/activity">Activity</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container">
|
||||
{content}
|
||||
</div>
|
||||
<script>
|
||||
function toggleKey(element) {{
|
||||
const key = element.getAttribute('data-key');
|
||||
if (element.textContent.includes('*')) {{
|
||||
element.textContent = key;
|
||||
}} else {{
|
||||
element.textContent = '*'.repeat(key.length);
|
||||
}}
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def serve_dashboard(self):
|
||||
accounts = self.load_data('accounts.json')
|
||||
api_keys = self.load_data('api-keys.json')
|
||||
budget = self.load_data('budget.json')
|
||||
|
||||
# Calculate stats
|
||||
total_accounts = len(accounts)
|
||||
active_accounts = len([a for a in accounts if a.get('status') == 'active'])
|
||||
total_api_keys = len(api_keys)
|
||||
monthly_spend = sum([b.get('amount', 0) for b in budget if
|
||||
b.get('type') == 'spending' and
|
||||
b.get('timestamp', '').startswith(datetime.now().strftime('%Y-%m'))])
|
||||
|
||||
content = f"""
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{total_accounts}</span>
|
||||
<div class="stat-label">Total Accounts</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{active_accounts}</span>
|
||||
<div class="stat-label">Active Services</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">{total_api_keys}</span>
|
||||
<div class="stat-label">API Keys</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">${monthly_spend:.2f}</span>
|
||||
<div class="stat-label">Monthly Spend</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Quick Actions</div>
|
||||
<a href="/accounts" class="btn">Manage Accounts</a>
|
||||
<a href="/api-keys" class="btn">Manage API Keys</a>
|
||||
<a href="/budget" class="btn">Add Budget Entry</a>
|
||||
<a href="/services" class="btn">Check Services</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Dashboard", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def serve_accounts(self):
|
||||
accounts = self.load_data('accounts.json')
|
||||
|
||||
accounts_table = ""
|
||||
for i, account in enumerate(accounts):
|
||||
status_class = "status-active" if account.get('status') == 'active' else "status-inactive"
|
||||
login_btn = f'<a href="{account.get("url", "#")}" target="_blank" class="btn btn-secondary">Login</a>' if account.get('url') else ""
|
||||
|
||||
accounts_table += f"""
|
||||
<tr>
|
||||
<td>{account.get('service', 'N/A')}</td>
|
||||
<td><a href="{account.get('url', '#')}" target="_blank">{account.get('url', 'N/A')}</a></td>
|
||||
<td>{account.get('username', 'N/A')}</td>
|
||||
<td><span class="{status_class}">{account.get('status', 'unknown')}</span></td>
|
||||
<td>{account.get('last_accessed', 'Never')}</td>
|
||||
<td>{account.get('notes', '')}</td>
|
||||
<td>{login_btn}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
content = f"""
|
||||
<div class="card">
|
||||
<div class="card-header">Account Management</div>
|
||||
|
||||
<form method="POST" style="margin-bottom: 2rem;">
|
||||
<div class="form-group">
|
||||
<label>Service Name:</label>
|
||||
<input type="text" name="service" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>URL:</label>
|
||||
<input type="url" name="url">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Username/Email:</label>
|
||||
<input type="text" name="username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status:</label>
|
||||
<select name="status">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Notes:</label>
|
||||
<textarea name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<input type="hidden" name="action" value="add">
|
||||
<button type="submit" class="btn">Add Account</button>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>URL</th>
|
||||
<th>Username/Email</th>
|
||||
<th>Status</th>
|
||||
<th>Last Accessed</th>
|
||||
<th>Notes</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{accounts_table}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Accounts", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def serve_api_keys(self):
|
||||
api_keys = self.load_data('api-keys.json')
|
||||
|
||||
keys_table = ""
|
||||
for key in api_keys:
|
||||
masked_key = f'<span class="masked-key" onclick="toggleKey(this)" data-key="{key.get("key", "")}">' + \
|
||||
('*' * len(key.get('key', ''))) + '</span>'
|
||||
|
||||
keys_table += f"""
|
||||
<tr>
|
||||
<td>{key.get('service', 'N/A')}</td>
|
||||
<td>{key.get('name', 'N/A')}</td>
|
||||
<td>{masked_key}</td>
|
||||
<td>{key.get('created', 'N/A')}</td>
|
||||
<td>{key.get('expires', 'Never')}</td>
|
||||
<td>{key.get('usage', 0)}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
content = f"""
|
||||
<div class="card">
|
||||
<div class="card-header">API Key Management</div>
|
||||
|
||||
<form method="POST" style="margin-bottom: 2rem;">
|
||||
<div class="form-group">
|
||||
<label>Service:</label>
|
||||
<input type="text" name="service" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Key Name:</label>
|
||||
<input type="text" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key:</label>
|
||||
<input type="text" name="key" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Expires (optional):</label>
|
||||
<input type="date" name="expires">
|
||||
</div>
|
||||
<input type="hidden" name="action" value="add">
|
||||
<button type="submit" class="btn">Add API Key</button>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Name</th>
|
||||
<th>Key (click to reveal)</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Usage Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys_table}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("API Keys", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def serve_services(self):
|
||||
services = [
|
||||
{"name": "Feed Hunter Portal", "port": 8888},
|
||||
{"name": "Chrome Debug", "port": 9222},
|
||||
{"name": "OpenClaw Gateway", "port": 18789},
|
||||
{"name": "Case Control Panel", "port": 8000},
|
||||
]
|
||||
|
||||
services_table = ""
|
||||
for service in services:
|
||||
is_healthy = self.check_service_health(service["port"])
|
||||
status = "Running" if is_healthy else "Stopped"
|
||||
status_class = "status-active" if is_healthy else "status-inactive"
|
||||
|
||||
services_table += f"""
|
||||
<tr>
|
||||
<td>{service['name']}</td>
|
||||
<td>{service['port']}</td>
|
||||
<td><span class="{status_class}">{status}</span></td>
|
||||
<td>N/A</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
content = f"""
|
||||
<div class="card">
|
||||
<div class="card-header">Running Services</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service Name</th>
|
||||
<th>Port</th>
|
||||
<th>Status</th>
|
||||
<th>Uptime</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services_table}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button onclick="location.reload()" class="btn">Refresh Status</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Services", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def serve_budget(self):
|
||||
budget = self.load_data('budget.json')
|
||||
|
||||
# Calculate totals
|
||||
total_balance = sum([b.get('amount', 0) for b in budget if b.get('type') == 'deposit']) - \
|
||||
sum([b.get('amount', 0) for b in budget if b.get('type') in ['withdrawal', 'spending']])
|
||||
|
||||
current_month = datetime.now().strftime('%Y-%m')
|
||||
monthly_spending = sum([b.get('amount', 0) for b in budget if
|
||||
b.get('type') == 'spending' and
|
||||
b.get('timestamp', '').startswith(current_month)])
|
||||
|
||||
budget_table = ""
|
||||
for entry in sorted(budget, key=lambda x: x.get('timestamp', ''), reverse=True)[:50]:
|
||||
amount_str = f"${entry.get('amount', 0):.2f}"
|
||||
if entry.get('type') == 'deposit':
|
||||
amount_str = f"+{amount_str}"
|
||||
elif entry.get('type') in ['withdrawal', 'spending']:
|
||||
amount_str = f"-{amount_str}"
|
||||
|
||||
budget_table += f"""
|
||||
<tr>
|
||||
<td>{entry.get('timestamp', 'N/A')}</td>
|
||||
<td>{entry.get('type', 'N/A')}</td>
|
||||
<td>{entry.get('service', 'General')}</td>
|
||||
<td>{amount_str}</td>
|
||||
<td>{entry.get('description', '')}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
content = f"""
|
||||
<div class="stats-grid" style="margin-bottom: 2rem;">
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">${total_balance:.2f}</span>
|
||||
<div class="stat-label">Total Balance</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-number">${monthly_spending:.2f}</span>
|
||||
<div class="stat-label">Monthly Spending</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">Budget Management</div>
|
||||
|
||||
<form method="POST" style="margin-bottom: 2rem;">
|
||||
<div class="form-group">
|
||||
<label>Type:</label>
|
||||
<select name="type" required>
|
||||
<option value="deposit">Deposit</option>
|
||||
<option value="withdrawal">Withdrawal</option>
|
||||
<option value="spending">Spending</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Service:</label>
|
||||
<input type="text" name="service" placeholder="General">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Amount ($):</label>
|
||||
<input type="number" step="0.01" name="amount" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Description:</label>
|
||||
<input type="text" name="description">
|
||||
</div>
|
||||
<input type="hidden" name="action" value="add">
|
||||
<button type="submit" class="btn">Add Entry</button>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>Service</th>
|
||||
<th>Amount</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{budget_table}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Budget", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def serve_activity(self):
|
||||
activity = self.load_data('activity.json')
|
||||
|
||||
activity_list = ""
|
||||
for entry in activity:
|
||||
activity_list += f"""
|
||||
<div class="activity-entry">
|
||||
<div class="activity-timestamp">{entry.get('timestamp', 'N/A')}</div>
|
||||
<div class="activity-action">{entry.get('action', 'N/A')}</div>
|
||||
<div>{entry.get('details', '')}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
content = f"""
|
||||
<div class="card">
|
||||
<div class="card-header">Activity Log</div>
|
||||
{activity_list if activity_list else '<p>No activity recorded yet.</p>'}
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Activity", content)
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'text/html')
|
||||
self.end_headers()
|
||||
self.wfile.write(html.encode())
|
||||
|
||||
def handle_accounts_post(self, form_data):
|
||||
if form_data.get('action', [''])[0] == 'add':
|
||||
accounts = self.load_data('accounts.json')
|
||||
new_account = {
|
||||
"service": form_data.get('service', [''])[0],
|
||||
"url": form_data.get('url', [''])[0],
|
||||
"username": form_data.get('username', [''])[0],
|
||||
"status": form_data.get('status', ['active'])[0],
|
||||
"notes": form_data.get('notes', [''])[0],
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
}
|
||||
accounts.append(new_account)
|
||||
self.save_data('accounts.json', accounts)
|
||||
self.log_activity("Account Added", f"Added {new_account['service']}")
|
||||
|
||||
# Redirect back to accounts page
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/accounts')
|
||||
self.end_headers()
|
||||
|
||||
def handle_api_keys_post(self, form_data):
|
||||
if form_data.get('action', [''])[0] == 'add':
|
||||
api_keys = self.load_data('api-keys.json')
|
||||
new_key = {
|
||||
"service": form_data.get('service', [''])[0],
|
||||
"name": form_data.get('name', [''])[0],
|
||||
"key": form_data.get('key', [''])[0],
|
||||
"created": datetime.now().strftime('%Y-%m-%d'),
|
||||
"expires": form_data.get('expires', ['Never'])[0] or "Never",
|
||||
"usage": 0
|
||||
}
|
||||
api_keys.append(new_key)
|
||||
self.save_data('api-keys.json', api_keys)
|
||||
self.log_activity("API Key Added", f"Added key for {new_key['service']}")
|
||||
|
||||
# Redirect back to api-keys page
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/api-keys')
|
||||
self.end_headers()
|
||||
|
||||
def handle_budget_post(self, form_data):
|
||||
if form_data.get('action', [''])[0] == 'add':
|
||||
budget = self.load_data('budget.json')
|
||||
new_entry = {
|
||||
"type": form_data.get('type', [''])[0],
|
||||
"service": form_data.get('service', ['General'])[0] or "General",
|
||||
"amount": float(form_data.get('amount', ['0'])[0]),
|
||||
"description": form_data.get('description', [''])[0],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
budget.append(new_entry)
|
||||
self.save_data('budget.json', budget)
|
||||
self.log_activity("Budget Entry Added", f"{new_entry['type']} of ${new_entry['amount']:.2f}")
|
||||
|
||||
# Redirect back to budget page
|
||||
self.send_response(302)
|
||||
self.send_header('Location', '/budget')
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Override to reduce logging noise"""
|
||||
pass
|
||||
|
||||
|
||||
def initialize_data():
|
||||
"""Pre-populate with known accounts and services"""
|
||||
data_dir = "/home/wdjones/.openclaw/workspace/projects/control-panel/data"
|
||||
os.makedirs(data_dir, exist_ok=True)
|
||||
|
||||
# Pre-populate accounts
|
||||
accounts_file = os.path.join(data_dir, "accounts.json")
|
||||
if not os.path.exists(accounts_file):
|
||||
initial_accounts = [
|
||||
{
|
||||
"service": "ProtonMail",
|
||||
"url": "https://mail.proton.me",
|
||||
"username": "case-lgn@protonmail.com",
|
||||
"status": "active",
|
||||
"notes": "Primary email account",
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Polymarket",
|
||||
"url": "https://polymarket.com",
|
||||
"username": "",
|
||||
"status": "inactive",
|
||||
"notes": "Not yet registered",
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Feed Hunter Portal",
|
||||
"url": "http://localhost:8888",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "Local service",
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "Chrome Debug",
|
||||
"url": "http://localhost:9222",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "Browser debugging interface",
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
},
|
||||
{
|
||||
"service": "OpenClaw Gateway",
|
||||
"url": "http://localhost:18789",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "OpenClaw main service",
|
||||
"created": datetime.now().isoformat(),
|
||||
"last_accessed": "Never"
|
||||
}
|
||||
]
|
||||
|
||||
with open(accounts_file, 'w') as f:
|
||||
json.dump(initial_accounts, f, indent=2)
|
||||
|
||||
# Initialize empty files if they don't exist
|
||||
for filename in ["api-keys.json", "budget.json", "activity.json"]:
|
||||
filepath = os.path.join(data_dir, filename)
|
||||
if not os.path.exists(filepath):
|
||||
with open(filepath, 'w') as f:
|
||||
json.dump([], f)
|
||||
|
||||
|
||||
def main():
|
||||
initialize_data()
|
||||
|
||||
server_address = ('0.0.0.0', 8000)
|
||||
httpd = ThreadedHTTPServer(server_address, ControlPanelHandler)
|
||||
|
||||
print(f"🖤 Case Control Panel starting on http://0.0.0.0:8000")
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nShutting down...")
|
||||
httpd.shutdown()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -7,6 +7,8 @@
|
||||
},
|
||||
"investigation": {
|
||||
"profile_url": "https://polymarket.com/@kch123",
|
||||
"wallet_address": "0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee",
|
||||
"secondary_wallet": "0x8c74b4eef9a894433B8126aA11d1345efb2B0488",
|
||||
"verified_data": {
|
||||
"all_time_pnl": "$9,371,829.00",
|
||||
"positions_value": "$2.3m",
|
||||
|
||||
@ -1,24 +1,100 @@
|
||||
{
|
||||
"positions": [
|
||||
{
|
||||
"id": "6607b9c1",
|
||||
"strategy": "polymarket-copy-kch123",
|
||||
"opened_at": "2026-02-08T05:50:14.328434+00:00",
|
||||
"id": "1403ffd3",
|
||||
"strategy": "copy-kch123-spread-4.5",
|
||||
"opened_at": "2026-02-08T15:15:17.482343+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Seahawks win Super Bowl 2026",
|
||||
"entry_price": 0.68,
|
||||
"size": 200,
|
||||
"quantity": 1470,
|
||||
"stop_loss": 0.4,
|
||||
"take_profit": 1.0,
|
||||
"current_price": 0.68,
|
||||
"asset": "Spread: Seahawks (-4.5)",
|
||||
"entry_price": 0.505,
|
||||
"size": 200.0,
|
||||
"quantity": 851,
|
||||
"stop_loss": null,
|
||||
"take_profit": null,
|
||||
"current_price": 0.505,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 largest active position. Seahawks Super Bowl at 68c. If they win, pays $1. kch123 has $9.3M all-time P&L, 1862 predictions. Sports betting specialist.",
|
||||
"notes": "Paper trade to track if copying kch123 positions is profitable. Entry simulated at current 68c price.",
|
||||
"thesis": "Mirror kch123 largest position. Seahawks -4.5 spread vs Patriots. Super Bowl today.",
|
||||
"notes": "",
|
||||
"updates": []
|
||||
},
|
||||
{
|
||||
"id": "5451b4d6",
|
||||
"strategy": "copy-kch123-sb-yes",
|
||||
"opened_at": "2026-02-08T15:15:17.519032+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Seahawks win Super Bowl 2026",
|
||||
"entry_price": 0.6845,
|
||||
"size": 200.0,
|
||||
"quantity": 324,
|
||||
"stop_loss": null,
|
||||
"take_profit": null,
|
||||
"current_price": 0.6845,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 SB winner bet. Seahawks YES at 68.45c.",
|
||||
"notes": "",
|
||||
"updates": []
|
||||
},
|
||||
{
|
||||
"id": "f2ddcf73",
|
||||
"strategy": "copy-kch123-moneyline",
|
||||
"opened_at": "2026-02-08T15:15:17.555276+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Seahawks vs Patriots (Moneyline)",
|
||||
"entry_price": 0.685,
|
||||
"size": 184,
|
||||
"quantity": 269,
|
||||
"stop_loss": null,
|
||||
"take_profit": null,
|
||||
"current_price": 0.685,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 moneyline. Seahawks to beat Patriots straight up.",
|
||||
"notes": "",
|
||||
"updates": []
|
||||
},
|
||||
{
|
||||
"id": "3fcfddb4",
|
||||
"strategy": "copy-kch123-spread-5.5",
|
||||
"opened_at": "2026-02-08T15:15:17.593863+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Spread: Seahawks (-5.5)",
|
||||
"entry_price": 0.475,
|
||||
"size": 89,
|
||||
"quantity": 188,
|
||||
"stop_loss": null,
|
||||
"take_profit": null,
|
||||
"current_price": 0.475,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 wider spread. Seahawks -5.5. Riskier.",
|
||||
"notes": "",
|
||||
"updates": []
|
||||
},
|
||||
{
|
||||
"id": "bf1e7b4f",
|
||||
"strategy": "copy-kch123-pats-no",
|
||||
"opened_at": "2026-02-08T15:15:17.632987+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Patriots win Super Bowl - NO",
|
||||
"entry_price": 0.6865,
|
||||
"size": 75,
|
||||
"quantity": 110,
|
||||
"stop_loss": null,
|
||||
"take_profit": null,
|
||||
"current_price": 0.6865,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 hedge/complement. Patriots NO to win SB.",
|
||||
"notes": "",
|
||||
"updates": []
|
||||
}
|
||||
],
|
||||
"bankroll_used": 200
|
||||
"bankroll_used": 748.0
|
||||
}
|
||||
27
projects/feed-hunter/data/simulations/history.json
Normal file
27
projects/feed-hunter/data/simulations/history.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"closed": [
|
||||
{
|
||||
"id": "6607b9c1",
|
||||
"strategy": "polymarket-copy-kch123",
|
||||
"opened_at": "2026-02-08T05:50:14.328434+00:00",
|
||||
"type": "bet",
|
||||
"asset": "Seahawks win Super Bowl 2026",
|
||||
"entry_price": 0.68,
|
||||
"size": 200,
|
||||
"quantity": 1470,
|
||||
"stop_loss": 0.4,
|
||||
"take_profit": 1.0,
|
||||
"current_price": 0.68,
|
||||
"unrealized_pnl": 0,
|
||||
"unrealized_pnl_pct": 0,
|
||||
"source_post": "https://x.com/linie_oo/status/2020141674828034243",
|
||||
"thesis": "Mirror kch123 largest active position. Seahawks Super Bowl at 68c. If they win, pays $1. kch123 has $9.3M all-time P&L, 1862 predictions. Sports betting specialist.",
|
||||
"notes": "Paper trade to track if copying kch123 positions is profitable. Entry simulated at current 68c price.",
|
||||
"updates": [],
|
||||
"closed_at": "2026-02-08T15:15:17.443369+00:00",
|
||||
"exit_price": 0.6845,
|
||||
"realized_pnl": 1.32,
|
||||
"realized_pnl_pct": 0.66
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,50 @@
|
||||
----------------------------------------
|
||||
Exception occurred during processing of request from ('127.0.0.1', 45572)
|
||||
Traceback (most recent call last):
|
||||
File "/usr/lib/python3.12/socketserver.py", line 318, in _handle_request_noblock
|
||||
self.process_request(request, client_address)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 349, in process_request
|
||||
self.finish_request(request, client_address)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 362, in finish_request
|
||||
self.RequestHandlerClass(request, client_address, self)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 761, in __init__
|
||||
self.handle()
|
||||
File "/usr/lib/python3.12/http/server.py", line 436, in handle
|
||||
self.handle_one_request()
|
||||
File "/usr/lib/python3.12/http/server.py", line 424, in handle_one_request
|
||||
method()
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 37, in do_GET
|
||||
self.serve_simulations()
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 243, in serve_simulations
|
||||
{self.render_trade_history(sims.get('history', []))}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 748, in render_trade_history
|
||||
for trade in history[-10:]: # Last 10 trades
|
||||
~~~~~~~^^^^^^
|
||||
KeyError: slice(-10, None, None)
|
||||
----------------------------------------
|
||||
----------------------------------------
|
||||
Exception occurred during processing of request from ('127.0.0.1', 48354)
|
||||
Traceback (most recent call last):
|
||||
File "/usr/lib/python3.12/socketserver.py", line 318, in _handle_request_noblock
|
||||
self.process_request(request, client_address)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 349, in process_request
|
||||
self.finish_request(request, client_address)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 362, in finish_request
|
||||
self.RequestHandlerClass(request, client_address, self)
|
||||
File "/usr/lib/python3.12/socketserver.py", line 761, in __init__
|
||||
self.handle()
|
||||
File "/usr/lib/python3.12/http/server.py", line 436, in handle
|
||||
self.handle_one_request()
|
||||
File "/usr/lib/python3.12/http/server.py", line 424, in handle_one_request
|
||||
method()
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 37, in do_GET
|
||||
self.serve_simulations()
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 243, in serve_simulations
|
||||
{self.render_trade_history(sims.get('history', []))}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 748, in render_trade_history
|
||||
for trade in history[-10:]: # Last 10 trades
|
||||
~~~~~~~^^^^^^
|
||||
KeyError: slice(-10, None, None)
|
||||
----------------------------------------
|
||||
|
||||
@ -9,37 +9,54 @@ 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
|
||||
DATA_DIR = "../data"
|
||||
SKILLS_DIR = "../../skills/deep-scraper/scripts"
|
||||
_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):
|
||||
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)
|
||||
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"""
|
||||
@ -237,7 +254,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler):
|
||||
<div class="card">
|
||||
<h3>Trade History</h3>
|
||||
<div class="trade-history">
|
||||
{self.render_trade_history(sims.get('history', []))}
|
||||
{self.render_trade_history(sims.get('history', {}).get('closed', []) if isinstance(sims.get('history'), dict) else sims.get('history', []))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -425,7 +442,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler):
|
||||
posts = []
|
||||
try:
|
||||
# Find latest x-feed directory
|
||||
x_feed_pattern = os.path.join("../../data/x-feed", "20*")
|
||||
x_feed_pattern = os.path.join(X_FEED_DIR, "20*")
|
||||
x_feed_dirs = sorted(glob.glob(x_feed_pattern))
|
||||
|
||||
if x_feed_dirs:
|
||||
@ -526,7 +543,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler):
|
||||
}
|
||||
|
||||
# Check for recent pipeline runs
|
||||
x_feed_pattern = os.path.join("../../data/x-feed", "20*")
|
||||
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])
|
||||
@ -592,7 +609,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler):
|
||||
return html
|
||||
|
||||
def render_investigations(self, investigations):
|
||||
"""Render investigation reports"""
|
||||
"""Render investigation reports with rich links"""
|
||||
if not investigations:
|
||||
return '<div class="empty-state">No investigations found</div>'
|
||||
|
||||
@ -601,21 +618,81 @@ class FeedHunterHandler(BaseHTTPRequestHandler):
|
||||
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>
|
||||
<div class="investigation-score">Risk Score: {risk_score}/10</div>
|
||||
<div class="investigation-actions">
|
||||
<button onclick="showInvestigationDetail('{inv.get('id', '')}')">View Details</button>
|
||||
</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>
|
||||
"""
|
||||
|
||||
@ -980,6 +1057,110 @@ body {
|
||||
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);
|
||||
@ -1252,7 +1433,7 @@ def main():
|
||||
print("")
|
||||
|
||||
try:
|
||||
server = HTTPServer(('localhost', PORT), FeedHunterHandler)
|
||||
server = ThreadedHTTPServer(('0.0.0.0', PORT), FeedHunterHandler)
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Portal stopped")
|
||||
|
||||
Reference in New Issue
Block a user