Files
dss/dss/status/dashboard.py
Bruno Sarlo faa19beef3
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Fix import paths and remove organ metaphors
- Update all `from storage.` imports to `from dss.storage.`
- Update `from config import config` to use `dss.settings`
- Update `from auth.` imports to `from dss.auth.`
- Update health check to use `dss.mcp.handler`
- Fix SmartMerger import (merger.py not smart_merger.py)
- Fix TranslationDictionary import path
- Fix test assertion for networkx edges/links
- Remove organ/body metaphors from:
  - API server health check
  - CLI status command and help text
  - Admin UI logger and error handler

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 13:05:00 -03:00

499 lines
18 KiB
Python

"""
DSS Status Dashboard - Comprehensive system status visualization
Provides a beautiful ASCII art dashboard that aggregates data from:
- DSSManager (system info, dependencies)
- Database stats (projects, components, styles)
- ActivityLog (recent activity)
- SyncHistory (sync operations)
- QuickWinFinder (improvement opportunities)
Expert-validated design with:
- Optimized database queries using LIMIT
- Modular render methods for maintainability
- Named constants for health score weights
- Dynamic terminal width support
"""
import shutil
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, field
# Health score weight constants (expert recommendation)
HEALTH_WEIGHT_DEPENDENCIES = 0.40
HEALTH_WEIGHT_INTEGRATIONS = 0.25
HEALTH_WEIGHT_DATABASE = 0.20
HEALTH_WEIGHT_ACTIVITY = 0.15
@dataclass
class HealthMetric:
"""Individual health check result."""
name: str
status: str # ok, warning, error
value: str
category: str = "general"
details: Optional[str] = None
@dataclass
class StatusData:
"""Aggregated status data container."""
version: str = ""
healthy: bool = True
health_score: int = 0
mode: str = "unknown"
timestamp: str = ""
# Health metrics
health_metrics: List[HealthMetric] = field(default_factory=list)
# Design system metrics
projects_count: int = 0
projects_active: int = 0
components_count: int = 0
styles_count: int = 0
tokens_count: int = 0
adoption_percent: int = 0
# Activity
recent_activity: List[Dict] = field(default_factory=list)
recent_syncs: List[Dict] = field(default_factory=list)
total_activities: int = 0
# Quick wins
quick_wins_count: int = 0
quick_wins: List[str] = field(default_factory=list)
# Configuration
project_root: str = ""
database_path: str = ""
cache_dir: str = ""
figma_configured: bool = False
anthropic_configured: bool = False
# Recommendations
recommendations: List[str] = field(default_factory=list)
class StatusDashboard:
"""
Generates comprehensive DSS status dashboard.
Aggregates data from multiple sources and presents it as either:
- ASCII art dashboard for CLI (render_text())
- JSON structure for programmatic access (get_status())
"""
def __init__(self):
"""Initialize dashboard with lazy loading."""
self._data: Optional[StatusData] = None
self._settings = None
self._manager = None
def _ensure_initialized(self):
"""Lazy initialization of DSS components."""
if self._settings is None:
from dss.settings import DSSSettings, DSSManager
self._settings = DSSSettings()
self._manager = DSSManager(self._settings)
def get_status(self) -> Dict[str, Any]:
"""
Get full status as dictionary.
Returns:
Dict with all status information
"""
data = self._gather_data()
return {
"success": True,
"version": data.version,
"healthy": data.healthy,
"health_score": data.health_score,
"mode": data.mode,
"timestamp": data.timestamp,
"health": [
{"name": m.name, "status": m.status, "value": m.value, "category": m.category}
for m in data.health_metrics
],
"metrics": {
"projects": {"total": data.projects_count, "active": data.projects_active},
"components": data.components_count,
"styles": data.styles_count,
"tokens": data.tokens_count,
"adoption_percent": data.adoption_percent
},
"activity": {
"recent": data.recent_activity,
"total": data.total_activities,
"recent_syncs": data.recent_syncs
},
"quick_wins": {
"count": data.quick_wins_count,
"items": data.quick_wins
},
"configuration": {
"project_root": data.project_root,
"database": data.database_path,
"cache": data.cache_dir,
"figma_configured": data.figma_configured,
"anthropic_configured": data.anthropic_configured
},
"recommendations": data.recommendations
}
def _gather_data(self) -> StatusData:
"""Aggregate data from all sources."""
self._ensure_initialized()
data = StatusData()
# Version and timestamp
from dss import __version__
data.version = __version__
data.timestamp = datetime.now().isoformat()
# System info
info = self._manager.get_system_info()
data.project_root = info["project_root"]
data.database_path = info["database_path"]
data.cache_dir = info["cache_dir"]
data.figma_configured = info["has_figma_token"]
data.anthropic_configured = info["has_anthropic_key"]
data.mode = "Mock APIs" if info["use_mock_apis"] else "Live"
# Dependencies health
deps = self._manager.check_dependencies()
for dep, ok in deps.items():
data.health_metrics.append(HealthMetric(
name=dep,
status="ok" if ok else "error",
value="Installed" if ok else "Missing",
category="dependency"
))
# Integration health
data.health_metrics.append(HealthMetric(
name="Figma",
status="ok" if data.figma_configured else "warning",
value="Connected" if data.figma_configured else "No token",
category="integration"
))
data.health_metrics.append(HealthMetric(
name="Anthropic",
status="ok" if data.anthropic_configured else "warning",
value="Connected" if data.anthropic_configured else "No key",
category="integration"
))
# Database stats
try:
from dss.storage.json_store import get_stats, ActivityLog, SyncHistory, Projects, Components
stats = get_stats()
data.projects_count = stats.get("projects", 0)
data.components_count = stats.get("components", 0)
data.styles_count = stats.get("styles", 0)
# Database size metric
db_size = stats.get("db_size_mb", 0)
data.health_metrics.append(HealthMetric(
name="Database",
status="ok" if db_size < 100 else "warning",
value=f"{db_size} MB",
category="database"
))
# Projects
projects = Projects.list()
data.projects_active = len([p for p in projects if p.get("status") == "active"])
# Recent activity (OPTIMIZED: use limit parameter, not slice)
# Expert recommendation: avoid [:5] slicing which fetches all records
activities = ActivityLog.recent(limit=5)
data.recent_activity = [
{
"action": a.get("action", ""),
"description": a.get("description", ""),
"created_at": a.get("created_at", ""),
"category": a.get("category", "")
}
for a in activities
]
data.total_activities = ActivityLog.count()
# Recent syncs (OPTIMIZED: use limit parameter)
syncs = SyncHistory.recent(limit=3)
data.recent_syncs = [
{
"sync_type": s.get("sync_type", ""),
"status": s.get("status", ""),
"items_synced": s.get("items_synced", 0),
"started_at": s.get("started_at", "")
}
for s in syncs
]
except Exception as e:
data.health_metrics.append(HealthMetric(
name="Database",
status="error",
value=f"Error: {str(e)[:30]}",
category="database"
))
# Calculate health score
data.health_score = self._calculate_health_score(data)
data.healthy = data.health_score >= 70
# Generate recommendations
data.recommendations = self._generate_recommendations(data)
return data
def _calculate_health_score(self, data: StatusData) -> int:
"""
Calculate overall health score (0-100).
Uses weighted components:
- Dependencies: 40%
- Integrations: 25%
- Database: 20%
- Activity: 15%
"""
# Dependencies score (40%)
dep_metrics = [m for m in data.health_metrics if m.category == "dependency"]
if dep_metrics:
deps_ok = sum(1 for m in dep_metrics if m.status == "ok") / len(dep_metrics)
else:
deps_ok = 0
# Integrations score (25%)
int_metrics = [m for m in data.health_metrics if m.category == "integration"]
if int_metrics:
int_ok = sum(1 for m in int_metrics if m.status == "ok") / len(int_metrics)
else:
int_ok = 0
# Database score (20%)
db_metrics = [m for m in data.health_metrics if m.category == "database"]
if db_metrics:
db_ok = sum(1 for m in db_metrics if m.status == "ok") / len(db_metrics)
else:
db_ok = 0
# Activity score (15%) - based on having recent data
activity_ok = 1.0 if data.projects_count > 0 or data.components_count > 0 else 0.5
# Weighted score using named constants
score = (
deps_ok * HEALTH_WEIGHT_DEPENDENCIES +
int_ok * HEALTH_WEIGHT_INTEGRATIONS +
db_ok * HEALTH_WEIGHT_DATABASE +
activity_ok * HEALTH_WEIGHT_ACTIVITY
) * 100
return int(score)
def _generate_recommendations(self, data: StatusData) -> List[str]:
"""Generate actionable recommendations based on current state."""
recs = []
if not data.figma_configured:
recs.append("Set FIGMA_TOKEN environment variable to enable live Figma sync")
if not data.anthropic_configured:
recs.append("Set ANTHROPIC_API_KEY for AI-powered design analysis")
if data.projects_count == 0:
recs.append("Run dss_analyze_project to scan your first codebase")
if data.tokens_count == 0:
recs.append("Extract design tokens with dss_extract_tokens")
if data.components_count == 0 and data.projects_count > 0:
recs.append("Run dss_audit_components to discover React components")
# Check for missing dependencies
for m in data.health_metrics:
if m.category == "dependency" and m.status == "error":
recs.append(f"Install missing dependency: {m.name}")
return recs[:5] # Limit to top 5 recommendations
def render_text(self) -> str:
"""
Render status as formatted ASCII art dashboard.
Uses dynamic terminal width for responsive layout.
Returns:
Formatted string with ASCII art dashboard
"""
data = self._gather_data()
# Get terminal width (expert recommendation)
term_width = shutil.get_terminal_size((80, 24)).columns
width = min(term_width - 2, 70) # Cap at 70 for readability
lines = []
lines.append(self._render_header(data, width))
lines.append("")
lines.append(self._render_health_panel(data, width))
lines.append("")
lines.append(self._render_metrics_panel(data, width))
lines.append("")
lines.append(self._render_activity_panel(data, width))
lines.append("")
lines.append(self._render_recommendations_panel(data, width))
return "\n".join(lines)
def _render_header(self, data: StatusData, width: int) -> str:
"""Render the header section."""
health_icon = "\u2705" if data.healthy else "\u26a0\ufe0f"
health_text = f"{health_icon} Healthy ({data.health_score}%)" if data.healthy else f"{health_icon} Issues ({data.health_score}%)"
lines = []
lines.append("\u2554" + "\u2550" * width + "\u2557")
lines.append("\u2551" + "\U0001f3a8 DSS Status Dashboard".center(width) + "\u2551")
lines.append("\u2560" + "\u2550" * width + "\u2563")
version_line = f" Version: {data.version:<20} Status: {health_text}"
lines.append("\u2551" + version_line.ljust(width) + "\u2551")
mode_line = f" Mode: {data.mode:<25} Time: {data.timestamp[:19]}"
lines.append("\u2551" + mode_line.ljust(width) + "\u2551")
lines.append("\u255a" + "\u2550" * width + "\u255d")
return "\n".join(lines)
def _render_health_panel(self, data: StatusData, width: int) -> str:
"""Render the health panel section."""
lines = []
lines.append("\u250c" + "\u2500" * width + "\u2510")
lines.append("\u2502" + " \U0001f3e5 SYSTEM HEALTH".ljust(width) + "\u2502")
lines.append("\u251c" + "\u2500" * width + "\u2524")
# Dependencies
deps = [m for m in data.health_metrics if m.category == "dependency"]
deps_line = " Dependencies: "
for d in deps:
icon = "\u2705" if d.status == "ok" else "\u274c"
deps_line += f"{icon} {d.name} "
lines.append("\u2502" + deps_line[:width].ljust(width) + "\u2502")
# Integrations
ints = [m for m in data.health_metrics if m.category == "integration"]
int_line = " Integrations: "
for i in ints:
icon = "\u2705" if i.status == "ok" else "\u26a0\ufe0f"
int_line += f"{icon} {i.name} ({i.value}) "
lines.append("\u2502" + int_line[:width].ljust(width) + "\u2502")
# Database
db = next((m for m in data.health_metrics if m.category == "database"), None)
if db:
db_icon = "\u2705" if db.status == "ok" else "\u26a0\ufe0f"
db_line = f" Database: {db_icon} {db.value}"
lines.append("\u2502" + db_line.ljust(width) + "\u2502")
lines.append("\u2514" + "\u2500" * width + "\u2518")
return "\n".join(lines)
def _render_metrics_panel(self, data: StatusData, width: int) -> str:
"""Render the design system metrics panel."""
lines = []
lines.append("\u250c" + "\u2500" * width + "\u2510")
lines.append("\u2502" + " \U0001f4ca DESIGN SYSTEM METRICS".ljust(width) + "\u2502")
lines.append("\u251c" + "\u2500" * width + "\u2524")
lines.append("\u2502" + f" Projects: {data.projects_count} total ({data.projects_active} active)".ljust(width) + "\u2502")
lines.append("\u2502" + f" Components: {data.components_count} tracked".ljust(width) + "\u2502")
lines.append("\u2502" + f" Styles: {data.styles_count} defined".ljust(width) + "\u2502")
# Tokens
if data.tokens_count > 0:
lines.append("\u2502" + f" Tokens: {data.tokens_count} extracted".ljust(width) + "\u2502")
else:
lines.append("\u2502" + " Tokens: -- (run dss_extract_tokens)".ljust(width) + "\u2502")
# Adoption progress bar
if data.adoption_percent > 0:
bar_width = 30
filled = int(bar_width * data.adoption_percent / 100)
bar = "\u2588" * filled + "\u2591" * (bar_width - filled)
lines.append("\u2502" + f" Adoption: [{bar}] {data.adoption_percent}%".ljust(width) + "\u2502")
lines.append("\u2514" + "\u2500" * width + "\u2518")
return "\n".join(lines)
def _render_activity_panel(self, data: StatusData, width: int) -> str:
"""Render the recent activity panel."""
lines = []
lines.append("\u250c" + "\u2500" * width + "\u2510")
lines.append("\u2502" + " \U0001f551 RECENT ACTIVITY".ljust(width) + "\u2502")
lines.append("\u251c" + "\u2500" * width + "\u2524")
if data.recent_activity:
for activity in data.recent_activity[:3]:
action = activity.get("action", "Unknown")
desc = activity.get("description", "")[:40]
created = activity.get("created_at", "")[:10]
line = f" \u2022 {created} | {action}: {desc}"
lines.append("\u2502" + line[:width].ljust(width) + "\u2502")
else:
lines.append("\u2502" + " No recent activity".ljust(width) + "\u2502")
lines.append("\u2502" + "".ljust(width) + "\u2502")
lines.append("\u2502" + f" Total activities: {data.total_activities}".ljust(width) + "\u2502")
lines.append("\u2514" + "\u2500" * width + "\u2518")
return "\n".join(lines)
def _render_recommendations_panel(self, data: StatusData, width: int) -> str:
"""Render the recommendations panel."""
if not data.recommendations:
return ""
lines = []
lines.append("\u250c" + "\u2500" * width + "\u2510")
lines.append("\u2502" + " \U0001f4a1 RECOMMENDED NEXT STEPS".ljust(width) + "\u2502")
lines.append("\u251c" + "\u2500" * width + "\u2524")
for i, rec in enumerate(data.recommendations[:4], 1):
line = f" {i}. {rec}"
# Truncate if too long
if len(line) > width - 1:
line = line[:width-4] + "..."
lines.append("\u2502" + line.ljust(width) + "\u2502")
lines.append("\u2514" + "\u2500" * width + "\u2518")
return "\n".join(lines)
# Convenience function
def get_dashboard() -> StatusDashboard:
"""Get a StatusDashboard instance."""
return StatusDashboard()
if __name__ == "__main__":
# CLI test
import sys
dashboard = StatusDashboard()
if len(sys.argv) > 1 and sys.argv[1] == "--json":
import json
print(json.dumps(dashboard.get_status(), indent=2))
else:
print(dashboard.render_text())