Full sync - all projects, memory, configs
This commit is contained in:
11
projects/control-panel/.gitignore
vendored
Normal file
11
projects/control-panel/.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
.next/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
*.db
|
||||
*.sqlite
|
||||
*.log
|
||||
venv/
|
||||
dist/
|
||||
.credentials/
|
||||
@ -4,44 +4,62 @@
|
||||
"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"
|
||||
"notes": "Primary email. Creds in .credentials/email.env",
|
||||
"created": "2026-02-08",
|
||||
"last_accessed": "2026-02-15"
|
||||
},
|
||||
{
|
||||
"service": "Polymarket",
|
||||
"url": "https://polymarket.com",
|
||||
"username": "",
|
||||
"status": "inactive",
|
||||
"notes": "Not yet registered",
|
||||
"created": "2026-02-08T09:57:59.243987",
|
||||
"last_accessed": "Never"
|
||||
"service": "CoinEx",
|
||||
"url": "https://www.coinex.com",
|
||||
"username": "D J's account (API key only)",
|
||||
"status": "active",
|
||||
"notes": "Futures-only API key. No spot/withdraw/transfer. Creds in .credentials/coinex.env",
|
||||
"created": "2026-02-20",
|
||||
"last_accessed": "2026-03-01"
|
||||
},
|
||||
{
|
||||
"service": "Feed Hunter Portal",
|
||||
"url": "http://localhost:8888",
|
||||
"service": "Gitea",
|
||||
"url": "https://git.letsgetnashty.com",
|
||||
"username": "case",
|
||||
"status": "active",
|
||||
"notes": "Private repos: workspace, knowledge-builder, coinex-dashboard, coinex-trader, coinex-ta-service",
|
||||
"created": "2026-01-30",
|
||||
"last_accessed": "2026-03-01"
|
||||
},
|
||||
{
|
||||
"service": "Coolify",
|
||||
"url": "http://192.168.86.44:8000",
|
||||
"username": "",
|
||||
"status": "active",
|
||||
"notes": "Local service",
|
||||
"created": "2026-02-08T09:57:59.243989",
|
||||
"last_accessed": "Never"
|
||||
"notes": "Deployment platform. Project: dz-studio. API token in .credentials/coolify.env",
|
||||
"created": "2026-02-26",
|
||||
"last_accessed": "2026-03-01"
|
||||
},
|
||||
{
|
||||
"service": "Chrome Debug",
|
||||
"url": "http://localhost:9222",
|
||||
"username": "",
|
||||
"service": "Telegram Bot",
|
||||
"url": "https://t.me/dzclaw_bot",
|
||||
"username": "@dzclaw_bot",
|
||||
"status": "active",
|
||||
"notes": "Browser debugging interface",
|
||||
"created": "2026-02-08T09:57:59.243991",
|
||||
"last_accessed": "Never"
|
||||
"notes": "Case's Telegram bot. D J chat ID: 6443752046",
|
||||
"created": "2026-02-08",
|
||||
"last_accessed": "2026-03-01"
|
||||
},
|
||||
{
|
||||
"service": "OpenClaw Gateway",
|
||||
"url": "http://localhost:18789",
|
||||
"username": "",
|
||||
"service": "Google Voice",
|
||||
"url": "https://voice.google.com",
|
||||
"username": "+1 (615) 933-1968",
|
||||
"status": "active",
|
||||
"notes": "OpenClaw main service",
|
||||
"created": "2026-02-08T09:57:59.243993",
|
||||
"last_accessed": "Never"
|
||||
"notes": "D J's number, shared for service registrations/verification codes",
|
||||
"created": "2026-02-08",
|
||||
"last_accessed": "2026-02-08"
|
||||
},
|
||||
{
|
||||
"service": "Craigslist",
|
||||
"url": "https://nashville.craigslist.org",
|
||||
"username": "case-lgn@protonmail.com",
|
||||
"status": "active",
|
||||
"notes": "CL_USERID=405642144, Nashville area. Passwordless login.",
|
||||
"created": "2026-02-08",
|
||||
"last_accessed": "2026-02-24"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@ -1 +1,107 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"timestamp": "2026-02-26T17:05:47.342833",
|
||||
"action": "API Key Added",
|
||||
"details": "Added key for Coolify"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-26",
|
||||
"event": "CoinEx Dashboard rebuild assigned to Glitch (Next.js 15 stack)",
|
||||
"type": "build"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-26",
|
||||
"event": "Polymarket Sports Scanner v1 built and archived (premature)",
|
||||
"type": "build"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-26",
|
||||
"event": "auto-memory-indexer cron model fixed (haiku\u2192sonnet)",
|
||||
"type": "fix"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-26",
|
||||
"event": "Orphaned tax-prep-portal (port 3002) killed",
|
||||
"type": "cleanup"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-25",
|
||||
"event": "ChromaDB re-indexed: 137 documents in openclaw-memory",
|
||||
"type": "infra"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-25",
|
||||
"event": "CoinEx trader amount bug fixed (close_avbl fallback)",
|
||||
"type": "fix"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-25",
|
||||
"event": "Knowledge Builder archived to Gitea, service stopped",
|
||||
"type": "cleanup"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-25",
|
||||
"event": "22 stale Chrome tabs closed",
|
||||
"type": "cleanup"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-25",
|
||||
"event": "Disabled: kch123-monitor, crypto-watch, anoin123-monitor timers",
|
||||
"type": "cleanup"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-24",
|
||||
"event": "Craigslist bed listing sold (Facebook Marketplace)",
|
||||
"type": "misc"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-24",
|
||||
"event": "Medium article analysis: 'Anthropic is Killing Bitcoin'",
|
||||
"type": "research"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-21",
|
||||
"event": "CoinEx trader lockfile cleared (funding fee API outage)",
|
||||
"type": "fix"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-20",
|
||||
"event": "CoinEx live trading enabled. PUMP SHORT opened.",
|
||||
"type": "trading"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-20",
|
||||
"event": "Paper trader (leverage-trader) disabled",
|
||||
"type": "cleanup"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"event": "Hawk hired. Full dev pipeline established.",
|
||||
"type": "team"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"event": "Knowledge Builder v3 refactor completed (14/14 tests)",
|
||||
"type": "build"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"event": "NotebookLM 3-panel layout built and QA'd",
|
||||
"type": "build"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-15",
|
||||
"event": "AVA KML email sent to Meg",
|
||||
"type": "misc"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-14",
|
||||
"event": "ARI completed 10 SPARK ideas research (batch 1)",
|
||||
"type": "research"
|
||||
},
|
||||
{
|
||||
"date": "2026-02-13",
|
||||
"event": "Nexus virtual office built (v1\u2192v2.4)",
|
||||
"type": "build"
|
||||
}
|
||||
]
|
||||
@ -1 +1,50 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"service": "Anthropic (Claude)",
|
||||
"key_location": "~/.openclaw/agents/main/agent/auth-profiles.json",
|
||||
"type": "OAuth token (sk-ant-oat01-...)",
|
||||
"status": "active",
|
||||
"notes": "Primary AI model. Opus for Case, Sonnet for agents.",
|
||||
"created": "2026-01-30"
|
||||
},
|
||||
{
|
||||
"service": "CoinEx",
|
||||
"key_location": "~/.openclaw/workspace/.credentials/coinex.env",
|
||||
"type": "HMAC API key (COINEX_ACCESS_ID + COINEX_SECRET_KEY)",
|
||||
"status": "active",
|
||||
"notes": "Futures-only. No spot, no withdraw, no transfer.",
|
||||
"created": "2026-02-20"
|
||||
},
|
||||
{
|
||||
"service": "Telegram Bot",
|
||||
"key_location": "~/.openclaw/workspace/.credentials/telegram-bot.env",
|
||||
"type": "Bot token (BOT_TOKEN + CHAT_ID)",
|
||||
"status": "active",
|
||||
"notes": "Used by CoinEx trader and other scripts for alerts.",
|
||||
"created": "2026-02-08"
|
||||
},
|
||||
{
|
||||
"service": "Gitea",
|
||||
"key_location": "~/.git-credentials",
|
||||
"type": "HTTP credentials",
|
||||
"status": "active",
|
||||
"notes": "User: case. For git push to git.letsgetnashty.com.",
|
||||
"created": "2026-01-30"
|
||||
},
|
||||
{
|
||||
"service": "Brave Search",
|
||||
"key_location": "NOT CONFIGURED",
|
||||
"type": "API key",
|
||||
"status": "missing",
|
||||
"notes": "web_search tool needs this. Run: openclaw configure --section web",
|
||||
"created": null
|
||||
},
|
||||
{
|
||||
"service": "Coolify",
|
||||
"key_location": "1|KfeWUGJLTpDlWiPrjsUoTRiLUBiike7eHD0qtR6347526a37",
|
||||
"type": "Coolify",
|
||||
"status": "active",
|
||||
"notes": "http://192.168.86.44:8000/",
|
||||
"created": "2026-02-26"
|
||||
}
|
||||
]
|
||||
@ -1 +1,50 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"category": "AI Models",
|
||||
"item": "Anthropic Claude API",
|
||||
"monthly_cost": 200,
|
||||
"currency": "USD",
|
||||
"notes": "~$200/mo estimated. Opus (Case) + Sonnet (agents) + cron jobs.",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"category": "Infrastructure",
|
||||
"item": "Proxmox VM (self-hosted)",
|
||||
"monthly_cost": 0,
|
||||
"currency": "USD",
|
||||
"notes": "192.168.86.45 — runs OpenClaw, all agents, web apps. D J's hardware.",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"category": "Infrastructure",
|
||||
"item": "GPU Box (self-hosted)",
|
||||
"monthly_cost": 0,
|
||||
"currency": "USD",
|
||||
"notes": "192.168.86.40 — RTX 3080+3060, Ollama, Whisper. Electricity only.",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"category": "Infrastructure",
|
||||
"item": "ChromaDB LXC (self-hosted)",
|
||||
"monthly_cost": 0,
|
||||
"currency": "USD",
|
||||
"notes": "192.168.86.25 — vector storage.",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"category": "Trading",
|
||||
"item": "CoinEx Futures Account",
|
||||
"monthly_cost": 0,
|
||||
"currency": "USD",
|
||||
"notes": "~$125 USDT balance. Live trading. Fees per trade only.",
|
||||
"status": "active"
|
||||
},
|
||||
{
|
||||
"category": "Infrastructure",
|
||||
"item": "D J's VPS",
|
||||
"monthly_cost": null,
|
||||
"currency": "USD",
|
||||
"notes": "Runs odds aggregation Postgres DB. Access pending.",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
|
||||
1
projects/control-panel/data/notes.json
Normal file
1
projects/control-panel/data/notes.json
Normal file
@ -0,0 +1 @@
|
||||
[]
|
||||
131
projects/control-panel/data/services.json
Normal file
131
projects/control-panel/data/services.json
Normal file
@ -0,0 +1,131 @@
|
||||
{
|
||||
"local": [
|
||||
{
|
||||
"name": "OpenClaw Gateway",
|
||||
"systemd": "openclaw-gateway.service",
|
||||
"port": 18789,
|
||||
"description": "Main agent gateway. WebSocket + HTTP, token auth."
|
||||
},
|
||||
{
|
||||
"name": "Nexus (Virtual Office)",
|
||||
"systemd": "nexus.service",
|
||||
"port": 3000,
|
||||
"description": "Next.js 14.2 virtual office dashboard."
|
||||
},
|
||||
{
|
||||
"name": "Case Control Panel",
|
||||
"systemd": "case-control-panel.service",
|
||||
"port": 8000,
|
||||
"description": "Admin dashboard — accounts, API keys, budget, notes."
|
||||
},
|
||||
{
|
||||
"name": "Agent Monitor",
|
||||
"systemd": "agent-monitor.service",
|
||||
"port": 8001,
|
||||
"description": "Agent team status tracking."
|
||||
},
|
||||
{
|
||||
"name": "CoinEx Dashboard",
|
||||
"systemd": "coinex-dashboard.service",
|
||||
"port": 8891,
|
||||
"description": "Futures scanner. Next.js 16 + WebSocket."
|
||||
},
|
||||
{
|
||||
"name": "CoinEx TA Service",
|
||||
"systemd": "coinex-ta-dev.service",
|
||||
"port": 8894,
|
||||
"description": "Technical analysis engine — EMA ribbons, TTM Squeeze, Stoch RSI. Redis pub/sub."
|
||||
},
|
||||
{
|
||||
"name": "Ticker",
|
||||
"systemd": "ticker.service",
|
||||
"port": 8890,
|
||||
"description": "ESPN-style todo display. Tabs: Daily, Work, Personal, Case, KIPP."
|
||||
},
|
||||
{
|
||||
"name": "Feed Hunter Portal",
|
||||
"systemd": "feed-hunter-portal.service",
|
||||
"port": 8888,
|
||||
"description": "Feed scraping pipeline portal."
|
||||
},
|
||||
{
|
||||
"name": "Chrome CDP",
|
||||
"systemd": "chrome-debug.service",
|
||||
"port": 9222,
|
||||
"description": "Headless Chrome debug port for browser automation."
|
||||
},
|
||||
{
|
||||
"name": "Redis",
|
||||
"systemd": null,
|
||||
"port": 6379,
|
||||
"description": "Local Redis server. OHLCV caching and signal pub/sub."
|
||||
}
|
||||
],
|
||||
"timers": [
|
||||
{
|
||||
"name": "CoinEx Live Trader",
|
||||
"systemd": "coinex-live-trader.timer",
|
||||
"interval": "every 5 min",
|
||||
"description": "Live futures trading bot. ~$125 USDT."
|
||||
},
|
||||
{
|
||||
"name": "Polymarket Arb Scanner",
|
||||
"systemd": "polymarket-arb-scanner.timer",
|
||||
"interval": "every 2 min",
|
||||
"description": "Sports arb scanner during market hours."
|
||||
},
|
||||
{
|
||||
"name": "Market Watch (GARP)",
|
||||
"systemd": "market-watch.timer",
|
||||
"interval": "weekdays",
|
||||
"description": "GARP paper trading scanner."
|
||||
},
|
||||
{
|
||||
"name": "Feed Monitor",
|
||||
"systemd": "feed-monitor.timer",
|
||||
"interval": "every 30 min",
|
||||
"description": "Feed scraping pipeline."
|
||||
}
|
||||
],
|
||||
"deployed": [
|
||||
{
|
||||
"name": "CoinEx Dashboard",
|
||||
"url": "https://fcs04o8w0sccookkw44sck8c.host.letsgetnashty.com",
|
||||
"platform": "Coolify",
|
||||
"app_uuid": "fcs04o8w0sccookkw44sck8c",
|
||||
"description": "Futures scanner — deployed via Coolify on 192.168.86.44"
|
||||
}
|
||||
],
|
||||
"infrastructure": [
|
||||
{
|
||||
"name": "ChromaDB",
|
||||
"host": "192.168.86.25",
|
||||
"port": 8000,
|
||||
"description": "Vector storage LXC. API v2 ONLY. Collections: openclaw-memory, skool-courses."
|
||||
},
|
||||
{
|
||||
"name": "Ollama (GPU Box)",
|
||||
"host": "192.168.86.40",
|
||||
"port": 11434,
|
||||
"description": "Debian LXC. RTX 3080 + 3060. Models: nomic-embed-text, qwen3:8b, glm-4.7-flash."
|
||||
},
|
||||
{
|
||||
"name": "Coolify Server",
|
||||
"host": "192.168.86.44",
|
||||
"port": 8000,
|
||||
"description": "Deployment platform. Project: dz-studio."
|
||||
},
|
||||
{
|
||||
"name": "KIPP VM",
|
||||
"host": "192.168.86.100",
|
||||
"port": null,
|
||||
"description": "KIPP assistant VM. Ubuntu 24.04."
|
||||
},
|
||||
{
|
||||
"name": "NAS",
|
||||
"host": "192.168.86.244",
|
||||
"port": null,
|
||||
"description": "NFS storage. Mounts at /mnt/nas on GPU box."
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,17 +1,74 @@
|
||||
[
|
||||
{
|
||||
"title": "Set up DNS for .case remote access",
|
||||
"description": "Configure DNS so feedhunter.case and admin.case resolve to 192.168.86.45 from all devices on the network.",
|
||||
"category": "dns",
|
||||
"title": "Get VPS Postgres credentials from D J",
|
||||
"description": "Need host/IP, port, read-only user/pass, schema dump. Unlocks Polymarket sports arb scanner Phase 2.",
|
||||
"category": "trading",
|
||||
"priority": "high",
|
||||
"status": "blocked",
|
||||
"source": "D J",
|
||||
"created": "2026-02-24"
|
||||
},
|
||||
{
|
||||
"title": "Configure Brave Search API key",
|
||||
"description": "web_search tool is non-functional without it. Run: openclaw configure --section web",
|
||||
"category": "infra",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"source": "Case",
|
||||
"created": "2026-02-08 10:06",
|
||||
"steps": [
|
||||
"Option A: Add entries to your router's DNS settings (if supported)",
|
||||
"Option B: Add to /etc/hosts on each device you want access from",
|
||||
"Option C: Set up a local DNS server (Pi-hole, dnsmasq, etc.)",
|
||||
"Entries needed: 192.168.86.45 feedhunter.case admin.case"
|
||||
]
|
||||
"created": "2026-02-26"
|
||||
},
|
||||
{
|
||||
"title": "Audit CoinEx futures pairs vs scanner coins",
|
||||
"description": "29 coins scanned from Binance US — verify which actually have CoinEx futures pairs. Remove phantom pairs.",
|
||||
"category": "trading",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"source": "Case",
|
||||
"created": "2026-02-25"
|
||||
},
|
||||
{
|
||||
"title": "Add exchange-side TP/SL orders on CoinEx",
|
||||
"description": "Bot only checks every 5min. Exchange-side orders would be instant safety net.",
|
||||
"category": "trading",
|
||||
"priority": "medium",
|
||||
"status": "pending",
|
||||
"source": "Case",
|
||||
"created": "2026-02-25"
|
||||
},
|
||||
{
|
||||
"title": "Reduce auto-memory-indexer frequency",
|
||||
"description": "Currently every 15min = 96 Sonnet calls/day. Consider 1h or 2h.",
|
||||
"category": "cost",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"source": "Case",
|
||||
"created": "2026-02-26"
|
||||
},
|
||||
{
|
||||
"title": "Research remaining SPARK ideas",
|
||||
"description": "61+ unresearched ideas. ARI completed batch 1 (10 ideas). Queue up batch 2.",
|
||||
"category": "strategy",
|
||||
"priority": "low",
|
||||
"status": "pending",
|
||||
"source": "Case",
|
||||
"created": "2026-02-15"
|
||||
},
|
||||
{
|
||||
"title": "CoinEx Dashboard rebuild",
|
||||
"description": "Glitch rebuilding in Next.js 15 stack. Forge handles Gitea repo + deployment. Jinx+Pixel QA after.",
|
||||
"category": "build",
|
||||
"priority": "high",
|
||||
"status": "in-progress",
|
||||
"source": "D J",
|
||||
"created": "2026-02-26"
|
||||
},
|
||||
{
|
||||
"title": "Polymarket Sports Arb Scanner",
|
||||
"description": "Phase 1 archived. Rebuild properly after VPS Postgres access. Highest priority money project.",
|
||||
"category": "trading",
|
||||
"priority": "high",
|
||||
"status": "blocked",
|
||||
"source": "D J",
|
||||
"created": "2026-02-24"
|
||||
}
|
||||
]
|
||||
|
||||
@ -48,6 +48,8 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
self.serve_activity()
|
||||
elif self.path == '/todos':
|
||||
self.serve_todos()
|
||||
elif self.path == '/notes':
|
||||
self.serve_notes()
|
||||
else:
|
||||
self.send_error(404, "Not found")
|
||||
|
||||
@ -64,6 +66,8 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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")
|
||||
|
||||
@ -217,12 +221,16 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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 {{
|
||||
@ -351,6 +359,7 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
<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>
|
||||
@ -358,15 +367,72 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
<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(key.length);
|
||||
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>"""
|
||||
@ -427,17 +493,24 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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 ""
|
||||
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>
|
||||
<td>{account.get('service', 'N/A')}</td>
|
||||
<td><a href="{account.get('url', '#')}" target="_blank">{account.get('url', 'N/A')}</a></td>
|
||||
<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>{account.get('last_accessed', 'Never')}</td>
|
||||
<td>{account.get('notes', '')}</td>
|
||||
<td>{login_btn}</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>
|
||||
"""
|
||||
|
||||
@ -476,13 +549,11 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
<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>
|
||||
<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>
|
||||
@ -490,6 +561,7 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>accountsData = {json.dumps(accounts)};</script>
|
||||
"""
|
||||
|
||||
html = self.get_base_template("Accounts", content)
|
||||
@ -502,18 +574,30 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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>'
|
||||
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>{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>
|
||||
<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>
|
||||
"""
|
||||
|
||||
@ -522,21 +606,31 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
<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 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>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">
|
||||
<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>
|
||||
@ -545,12 +639,12 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
<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>
|
||||
<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>
|
||||
@ -558,6 +652,22 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
</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)
|
||||
@ -566,52 +676,165 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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):
|
||||
services = [
|
||||
{"name": "Feed Hunter Portal", "port": 8888, "path": ""},
|
||||
{"name": "Chrome Debug", "port": 9222, "path": ""},
|
||||
{"name": "OpenClaw Gateway", "port": 18789, "path": ""},
|
||||
{"name": "Case Control Panel", "port": 8000, "path": ""},
|
||||
]
|
||||
|
||||
services_table = ""
|
||||
for service in services:
|
||||
is_healthy = self.check_service_health(service["port"])
|
||||
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://localhost:{service['port']}{service['path']}"
|
||||
link = f'<a href="{url}" target="_blank" style="color:#58a6ff;">{service["name"]}</a>'
|
||||
|
||||
services_table += f"""
|
||||
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>{link}</td>
|
||||
<td><a href="{url}" target="_blank" style="color:#c9d1d9;">{service['port']}</a></td>
|
||||
<td><span class="{status_class}">{status}</span></td>
|
||||
<td>N/A</td>
|
||||
</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">Running Services</div>
|
||||
|
||||
<div class="card-header">🖥️ Local Services (192.168.86.45)</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service Name</th>
|
||||
<th>Port</th>
|
||||
<th>Status</th>
|
||||
<th>Uptime</th>
|
||||
<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>
|
||||
{services_table}
|
||||
</tbody>
|
||||
<tbody>{local_rows}</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 1rem;">
|
||||
<button onclick="location.reload()" class="btn">Refresh Status</button>
|
||||
</div>
|
||||
</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>
|
||||
"""
|
||||
|
||||
@ -812,6 +1035,123 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
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')
|
||||
@ -831,8 +1171,10 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
|
||||
def handle_accounts_post(self, form_data):
|
||||
if form_data.get('action', [''])[0] == 'add':
|
||||
accounts = self.load_data('accounts.json')
|
||||
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],
|
||||
@ -846,27 +1188,64 @@ class ControlPanelHandler(BaseHTTPRequestHandler):
|
||||
self.save_data('accounts.json', accounts)
|
||||
self.log_activity("Account Added", f"Added {new_account['service']}")
|
||||
|
||||
# Redirect back to accounts page
|
||||
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):
|
||||
if form_data.get('action', [''])[0] == 'add':
|
||||
api_keys = self.load_data('api-keys.json')
|
||||
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],
|
||||
"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
|
||||
"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']}")
|
||||
|
||||
# Redirect back to api-keys page
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user