Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
- 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>
499 lines
18 KiB
Python
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())
|