Initial commit: Clean DSS implementation

Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View File

@@ -0,0 +1,455 @@
"""
Local Browser Strategy implementation using Playwright.
Provides direct, local control over a browser for tasks like DOM inspection,
screenshotting, and running audits. This is the LOCAL mode counterpart to
RemoteBrowserStrategy which uses Shadow State pattern.
"""
import asyncio
import json
import logging
import os
import tempfile
from typing import Any, Dict, List, Optional, Type
from ..base import BrowserStrategy
# Logger setup
logger = logging.getLogger(__name__)
# URL for axe-core accessibility testing library
AXE_CORE_SCRIPT_URL = "https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.8.4/axe.min.js"
# Optional Playwright import for graceful degradation
try:
from playwright.async_api import (
Browser,
ConsoleMessage,
Error as PlaywrightError,
Page,
Playwright,
TimeoutError as PlaywrightTimeoutError,
async_playwright,
)
PLAYWRIGHT_AVAILABLE = True
except ImportError:
PLAYWRIGHT_AVAILABLE = False
# Create dummy types for type hinting when Playwright is not installed
Playwright = Type[Any]
Browser = Type[Any]
Page = Type[Any]
ConsoleMessage = Type[Any]
PlaywrightError = Exception
PlaywrightTimeoutError = Exception
class LocalBrowserStrategy(BrowserStrategy):
"""
Implements the BrowserStrategy using Playwright for local browser automation.
This strategy manages a singleton browser instance to perform actions
directly on the local machine. It is ideal for development environments
where direct access to a browser is possible.
Features:
- Browser pool pattern (reuses browser instances)
- Console log capture via CDP
- Screenshot capture (element or full page)
- DOM snapshot retrieval
- Accessibility auditing via axe-core injection
- Core Web Vitals and performance metrics
Note: This class requires Playwright to be installed.
Run `pip install "playwright"` and `playwright install chromium`.
"""
# Class-level browser pool (shared across instances)
_playwright: Optional[Playwright] = None
_browser: Optional[Browser] = None
_browser_lock = asyncio.Lock()
def __init__(self, context: Any):
"""
Initialize the LocalBrowserStrategy.
Args:
context: The DSSContext providing configuration and session info.
"""
self.context = context
self.page: Optional[Page] = None
self._console_logs: List[Any] = []
self._page_errors: List[Any] = []
if not PLAYWRIGHT_AVAILABLE:
logger.warning(
"Playwright not found. LocalBrowserStrategy will be non-functional. "
"Please run 'pip install \"playwright\"' and 'playwright install chromium'."
)
def _check_playwright(self) -> None:
"""Ensure Playwright is available, raising an error if not."""
if not PLAYWRIGHT_AVAILABLE:
raise NotImplementedError(
"Playwright is not installed. Cannot use LocalBrowserStrategy. "
"Install with: pip install playwright && playwright install chromium"
)
async def launch(self, headless: bool = True) -> None:
"""
Launch and initialize the Playwright browser instance.
This method is idempotent and ensures that a single browser instance
is shared across the application lifecycle (browser pool pattern).
Args:
headless: Whether to run browser in headless mode (default: True)
"""
self._check_playwright()
# Check if browser is already running
if LocalBrowserStrategy._browser and LocalBrowserStrategy._browser.is_connected():
logger.debug("Browser already running, reusing existing instance.")
return
async with LocalBrowserStrategy._browser_lock:
# Double-check lock to prevent race conditions
if LocalBrowserStrategy._browser and LocalBrowserStrategy._browser.is_connected():
return
logger.info("Starting Playwright...")
LocalBrowserStrategy._playwright = await async_playwright().start()
logger.info("Launching new browser instance...")
LocalBrowserStrategy._browser = await LocalBrowserStrategy._playwright.chromium.launch(
headless=headless
)
logger.info("Browser instance launched successfully.")
async def navigate(self, url: str, wait_until: str = "domcontentloaded") -> None:
"""
Navigate the browser to a specific URL.
This creates a new page context for the session, replacing any
existing page. It also sets up listeners for console logs and errors.
Args:
url: The URL to navigate to.
wait_until: The navigation event to wait for
('load', 'domcontentloaded', 'networkidle').
"""
await self.launch()
# Close existing page if any
if self.page and not self.page.is_closed():
await self.page.close()
if not LocalBrowserStrategy._browser:
raise RuntimeError("Browser is not launched. Call launch() first.")
# Create new page
self.page = await LocalBrowserStrategy._browser.new_page()
self._console_logs.clear()
self._page_errors.clear()
# Set up event listeners for log capture
self.page.on("console", self._on_console_message)
self.page.on("pageerror", self._on_page_error)
logger.info(f"Navigating to {url}...")
await self.page.goto(url, wait_until=wait_until)
logger.info(f"Navigation to {url} complete.")
def _on_console_message(self, msg: Any) -> None:
"""Handle console message events."""
self._console_logs.append(msg)
def _on_page_error(self, error: Any) -> None:
"""Handle page error events."""
self._page_errors.append(error)
async def get_console_logs(
self,
session_id: Optional[str] = None,
limit: int = 100,
level: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
Retrieve captured console logs from the current page.
Args:
session_id: Ignored in LOCAL mode (used for API compatibility).
limit: Maximum number of logs to return.
level: Filter by log level ('log', 'warn', 'error', 'info', 'debug').
Returns:
List of log entries with level, text, and location.
"""
if not self.page:
logger.warning("No active page. Returning empty logs.")
return []
logs = []
for msg in self._console_logs:
try:
log_entry = {
"level": msg.type,
"message": msg.text,
"timestamp": None, # Playwright doesn't provide timestamp directly
"category": "console",
"data": {
"location": msg.location if hasattr(msg, 'location') else None,
}
}
logs.append(log_entry)
except Exception as e:
logger.debug(f"Error processing console message: {e}")
if level:
logs = [log for log in logs if log["level"] == level]
# Return most recent logs up to limit
return logs[-limit:]
async def capture_screenshot(
self, selector: Optional[str] = None, full_page: bool = False
) -> str:
"""
Capture a screenshot of the current page or a specific element.
Args:
selector: CSS selector to capture a specific element.
If None, captures the viewport.
full_page: If True, captures the full scrollable page content.
Ignored if selector is provided.
Returns:
Path to the saved screenshot file.
Raises:
RuntimeError: If no active page is available.
"""
if not self.page or self.page.is_closed():
raise RuntimeError("No active page to capture screenshot from.")
# Generate unique filename
session_id = getattr(self.context, 'session_id', 'local')
path = os.path.join(
tempfile.gettempdir(), f"dss_screenshot_{session_id}.png"
)
try:
if selector:
element = self.page.locator(selector)
await element.screenshot(path=path, timeout=10000)
logger.info(f"Element screenshot saved to {path}")
else:
await self.page.screenshot(path=path, full_page=full_page, timeout=10000)
logger.info(f"Page screenshot saved to {path}")
return path
except Exception as e:
logger.error(f"Failed to capture screenshot: {e}")
raise
async def get_dom_snapshot(self) -> str:
"""
Get the current DOM state as an HTML string.
Returns:
String containing the outer HTML of the document.
"""
if not self.page or self.page.is_closed():
return "<!-- No active page to get DOM snapshot from. -->"
return await self.page.content()
async def get_errors(
self, severity: Optional[str] = None, limit: int = 50
) -> List[Dict[str, Any]]:
"""
Retrieve captured page errors (e.g., uncaught exceptions).
Args:
severity: Filter by severity (not yet implemented).
limit: Maximum number of errors to return.
Returns:
List of error details with name, message, and stack trace.
"""
errors = []
for err in self._page_errors:
try:
error_entry = {
"level": "error",
"category": "uncaughtError",
"message": str(err),
"data": {
"name": getattr(err, 'name', 'Error'),
"stack": getattr(err, 'stack', None),
}
}
errors.append(error_entry)
except Exception as e:
logger.debug(f"Error processing page error: {e}")
return errors[-limit:]
async def run_accessibility_audit(
self, selector: Optional[str] = None
) -> Dict[str, Any]:
"""
Run an accessibility audit on the current page using axe-core.
This injects the axe-core library into the page and runs a full
accessibility scan.
Args:
selector: A CSS selector to limit the audit to a specific element.
If None, audits the entire page.
Returns:
A dictionary containing the axe-core audit results with:
- violations: List of accessibility violations
- passes: List of passing rules
- incomplete: List of rules that need review
- inapplicable: List of rules that don't apply
Raises:
RuntimeError: If no active page is available.
"""
if not self.page or self.page.is_closed():
raise RuntimeError("No active page to run accessibility audit on.")
logger.info("Injecting axe-core library...")
await self.page.add_script_tag(url=AXE_CORE_SCRIPT_URL)
# Wait for axe to be available
await self.page.wait_for_function("typeof axe !== 'undefined'", timeout=5000)
logger.info(f"Running accessibility audit{' on ' + selector if selector else ''}...")
# Run axe with selector context if provided
if selector:
result = await self.page.evaluate(
"(selector) => axe.run(selector)", selector
)
else:
result = await self.page.evaluate("() => axe.run()")
violations_count = len(result.get('violations', []))
logger.info(f"Accessibility audit complete. Found {violations_count} violations.")
return result
async def get_performance_metrics(self) -> Dict[str, Any]:
"""
Get performance metrics, including Navigation Timing and Core Web Vitals.
Returns:
Dictionary containing:
- navigation_timing: Raw Navigation Timing API data
- core_web_vitals: FCP, LCP, and CLS metrics
Raises:
RuntimeError: If no active page is available.
"""
if not self.page or self.page.is_closed():
raise RuntimeError("No active page to get performance metrics from.")
# 1. Get Navigation Timing API metrics
timing_raw = await self.page.evaluate(
"() => JSON.stringify(window.performance.timing)"
)
nav_timing = json.loads(timing_raw)
# 2. Get Core Web Vitals via PerformanceObserver
# This script collects buffered entries and waits briefly for new ones
metrics_script = """
() => new Promise((resolve) => {
const metrics = { lcp: null, cls: 0, fcp: null, ttfb: null };
// Get TTFB from navigation timing
const navEntry = performance.getEntriesByType('navigation')[0];
if (navEntry) {
metrics.ttfb = navEntry.responseStart - navEntry.requestStart;
}
// Get FCP from paint entries
const paintEntries = performance.getEntriesByType('paint');
for (const entry of paintEntries) {
if (entry.name === 'first-contentful-paint') {
metrics.fcp = entry.startTime;
}
}
// Set up observer for LCP and CLS
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'largest-contentful-paint') {
metrics.lcp = entry.startTime;
}
if (entry.entryType === 'layout-shift' && !entry.hadRecentInput) {
metrics.cls += entry.value;
}
}
});
observer.observe({
type: 'largest-contentful-paint',
buffered: true
});
observer.observe({
type: 'layout-shift',
buffered: true
});
// Give some time for metrics to be collected
setTimeout(() => {
observer.disconnect();
resolve(metrics);
}, 500);
} catch (e) {
// PerformanceObserver may not be fully supported
resolve(metrics);
}
})
"""
core_web_vitals = await self.page.evaluate(metrics_script)
return {
"navigation_timing": nav_timing,
"core_web_vitals": core_web_vitals
}
async def close(self) -> None:
"""
Close the current page. Browser instance is kept in pool for reuse.
To fully close the browser, use close_browser() class method.
"""
if self.page and not self.page.is_closed():
await self.page.close()
self.page = None
self._console_logs.clear()
self._page_errors.clear()
logger.info("Page closed.")
@classmethod
async def close_browser(cls) -> None:
"""
Close the browser and stop the Playwright instance.
This is a class method that closes the shared browser pool.
Should be called during application shutdown.
"""
async with cls._browser_lock:
if cls._browser:
await cls._browser.close()
cls._browser = None
logger.info("Browser instance closed.")
if cls._playwright:
await cls._playwright.stop()
cls._playwright = None
logger.info("Playwright stopped.")