diff --git a/tools/local_screenshot_bridge.py b/tools/local_screenshot_bridge.py index defce21..92706d6 100755 --- a/tools/local_screenshot_bridge.py +++ b/tools/local_screenshot_bridge.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse import base64 +import io import json import os import re @@ -8,10 +9,12 @@ import subprocess import sys import time import traceback +import zipfile from datetime import datetime, timezone from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path from typing import Optional +from urllib.parse import urlparse _LOG_LEVELS = {"debug": 10, "info": 20, "error": 40, "quiet": 100} @@ -585,20 +588,172 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) + def _send_html(self, status: int, html: str): + body = html.encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body) + + def _send_bytes(self, status: int, body: bytes, content_type: str, content_disposition: Optional[str] = None): + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") + if content_disposition: + self.send_header("Content-Disposition", content_disposition) + self.end_headers() + self.wfile.write(body) + + def _build_extension_zip(self) -> tuple[Optional[bytes], Optional[str]]: + project_root: Path = self.server.project_root # type: ignore[attr-defined] + ext_dir = project_root / "chrome_screenshot_ext" + if not ext_dir.exists() or not ext_dir.is_dir(): + return None, f"missing extension directory: {ext_dir}" + + buf = io.BytesIO() + with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: + for p in sorted(ext_dir.rglob("*")): + if p.is_file(): + zf.write(p, arcname=p.relative_to(project_root)) + return buf.getvalue(), None + + def _health_payload(self) -> dict: + return { + "ok": True, + "service": "local_screenshot_bridge", + "out_dir": str(self.server.out_dir), # type: ignore[attr-defined] + "has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined] + "ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined] + } + + def _home_html(self) -> str: + health = self._health_payload() + ai_text = "Enabled" if health["ai_enabled"] else "Disabled" + run_cmd_text = "Configured" if health["has_run_cmd"] else "Not configured" + return f""" + + + + + Local Screenshot Bridge + + + +
+
+

✅ Screenshot Bridge is running

+

This server accepts screenshot payloads from the Chrome extension and can generate AI response suggestions.

+ +
+
Server: Online
+
AI: {ai_text}
+
Run command: {run_cmd_text}
+
+ +

How to use

+
    +
  1. Download the extension zip from /download/chrome-extension.zip (or copy chrome_screenshot_ext manually).
  2. +
  3. Open chrome://extensions in Chrome and enable Developer mode.
  4. +
  5. Click Load unpacked and choose the chrome_screenshot_ext folder.
  6. +
  7. Open any page, run the extension, and send a screenshot to this server.
  8. +
+ +

Need JSON health output? Use /health.

+
+
+ +""" + def do_GET(self): # noqa: N802 - if self.path not in ("/", "/health"): - self._send_json(404, {"ok": False, "error": "not_found"}) + parsed = urlparse(self.path) + path = parsed.path or "/" + + if path == "/": + # Keep root JSON-compatible for extension and existing integrations. + self._send_json(200, self._health_payload()) return - self._send_json( - 200, - { - "ok": True, - "service": "local_screenshot_bridge", - "out_dir": str(self.server.out_dir), # type: ignore[attr-defined] - "has_run_cmd": bool(getattr(self.server, "run_cmd", None)), # type: ignore[attr-defined] - "ai_enabled": bool(getattr(self.server, "ai_enabled", False)), # type: ignore[attr-defined] - }, - ) + + if path in ("/guide", "/docs"): + self._send_html(200, self._home_html()) + return + + if path in ("/download/chrome-extension.zip", "/chrome_screenshot_ext.zip"): + zip_bytes, err = self._build_extension_zip() + if err: + _log(self.server, "error", f"Extension zip failed: {err}") # type: ignore[arg-type] + self._send_json(500, {"ok": False, "error": "extension_zip_failed", "detail": err}) + return + self._send_bytes( + 200, + zip_bytes or b"", + "application/zip", + 'attachment; filename="chrome_screenshot_ext.zip"', + ) + return + + if path == "/health": + self._send_json(200, self._health_payload()) + return + + self._send_json(404, {"ok": False, "error": "not_found"}) def do_OPTIONS(self): # noqa: N802 self.send_response(204)