Files
Timmy-time-dashboard/scripts/capture_nexus_screenshots.py
Alexander Whitestone d05ce8bf86
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
feat: add automated Nexus zone screenshot capture script (#1492)
Co-authored-by: Alexander Whitestone <alexander@alexanderwhitestone.com>
Co-committed-by: Alexander Whitestone <alexander@alexanderwhitestone.com>
2026-04-13 03:05:53 +00:00

284 lines
10 KiB
Python

#!/usr/bin/env python3
"""Capture automated screenshots of all primary Nexus zones.
Part of Epic 1: Visual QA for Nexus World.
Uses Selenium + Chrome headless to navigate each dashboard zone and
save full-page screenshots for visual audit.
Usage:
# Start the dashboard first (in another terminal):
PYTHONPATH=src python3 -m uvicorn dashboard.app:app --host 127.0.0.1 --port 8000
# Then run this script:
python3 scripts/capture_nexus_screenshots.py [--base-url http://127.0.0.1:8000] [--output-dir data/nexus_screenshots]
Requirements:
pip install selenium Pillow
Chrome/Chromium browser installed
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import (
TimeoutException,
WebDriverException,
)
# ── Primary Nexus Zones ──────────────────────────────────────────────────────
# These are the main HTML page routes of the Timmy dashboard.
# API endpoints, HTMX partials, and WebSocket routes are excluded.
PRIMARY_ZONES: list[dict] = [
{"path": "/", "name": "landing", "description": "Public landing page"},
{"path": "/dashboard", "name": "dashboard", "description": "Main mission control dashboard"},
{"path": "/nexus", "name": "nexus", "description": "Nexus conversational awareness space"},
{"path": "/agents", "name": "agents", "description": "Agent management panel"},
{"path": "/briefing", "name": "briefing", "description": "Daily briefing view"},
{"path": "/calm", "name": "calm", "description": "Calm ritual space"},
{"path": "/thinking", "name": "thinking", "description": "Thinking engine visualization"},
{"path": "/memory", "name": "memory", "description": "Memory system explorer"},
{"path": "/tasks", "name": "tasks", "description": "Task management"},
{"path": "/experiments", "name": "experiments", "description": "Experiments dashboard"},
{"path": "/monitoring", "name": "monitoring", "description": "System monitoring"},
{"path": "/tower", "name": "tower", "description": "Tower world view"},
{"path": "/tools", "name": "tools", "description": "Tools overview"},
{"path": "/voice/settings", "name": "voice-settings", "description": "Voice/TTS settings"},
{"path": "/scorecards", "name": "scorecards", "description": "Agent scorecards"},
{"path": "/quests", "name": "quests", "description": "Quest tracking"},
{"path": "/spark", "name": "spark", "description": "Spark intelligence UI"},
{"path": "/self-correction/ui", "name": "self-correction", "description": "Self-correction interface"},
{"path": "/energy/report", "name": "energy", "description": "Energy management report"},
{"path": "/creative/ui", "name": "creative", "description": "Creative generation UI"},
{"path": "/mobile", "name": "mobile", "description": "Mobile companion view"},
{"path": "/db-explorer", "name": "db-explorer", "description": "Database explorer"},
{"path": "/bugs", "name": "bugs", "description": "Bug tracker"},
{"path": "/self-coding", "name": "self-coding", "description": "Self-coding interface"},
]
# ── Defaults ─────────────────────────────────────────────────────────────────
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
DEFAULT_OUTPUT_DIR = "data/nexus_screenshots"
DEFAULT_WIDTH = 1920
DEFAULT_HEIGHT = 1080
PAGE_LOAD_TIMEOUT = 15 # seconds
def create_driver(width: int, height: int) -> webdriver.Chrome:
"""Create a headless Chrome driver with the given viewport size."""
options = Options()
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument(f"--window-size={width},{height}")
options.add_argument("--hide-scrollbars")
options.add_argument("--force-device-scale-factor=1")
# Try common Chrome paths
chrome_paths = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/usr/bin/google-chrome",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
]
for path in chrome_paths:
if os.path.exists(path):
options.binary_location = path
break
driver = webdriver.Chrome(options=options)
driver.set_window_size(width, height)
return driver
def capture_zone(
driver: webdriver.Chrome,
base_url: str,
zone: dict,
output_dir: Path,
timeout: int = PAGE_LOAD_TIMEOUT,
) -> dict:
"""Capture a screenshot of a single Nexus zone.
Returns a result dict with status, file path, and metadata.
"""
url = base_url.rstrip("/") + zone["path"]
name = zone["name"]
screenshot_path = output_dir / f"{name}.png"
result = {
"zone": name,
"path": zone["path"],
"url": url,
"description": zone["description"],
"screenshot": str(screenshot_path),
"status": "pending",
"error": None,
"timestamp": None,
}
try:
print(f" Capturing {zone['path']:30s}{name}...", end=" ", flush=True)
driver.get(url)
# Wait for body to be present (basic page load)
try:
WebDriverWait(driver, timeout).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
except TimeoutException:
result["status"] = "timeout"
result["error"] = f"Page load timed out after {timeout}s"
print(f"TIMEOUT ({timeout}s)")
return result
# Additional wait for JS frameworks to render
time.sleep(2)
# Capture full-page screenshot (scroll to capture all content)
total_height = driver.execute_script("return document.body.scrollHeight")
driver.set_window_size(DEFAULT_WIDTH, max(DEFAULT_HEIGHT, total_height))
time.sleep(0.5)
# Save screenshot
output_dir.mkdir(parents=True, exist_ok=True)
driver.save_screenshot(str(screenshot_path))
# Capture page title for metadata
title = driver.title or "(no title)"
result["status"] = "ok"
result["timestamp"] = datetime.now(timezone.utc).isoformat()
result["page_title"] = title
result["file_size"] = screenshot_path.stat().st_size if screenshot_path.exists() else 0
print(f"OK — {title} ({result['file_size']:,} bytes)")
except WebDriverException as exc:
result["status"] = "error"
result["error"] = str(exc)[:200]
print(f"ERROR — {str(exc)[:100]}")
return result
def main() -> int:
parser = argparse.ArgumentParser(
description="Capture screenshots of all primary Nexus zones."
)
parser.add_argument(
"--base-url",
default=DEFAULT_BASE_URL,
help=f"Dashboard base URL (default: {DEFAULT_BASE_URL})",
)
parser.add_argument(
"--output-dir",
default=DEFAULT_OUTPUT_DIR,
help=f"Output directory for screenshots (default: {DEFAULT_OUTPUT_DIR})",
)
parser.add_argument(
"--width",
type=int,
default=DEFAULT_WIDTH,
help=f"Viewport width (default: {DEFAULT_WIDTH})",
)
parser.add_argument(
"--height",
type=int,
default=DEFAULT_HEIGHT,
help=f"Viewport height (default: {DEFAULT_HEIGHT})",
)
parser.add_argument(
"--timeout",
type=int,
default=PAGE_LOAD_TIMEOUT,
help=f"Page load timeout in seconds (default: {PAGE_LOAD_TIMEOUT})",
)
parser.add_argument(
"--zones",
nargs="*",
help="Specific zone names to capture (default: all)",
)
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Filter zones if specific ones requested
zones = PRIMARY_ZONES
if args.zones:
zones = [z for z in PRIMARY_ZONES if z["name"] in args.zones]
if not zones:
print(f"Error: No matching zones found for: {args.zones}")
print(f"Available: {[z['name'] for z in PRIMARY_ZONES]}")
return 1
print(f"Nexus Screenshot Capture")
print(f" Base URL: {args.base_url}")
print(f" Output dir: {output_dir}")
print(f" Viewport: {args.width}x{args.height}")
print(f" Zones: {len(zones)}")
print()
# Create driver
try:
driver = create_driver(args.width, args.height)
except WebDriverException as exc:
print(f"Failed to create Chrome driver: {exc}")
return 1
results = []
try:
for zone in zones:
result = capture_zone(
driver, args.base_url, zone, output_dir, timeout=args.timeout
)
results.append(result)
finally:
driver.quit()
# Write manifest
manifest = {
"captured_at": datetime.now(timezone.utc).isoformat(),
"base_url": args.base_url,
"viewport": {"width": args.width, "height": args.height},
"total_zones": len(zones),
"ok": sum(1 for r in results if r["status"] == "ok"),
"errors": sum(1 for r in results if r["status"] != "ok"),
"zones": results,
}
manifest_path = output_dir / "manifest.json"
with open(manifest_path, "w") as f:
json.dump(manifest, f, indent=2)
print()
print(f"Done! {manifest['ok']}/{manifest['total_zones']} zones captured successfully.")
print(f"Manifest: {manifest_path}")
if manifest["errors"] > 0:
print(f"\nFailed zones:")
for r in results:
if r["status"] != "ok":
print(f" {r['zone']:20s}{r['status']}: {r['error']}")
return 0 if manifest["errors"] == 0 else 1
if __name__ == "__main__":
sys.exit(main())