1362 lines
53 KiB
Python
Executable File
1362 lines
53 KiB
Python
Executable File
#!/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"""<!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;
|
|
table-layout: fixed;
|
|
}}
|
|
|
|
th, td {{
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #30363d;
|
|
word-break: break-word;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}}
|
|
|
|
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>
|
|
<a href="/notes">📝 Notes</a>
|
|
<a href="/todos">⚡ Action Required</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
<div class="container">
|
|
{content}
|
|
</div>
|
|
<div id="editModal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:1000;justify-content:center;align-items:center;">
|
|
<div style="background:#21262d;border:1px solid #30363d;border-radius:8px;padding:2rem;width:500px;max-width:90%;">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
|
<h3 style="color:#58a6ff;">Edit Account</h3>
|
|
<button onclick="closeEdit()" style="background:none;border:none;color:#8b949e;font-size:1.5em;cursor:pointer;">×</button>
|
|
</div>
|
|
<form method="POST" action="/accounts" id="editForm">
|
|
<input type="hidden" name="action" value="edit">
|
|
<input type="hidden" name="index" id="editIndex">
|
|
<div class="form-group">
|
|
<label>Service Name:</label>
|
|
<input type="text" name="service" id="editService" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>URL:</label>
|
|
<input type="url" name="url" id="editUrl">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Username/Email:</label>
|
|
<input type="text" name="username" id="editUsername">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Status:</label>
|
|
<select name="status" id="editStatus">
|
|
<option value="active">Active</option>
|
|
<option value="inactive">Inactive</option>
|
|
<option value="rebuilding">Rebuilding</option>
|
|
<option value="pending">Pending</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Notes:</label>
|
|
<textarea name="notes" id="editNotes" rows="3"></textarea>
|
|
</div>
|
|
<button type="submit" class="btn">Save Changes</button>
|
|
<button type="button" class="btn btn-secondary" onclick="closeEdit()">Cancel</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
var accountsData = {{}};
|
|
function toggleKey(element) {{
|
|
const key = element.getAttribute('data-key');
|
|
if (element.textContent.includes('*')) {{
|
|
element.textContent = key;
|
|
}} else {{
|
|
element.textContent = '*'.repeat(Math.min(key.length, 30));
|
|
}}
|
|
}}
|
|
function editAccount(idx) {{
|
|
var a = accountsData[idx];
|
|
if (!a) return;
|
|
document.getElementById('editIndex').value = idx;
|
|
document.getElementById('editService').value = a.service || '';
|
|
document.getElementById('editUrl').value = a.url || '';
|
|
document.getElementById('editUsername').value = a.username || '';
|
|
document.getElementById('editStatus').value = a.status || 'active';
|
|
document.getElementById('editNotes').value = a.notes || '';
|
|
document.getElementById('editModal').style.display = 'flex';
|
|
}}
|
|
function closeEdit() {{
|
|
document.getElementById('editModal').style.display = 'none';
|
|
}}
|
|
document.getElementById('editModal').addEventListener('click', function(e) {{
|
|
if (e.target === this) closeEdit();
|
|
}});
|
|
</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')
|
|
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"""
|
|
<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" style="color:{'#f85149' if pending_todos > 0 else '#40c463'};">{pending_todos}</span>
|
|
<div class="stat-label">⚡ Actions Required</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"
|
|
url = account.get('url', '')
|
|
service_name = account.get('service', 'N/A')
|
|
service_link = f'<a href="{url}" target="_blank" style="color:#58a6ff;">{service_name}</a>' if url else service_name
|
|
|
|
accounts_table += f"""
|
|
<tr id="row-{i}">
|
|
<td>{service_link}</td>
|
|
<td>{account.get('username', 'N/A')}</td>
|
|
<td><span class="{status_class}">{account.get('status', 'unknown')}</span></td>
|
|
<td style="font-size:0.85em;">{account.get('notes', '')}</td>
|
|
<td style="width:120px;white-space:nowrap;">
|
|
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px;" onclick="editAccount({i})">✏️ Edit</button>
|
|
<form method="POST" style="display:inline;">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="index" value="{i}">
|
|
<button type="submit" class="btn btn-danger" style="padding:4px 10px;font-size:12px;" onclick="return confirm('Delete {service_name}?')">🗑</button>
|
|
</form>
|
|
</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 style="width:18%;">Service</th>
|
|
<th style="width:20%;">Username/Email</th>
|
|
<th style="width:8%;">Status</th>
|
|
<th style="width:40%;">Notes</th>
|
|
<th style="width:14%;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{accounts_table}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<script>accountsData = {json.dumps(accounts)};</script>
|
|
"""
|
|
|
|
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"""
|
|
<tr>
|
|
<td>{key.get('service', 'N/A')}</td>
|
|
<td style="font-size:0.85em;color:#8b949e;">{key_type}</td>
|
|
<td><span class="masked-key" onclick="toggleKey(this)" data-key="{display_val}">{masked}</span></td>
|
|
<td><span class="{status_class}">{status}</span></td>
|
|
<td style="font-size:0.85em;">{notes}</td>
|
|
<td style="width:120px;white-space:nowrap;">
|
|
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px;" onclick="editApiKey({i})">✏️ Edit</button>
|
|
<form method="POST" style="display:inline;">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="index" value="{i}">
|
|
<button type="submit" class="btn btn-danger" style="padding:4px 10px;font-size:12px;" onclick="return confirm('Delete {key.get("service", "")}?')">🗑</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
"""
|
|
|
|
content = f"""
|
|
<div class="card">
|
|
<div class="card-header">API Key Management</div>
|
|
|
|
<form method="POST" style="margin-bottom: 2rem;">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
|
<div class="form-group">
|
|
<label>Service:</label>
|
|
<input type="text" name="service" required placeholder="e.g. Anthropic">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Type:</label>
|
|
<input type="text" name="type" placeholder="e.g. API key, OAuth token">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Key Location / Value:</label>
|
|
<input type="text" name="key_location" required placeholder="e.g. ~/.credentials/key.env">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Status:</label>
|
|
<select name="status">
|
|
<option value="active">Active</option>
|
|
<option value="missing">Missing</option>
|
|
<option value="inactive">Inactive</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Notes:</label>
|
|
<input type="text" name="notes" placeholder="Additional details">
|
|
</div>
|
|
<input type="hidden" name="action" value="add">
|
|
<button type="submit" class="btn">Add API Key</button>
|
|
</form>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:18%;">Service</th>
|
|
<th style="width:15%;">Type</th>
|
|
<th style="width:22%;">Location (click to reveal)</th>
|
|
<th style="width:8%;">Status</th>
|
|
<th style="width:25%;">Notes</th>
|
|
<th style="width:12%;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{keys_table}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<script>
|
|
var apiKeysData = {json.dumps(api_keys)};
|
|
function editApiKey(idx) {{
|
|
var k = apiKeysData[idx];
|
|
if (!k) return;
|
|
document.getElementById('editIndex').value = idx;
|
|
document.getElementById('editService').value = k.service || '';
|
|
document.getElementById('editUrl').value = k.key_location || k.key || '';
|
|
document.getElementById('editUsername').value = k.type || '';
|
|
document.getElementById('editStatus').value = k.status || 'active';
|
|
document.getElementById('editNotes').value = k.notes || '';
|
|
document.getElementById('editForm').action = '/api-keys';
|
|
document.getElementById('editModal').querySelector('h3').textContent = 'Edit API Key';
|
|
document.getElementById('editModal').style.display = 'flex';
|
|
}}
|
|
</script>
|
|
"""
|
|
|
|
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'<a href="{url}" target="_blank" style="color:#58a6ff;">{svc["name"]}</a>' if url else svc["name"]
|
|
systemd = svc.get('systemd', '') or '—'
|
|
local_rows += f"""
|
|
<tr>
|
|
<td>{name_cell}</td>
|
|
<td style="color:#8b949e;">{port or '—'}</td>
|
|
<td><span class="{status_class}">● {status}</span></td>
|
|
<td style="font-size:0.85em;color:#8b949e;">{systemd}</td>
|
|
<td style="font-size:0.85em;">{svc.get('description', '')}</td>
|
|
</tr>"""
|
|
|
|
# --- Timers ---
|
|
timer_rows = ""
|
|
for t in svc_data.get('timers', []):
|
|
timer_rows += f"""
|
|
<tr>
|
|
<td style="color:#f0f6fc;">{t['name']}</td>
|
|
<td style="color:#8b949e;">{t.get('interval', '')}</td>
|
|
<td style="font-size:0.85em;color:#8b949e;">{t.get('systemd', '')}</td>
|
|
<td style="font-size:0.85em;">{t.get('description', '')}</td>
|
|
</tr>"""
|
|
|
|
# --- Deployed Apps ---
|
|
deployed_cards = ""
|
|
for app in svc_data.get('deployed', []):
|
|
deployed_cards += f"""
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:12px;border:1px solid #30363d;border-radius:8px;margin-bottom:8px;background:#161b22;">
|
|
<div style="flex:1;">
|
|
<a href="{app['url']}" target="_blank" style="color:#58a6ff;font-weight:bold;font-size:1.1em;">{app['name']}</a>
|
|
<div style="color:#8b949e;font-size:0.85em;margin-top:4px;">{app.get('description', '')}</div>
|
|
</div>
|
|
<span style="background:#21262d;border:1px solid #30363d;border-radius:4px;padding:4px 10px;font-size:0.8em;color:#a371f7;">{app.get('platform', '')}</span>
|
|
</div>"""
|
|
|
|
if not deployed_cards:
|
|
deployed_cards = '<p style="color:#484f58;">No deployed apps yet.</p>'
|
|
|
|
# --- 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'<a href="{url}" target="_blank" style="color:#58a6ff;">{inf["name"]}</a>' if url else inf["name"]
|
|
infra_rows += f"""
|
|
<tr>
|
|
<td>{name_cell}</td>
|
|
<td style="color:#8b949e;">{host}</td>
|
|
<td style="color:#8b949e;">{port or '—'}</td>
|
|
<td><span class="{status_class}">● {status}</span></td>
|
|
<td style="font-size:0.85em;">{inf.get('description', '')}</td>
|
|
</tr>"""
|
|
|
|
total_local = len(svc_data.get('local', []))
|
|
total_timers = len(svc_data.get('timers', []))
|
|
|
|
content = f"""
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<span class="stat-number">{running_count}/{total_local}</span>
|
|
<div class="stat-label">Services Running</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number">{total_timers}</span>
|
|
<div class="stat-label">Active Timers</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number">{len(svc_data.get('deployed', []))}</span>
|
|
<div class="stat-label">Deployed Apps</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number">{len(svc_data.get('infrastructure', []))}</span>
|
|
<div class="stat-label">Infrastructure</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">🖥️ Local Services (192.168.86.45)</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:20%;">Service</th>
|
|
<th style="width:8%;">Port</th>
|
|
<th style="width:10%;">Status</th>
|
|
<th style="width:22%;">Systemd Unit</th>
|
|
<th style="width:40%;">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>{local_rows}</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">⏱️ Scheduled Timers</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:20%;">Name</th>
|
|
<th style="width:15%;">Interval</th>
|
|
<th style="width:25%;">Systemd Unit</th>
|
|
<th style="width:40%;">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>{timer_rows}</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">🚀 Deployed Applications</div>
|
|
{deployed_cards}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">🏗️ Infrastructure</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:18%;">Service</th>
|
|
<th style="width:15%;">Host</th>
|
|
<th style="width:8%;">Port</th>
|
|
<th style="width:12%;">Status</th>
|
|
<th style="width:47%;">Description</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>{infra_rows}</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div style="margin-top:1rem;">
|
|
<button onclick="location.reload()" class="btn">🔄 Refresh Status</button>
|
|
</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 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"<li>{s}</li>" for s in t['steps'])
|
|
steps = f'<div style="margin-top:8px;color:#8b949e;font-size:0.9em;"><strong>Steps:</strong><ol style="margin:4px 0 0 20px;">{steps_list}</ol></div>'
|
|
pending_html += f"""
|
|
<div class="card" style="border-left: 3px solid {pc};">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;">
|
|
<div>
|
|
<span style="font-size:1.2em;">{icon}</span>
|
|
<strong style="color:#f0f6fc;">{t.get('title','Untitled')}</strong>
|
|
<span style="color:{pc};font-size:0.8em;margin-left:8px;">● {t.get('priority','medium').upper()}</span>
|
|
</div>
|
|
<form method="POST" style="display:inline;">
|
|
<input type="hidden" name="action" value="complete">
|
|
<input type="hidden" name="index" value="{i}">
|
|
<button type="submit" class="btn" style="background:#238636;">✓ Done</button>
|
|
</form>
|
|
</div>
|
|
<div style="color:#c9d1d9;margin-top:6px;">{t.get('description','')}</div>
|
|
{steps}
|
|
<div style="color:#484f58;font-size:0.8em;margin-top:8px;">Added {t.get('created','?')} by {t.get('source','unknown')}</div>
|
|
</div>"""
|
|
|
|
done_html = ""
|
|
for t in done[:10]:
|
|
done_html += f"""
|
|
<div style="padding:8px 12px;border-bottom:1px solid #21262d;color:#484f58;">
|
|
<span style="text-decoration:line-through;">{t.get('title','')}</span>
|
|
<span style="float:right;font-size:0.8em;">completed {t.get('completed','')}</span>
|
|
</div>"""
|
|
|
|
content = f"""
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<span class="stat-number" style="color:#f85149;">{len(pending)}</span>
|
|
<div class="stat-label">Pending Actions</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-number" style="color:#40c463;">{len(done)}</span>
|
|
<div class="stat-label">Completed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header" style="color:#f85149;">⚡ Action Required</div>
|
|
{pending_html if pending_html else '<p style="color:#484f58;">Nothing pending — all clear! 🎉</p>'}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">Recently Completed</div>
|
|
{done_html if done_html else '<p style="color:#484f58;">Nothing completed yet.</p>'}
|
|
</div>
|
|
"""
|
|
|
|
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"""
|
|
<div class="card" style="border-left: 3px solid {color};">
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
|
|
<div style="flex:1;">
|
|
<strong style="color:#f0f6fc;font-size:1.1em;">{note.get('title', 'Untitled')}</strong>
|
|
<span style="color:#484f58;font-size:0.8em;margin-left:8px;">{note.get('created', '')}</span>
|
|
</div>
|
|
<div style="white-space:nowrap;">
|
|
<button class="btn btn-secondary" style="padding:4px 10px;font-size:12px;" onclick="editNote({i})">✏️</button>
|
|
<form method="POST" style="display:inline;">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="index" value="{i}">
|
|
<button type="submit" class="btn btn-danger" style="padding:4px 10px;font-size:12px;" onclick="return confirm('Delete this note?')">🗑</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div style="color:#c9d1d9;margin-top:8px;white-space:pre-wrap;line-height:1.6;">{note.get('content', '')}</div>
|
|
</div>
|
|
"""
|
|
|
|
content = f"""
|
|
<div class="card">
|
|
<div class="card-header">📝 Add Note</div>
|
|
<form method="POST">
|
|
<div style="display:grid;grid-template-columns:1fr auto;gap:1rem;">
|
|
<div class="form-group">
|
|
<label>Title:</label>
|
|
<input type="text" name="title" required placeholder="Note title">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Color:</label>
|
|
<select name="color">
|
|
<option value="#58a6ff">🔵 Blue</option>
|
|
<option value="#40c463">🟢 Green</option>
|
|
<option value="#f59e0b">🟡 Yellow</option>
|
|
<option value="#f85149">🔴 Red</option>
|
|
<option value="#a371f7">🟣 Purple</option>
|
|
<option value="#30363d">⚫ Gray</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Content:</label>
|
|
<textarea name="content" rows="4" placeholder="Write your note..."></textarea>
|
|
</div>
|
|
<input type="hidden" name="action" value="add">
|
|
<button type="submit" class="btn">Add Note</button>
|
|
</form>
|
|
</div>
|
|
|
|
{notes_html if notes_html else '<div class="card"><p style="color:#484f58;">No notes yet. Add one above.</p></div>'}
|
|
|
|
<script>
|
|
var notesData = {json.dumps(notes)};
|
|
function editNote(idx) {{
|
|
var n = notesData[idx];
|
|
if (!n) return;
|
|
document.getElementById('editIndex').value = idx;
|
|
document.getElementById('editService').value = n.title || '';
|
|
document.getElementById('editUrl').value = '';
|
|
document.getElementById('editUsername').value = '';
|
|
document.getElementById('editStatus').value = 'active';
|
|
document.getElementById('editNotes').value = n.content || '';
|
|
document.getElementById('editForm').action = '/notes';
|
|
document.getElementById('editModal').querySelector('h3').textContent = 'Edit Note';
|
|
document.getElementById('editModal').style.display = 'flex';
|
|
}}
|
|
</script>
|
|
"""
|
|
|
|
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() |