Add advanced tools - dashboard.py: Web dashboard on localhost:8080 - briefing.py: Morning briefing generator - scaffold.py: Project scaffolding (python, node, script, docs, experiment) - watcher.py: File change monitor - focus.py: Pomodoro-style focus timer
This commit is contained in:
187
tools/dashboard.py
Executable file
187
tools/dashboard.py
Executable file
@ -0,0 +1,187 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user