#!/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() elif self.path == '/todos': self.serve_todos() elif self.path == '/notes': self.serve_notes() 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) elif self.path == '/todos': self.handle_todos_post(form_data) elif self.path == '/notes': self.handle_notes_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""" {title} - Case Control Panel
{content}
""" def serve_dashboard(self): accounts = self.load_data('accounts.json') api_keys = self.load_data('api-keys.json') budget = self.load_data('budget.json') todos = self.load_data('todos.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) pending_todos = len([t for t in todos if t.get('status') == 'pending']) 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"""
{total_accounts}
Total Accounts
{active_accounts}
Active Services
{total_api_keys}
API Keys
{pending_todos}
⚡ Actions Required
Quick Actions
Manage Accounts Manage API Keys Add Budget Entry Check Services
""" 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" url = account.get('url', '') service_name = account.get('service', 'N/A') service_link = f'{service_name}' if url else service_name accounts_table += f""" {service_link} {account.get('username', 'N/A')} {account.get('status', 'unknown')} {account.get('notes', '')}
""" content = f"""
Account Management
{accounts_table}
Service Username/Email Status Notes Actions
""" 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 i, key in enumerate(api_keys): key_val = key.get('key', key.get('key_location', '')) key_type = key.get('type', '') status = key.get('status', 'active') status_class = "status-active" if status == 'active' else ("status-inactive" if status in ['missing','inactive'] else "") display_val = key_val if key_val else 'N/A' masked = '*' * min(len(display_val), 30) if display_val not in ['N/A', 'NOT CONFIGURED'] else display_val notes = key.get('notes', '') keys_table += f""" {key.get('service', 'N/A')} {key_type} {masked} {status} {notes}
""" content = f"""
API Key Management
{keys_table}
Service Type Location (click to reveal) Status Notes Actions
""" 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 check_remote_health(self, host, port): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) result = sock.connect_ex((host, port)) sock.close() return result == 0 except: return False def serve_services(self): svc_data = self.load_data('services.json') if not svc_data: svc_data = {"local": [], "timers": [], "deployed": [], "infrastructure": []} # --- Local Services --- local_rows = "" running_count = 0 for svc in svc_data.get('local', []): port = svc.get('port') is_healthy = self.check_service_health(port) if port else False if is_healthy: running_count += 1 status = "Running" if is_healthy else "Stopped" status_class = "status-active" if is_healthy else "status-inactive" url = f"http://192.168.86.45:{port}" if port else "" name_cell = f'{svc["name"]}' if url else svc["name"] systemd = svc.get('systemd', '') or '—' local_rows += f""" {name_cell} {port or '—'} ● {status} {systemd} {svc.get('description', '')} """ # --- Timers --- timer_rows = "" for t in svc_data.get('timers', []): timer_rows += f""" {t['name']} {t.get('interval', '')} {t.get('systemd', '')} {t.get('description', '')} """ # --- Deployed Apps --- deployed_cards = "" for app in svc_data.get('deployed', []): deployed_cards += f"""
{app['name']}
{app.get('description', '')}
{app.get('platform', '')}
""" if not deployed_cards: deployed_cards = '

No deployed apps yet.

' # --- Infrastructure --- infra_rows = "" for inf in svc_data.get('infrastructure', []): host = inf.get('host', '') port = inf.get('port') is_healthy = self.check_remote_health(host, port) if host and port else False status = "Reachable" if is_healthy else ("Unknown" if not port else "Unreachable") status_class = "status-active" if is_healthy else ("" if not port else "status-inactive") url = f"http://{host}:{port}" if port else "" name_cell = f'{inf["name"]}' if url else inf["name"] infra_rows += f""" {name_cell} {host} {port or '—'} ● {status} {inf.get('description', '')} """ total_local = len(svc_data.get('local', [])) total_timers = len(svc_data.get('timers', [])) content = f"""
{running_count}/{total_local}
Services Running
{total_timers}
Active Timers
{len(svc_data.get('deployed', []))}
Deployed Apps
{len(svc_data.get('infrastructure', []))}
Infrastructure
🖥️ Local Services (192.168.86.45)
{local_rows}
Service Port Status Systemd Unit Description
⏱️ Scheduled Timers
{timer_rows}
Name Interval Systemd Unit Description
🚀 Deployed Applications
{deployed_cards}
🏗️ Infrastructure
{infra_rows}
Service Host Port Status Description
""" 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""" {entry.get('timestamp', 'N/A')} {entry.get('type', 'N/A')} {entry.get('service', 'General')} {amount_str} {entry.get('description', '')} """ content = f"""
${total_balance:.2f}
Total Balance
${monthly_spending:.2f}
Monthly Spending
Budget Management
{budget_table}
Date Type Service Amount Description
""" 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"""
{entry.get('timestamp', 'N/A')}
{entry.get('action', 'N/A')}
{entry.get('details', '')}
""" content = f"""
Activity Log
{activity_list if activity_list else '

No activity recorded yet.

'}
""" 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 serve_todos(self): todos = self.load_data('todos.json') pending = [t for t in todos if t.get('status') == 'pending'] done = [t for t in todos if t.get('status') == 'done'] priority_colors = {'high': '#f85149', 'medium': '#d29922', 'low': '#8b949e'} category_icons = {'dns': '🌐', 'account': '🔑', 'config': '⚙️', 'install': '📦', 'other': '📋'} pending_html = "" for i, t in enumerate(pending): pc = priority_colors.get(t.get('priority', 'medium'), '#d29922') icon = category_icons.get(t.get('category', 'other'), '📋') steps = "" if t.get('steps'): steps_list = "".join(f"
  • {s}
  • " for s in t['steps']) steps = f'
    Steps:
      {steps_list}
    ' pending_html += f"""
    {icon} {t.get('title','Untitled')} ● {t.get('priority','medium').upper()}
    {t.get('description','')}
    {steps}
    Added {t.get('created','?')} by {t.get('source','unknown')}
    """ done_html = "" for t in done[:10]: done_html += f"""
    {t.get('title','')} completed {t.get('completed','')}
    """ content = f"""
    {len(pending)}
    Pending Actions
    {len(done)}
    Completed
    ⚡ Action Required
    {pending_html if pending_html else '

    Nothing pending — all clear! 🎉

    '}
    Recently Completed
    {done_html if done_html else '

    Nothing completed yet.

    '}
    """ html = self.get_base_template("Action Required", content) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def serve_notes(self): notes = self.load_data('notes.json') notes_html = "" for i, note in enumerate(notes): color = note.get('color', '#30363d') notes_html += f"""
    {note.get('title', 'Untitled')} {note.get('created', '')}
    {note.get('content', '')}
    """ content = f"""
    📝 Add Note
    {notes_html if notes_html else '

    No notes yet. Add one above.

    '} """ html = self.get_base_template("Notes", content) self.send_response(200) self.send_header('Content-type', 'text/html') self.end_headers() self.wfile.write(html.encode()) def handle_notes_post(self, form_data): action = form_data.get('action', [''])[0] notes = self.load_data('notes.json') if action == 'add': new_note = { "title": form_data.get('title', [''])[0], "content": form_data.get('content', [''])[0], "color": form_data.get('color', ['#30363d'])[0], "created": datetime.now().strftime('%Y-%m-%d %H:%M') } notes.insert(0, new_note) self.save_data('notes.json', notes) self.log_activity("Note Added", f"Added: {new_note['title']}") elif action == 'edit': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(notes): # Edit modal: service=title, notes=content notes[idx]['title'] = form_data.get('service', [notes[idx].get('title', '')])[0] notes[idx]['content'] = form_data.get('notes', [notes[idx].get('content', '')])[0] self.save_data('notes.json', notes) self.log_activity("Note Updated", f"Updated: {notes[idx]['title']}") elif action == 'delete': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(notes): deleted = notes.pop(idx) self.save_data('notes.json', notes) self.log_activity("Note Deleted", f"Deleted: {deleted.get('title', 'unknown')}") self.send_response(302) self.send_header('Location', '/notes') self.end_headers() def handle_todos_post(self, form_data): action = form_data.get('action', [''])[0] todos = self.load_data('todos.json') if action == 'complete': idx = int(form_data.get('index', ['0'])[0]) pending = [t for t in todos if t.get('status') == 'pending'] if 0 <= idx < len(pending): target = pending[idx] target['status'] = 'done' target['completed'] = datetime.now().strftime('%Y-%m-%d %H:%M') self.save_data('todos.json', todos) self.log_activity("Todo Completed", target.get('title', '')) self.send_response(302) self.send_header('Location', '/todos') self.end_headers() def handle_accounts_post(self, form_data): action = form_data.get('action', [''])[0] accounts = self.load_data('accounts.json') if action == 'add': 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']}") elif action == 'edit': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(accounts): accounts[idx]['service'] = form_data.get('service', [accounts[idx].get('service', '')])[0] accounts[idx]['url'] = form_data.get('url', [accounts[idx].get('url', '')])[0] accounts[idx]['username'] = form_data.get('username', [accounts[idx].get('username', '')])[0] accounts[idx]['status'] = form_data.get('status', [accounts[idx].get('status', 'active')])[0] accounts[idx]['notes'] = form_data.get('notes', [accounts[idx].get('notes', '')])[0] self.save_data('accounts.json', accounts) self.log_activity("Account Updated", f"Updated {accounts[idx]['service']}") elif action == 'delete': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(accounts): deleted = accounts.pop(idx) self.save_data('accounts.json', accounts) self.log_activity("Account Deleted", f"Deleted {deleted.get('service', 'unknown')}") self.send_response(302) self.send_header('Location', '/accounts') self.end_headers() def handle_api_keys_post(self, form_data): action = form_data.get('action', [''])[0] api_keys = self.load_data('api-keys.json') if action == 'add': new_key = { "service": form_data.get('service', [''])[0], "key_location": form_data.get('key_location', [''])[0], "type": form_data.get('type', [''])[0], "status": form_data.get('status', ['active'])[0], "notes": form_data.get('notes', [''])[0], "created": datetime.now().strftime('%Y-%m-%d') } 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']}") elif action == 'edit': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(api_keys): # The edit modal reuses account fields: service=service, url=key_location, username=type, status=status, notes=notes api_keys[idx]['service'] = form_data.get('service', [api_keys[idx].get('service', '')])[0] api_keys[idx]['key_location'] = form_data.get('url', [api_keys[idx].get('key_location', '')])[0] api_keys[idx]['type'] = form_data.get('username', [api_keys[idx].get('type', '')])[0] api_keys[idx]['status'] = form_data.get('status', [api_keys[idx].get('status', 'active')])[0] api_keys[idx]['notes'] = form_data.get('notes', [api_keys[idx].get('notes', '')])[0] self.save_data('api-keys.json', api_keys) self.log_activity("API Key Updated", f"Updated {api_keys[idx]['service']}") elif action == 'delete': idx = int(form_data.get('index', ['0'])[0]) if 0 <= idx < len(api_keys): deleted = api_keys.pop(idx) self.save_data('api-keys.json', api_keys) self.log_activity("API Key Deleted", f"Deleted {deleted.get('service', 'unknown')}") 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()