188 lines
7.1 KiB
Python
Executable File
188 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Simple workspace dashboard - serves a status page on localhost.
|
|
"""
|
|
|
|
import http.server
|
|
import socketserver
|
|
import json
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
WORKSPACE = Path("/home/wdjones/.openclaw/workspace")
|
|
PORT = 8080
|
|
|
|
def get_workspace_stats():
|
|
"""Gather workspace statistics."""
|
|
stats = {
|
|
'generated': datetime.now().isoformat(),
|
|
'files': {'total': 0, 'by_type': {}},
|
|
'recent': [],
|
|
'tasks': {'inbox': 0, 'in_progress': 0, 'waiting': 0, 'done_today': 0},
|
|
'memory': {'today': None, 'days_logged': 0}
|
|
}
|
|
|
|
# Count files
|
|
for root, dirs, files in os.walk(WORKSPACE):
|
|
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
for fname in files:
|
|
if fname.startswith('.'):
|
|
continue
|
|
stats['files']['total'] += 1
|
|
ext = Path(fname).suffix or 'no-ext'
|
|
stats['files']['by_type'][ext] = stats['files']['by_type'].get(ext, 0) + 1
|
|
|
|
# Track recent files
|
|
fpath = Path(root) / fname
|
|
try:
|
|
mtime = datetime.fromtimestamp(fpath.stat().st_mtime)
|
|
if datetime.now() - mtime < timedelta(hours=24):
|
|
rel = str(fpath.relative_to(WORKSPACE))
|
|
stats['recent'].append({'path': rel, 'modified': mtime.isoformat()})
|
|
except:
|
|
pass
|
|
|
|
stats['recent'] = sorted(stats['recent'], key=lambda x: x['modified'], reverse=True)[:10]
|
|
|
|
# Parse tasks
|
|
tasks_file = WORKSPACE / "TASKS.md"
|
|
if tasks_file.exists():
|
|
section = None
|
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
with open(tasks_file) as f:
|
|
for line in f:
|
|
if "## Inbox" in line: section = 'inbox'
|
|
elif "## In Progress" in line: section = 'in_progress'
|
|
elif "## Waiting" in line: section = 'waiting'
|
|
elif "## Done" in line: section = 'done'
|
|
elif line.strip().startswith("- ["):
|
|
if section == 'done' and today in line:
|
|
stats['tasks']['done_today'] += 1
|
|
elif section and section != 'done':
|
|
stats['tasks'][section] += 1
|
|
|
|
# Memory stats
|
|
memory_dir = WORKSPACE / "memory"
|
|
if memory_dir.exists():
|
|
days = [f for f in memory_dir.iterdir() if f.suffix == '.md']
|
|
stats['memory']['days_logged'] = len(days)
|
|
today_file = memory_dir / f"{datetime.now().strftime('%Y-%m-%d')}.md"
|
|
if today_file.exists():
|
|
stats['memory']['today'] = len(today_file.read_text().split('\n'))
|
|
|
|
return stats
|
|
|
|
def generate_html(stats):
|
|
"""Generate dashboard HTML."""
|
|
return f"""<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Workspace Dashboard</title>
|
|
<meta charset="utf-8">
|
|
<style>
|
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #0a0a0a; color: #e0e0e0; padding: 2rem;
|
|
}}
|
|
h1 {{ color: #fff; margin-bottom: 0.5rem; }}
|
|
.subtitle {{ color: #666; margin-bottom: 2rem; }}
|
|
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }}
|
|
.card {{
|
|
background: #1a1a1a; border-radius: 12px; padding: 1.5rem;
|
|
border: 1px solid #333;
|
|
}}
|
|
.card h2 {{ color: #888; font-size: 0.875rem; text-transform: uppercase; margin-bottom: 1rem; }}
|
|
.stat {{ font-size: 2.5rem; font-weight: bold; color: #fff; }}
|
|
.stat-label {{ color: #666; font-size: 0.875rem; }}
|
|
.list {{ list-style: none; }}
|
|
.list li {{
|
|
padding: 0.5rem 0; border-bottom: 1px solid #222;
|
|
font-family: monospace; font-size: 0.875rem;
|
|
}}
|
|
.list li:last-child {{ border-bottom: none; }}
|
|
.tag {{
|
|
display: inline-block; background: #333; padding: 0.25rem 0.5rem;
|
|
border-radius: 4px; font-size: 0.75rem; margin-right: 0.5rem;
|
|
}}
|
|
.heart {{ color: #ff6b6b; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>🖤 Case Dashboard</h1>
|
|
<p class="subtitle">Generated {stats['generated'][:19]}</p>
|
|
|
|
<div class="grid">
|
|
<div class="card">
|
|
<h2>📁 Files</h2>
|
|
<div class="stat">{stats['files']['total']}</div>
|
|
<div class="stat-label">total files in workspace</div>
|
|
<div style="margin-top: 1rem;">
|
|
{''.join(f'<span class="tag">{ext}: {count}</span>' for ext, count in sorted(stats['files']['by_type'].items(), key=lambda x: -x[1])[:5])}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>✅ Tasks</h2>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
|
<div><div class="stat">{stats['tasks']['in_progress']}</div><div class="stat-label">in progress</div></div>
|
|
<div><div class="stat">{stats['tasks']['waiting']}</div><div class="stat-label">waiting</div></div>
|
|
<div><div class="stat">{stats['tasks']['inbox']}</div><div class="stat-label">inbox</div></div>
|
|
<div><div class="stat">{stats['tasks']['done_today']}</div><div class="stat-label">done today</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>📝 Memory</h2>
|
|
<div class="stat">{stats['memory']['days_logged']}</div>
|
|
<div class="stat-label">days logged</div>
|
|
<div style="margin-top: 1rem; color: #666;">
|
|
Today: {stats['memory']['today'] or 0} lines
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" style="grid-column: span 2;">
|
|
<h2>🕐 Recent Activity</h2>
|
|
<ul class="list">
|
|
{''.join(f"<li>{f['path']}</li>" for f in stats['recent'][:8]) or '<li>No recent activity</li>'}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
class DashboardHandler(http.server.SimpleHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == '/' or self.path == '/dashboard':
|
|
stats = get_workspace_stats()
|
|
html = generate_html(stats)
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'text/html')
|
|
self.end_headers()
|
|
self.wfile.write(html.encode())
|
|
elif self.path == '/api/stats':
|
|
stats = get_workspace_stats()
|
|
self.send_response(200)
|
|
self.send_header('Content-type', 'application/json')
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(stats, indent=2).encode())
|
|
else:
|
|
self.send_error(404)
|
|
|
|
def log_message(self, format, *args):
|
|
pass # Suppress logs
|
|
|
|
def main():
|
|
with socketserver.TCPServer(("", PORT), DashboardHandler) as httpd:
|
|
print(f"Dashboard running at http://localhost:{PORT}")
|
|
print("Press Ctrl+C to stop")
|
|
try:
|
|
httpd.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|