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:
2026-01-30 23:19:19 -06:00
parent 7123c4ccd2
commit b579251e90
5 changed files with 757 additions and 0 deletions

187
tools/dashboard.py Executable file
View 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()