feat: create v2-prep branch with comprehensive planning
Restructured project for V2 refactor: **Structure Changes:** - Moved all V1 code to orig/ folder (preserved with git mv) - Created docs/planning/ directory - Added orig/README_V1.md explaining V1 preservation **Planning Documents:** - 00_V2_MASTER_PLAN.md: Complete architecture overview - Executive summary of critical V1 issues - High-level component architecture diagrams - 5-phase implementation roadmap - Success metrics and risk mitigation - 07_TASK_BREAKDOWN.md: Atomic task breakdown - 99+ hours of detailed tasks - Every task < 2 hours (atomic) - Clear dependencies and success criteria - Organized by implementation phase **V2 Key Improvements:** - Per-exchange parsers (factory pattern) - Multi-layer strict validation - Multi-index pool cache - Background validation pipeline - Comprehensive observability **Critical Issues Addressed:** - Zero address tokens (strict validation + cache enrichment) - Parsing accuracy (protocol-specific parsers) - No audit trail (background validation channel) - Inefficient lookups (multi-index cache) - Stats disconnection (event-driven metrics) Next Steps: 1. Review planning documents 2. Begin Phase 1: Foundation (P1-001 through P1-010) 3. Implement parsers in Phase 2 4. Build cache system in Phase 3 5. Add validation pipeline in Phase 4 6. Migrate and test in Phase 5 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
549
orig/internal/monitoring/dashboard.go
Normal file
549
orig/internal/monitoring/dashboard.go
Normal file
@@ -0,0 +1,549 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/fraktal/mev-beta/internal/logger"
|
||||
)
|
||||
|
||||
// DashboardServer provides a web-based monitoring dashboard
|
||||
type DashboardServer struct {
|
||||
logger *logger.Logger
|
||||
integrityMonitor *IntegrityMonitor
|
||||
healthChecker *HealthCheckRunner
|
||||
port int
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewDashboardServer creates a new dashboard server
|
||||
func NewDashboardServer(logger *logger.Logger, integrityMonitor *IntegrityMonitor, healthChecker *HealthCheckRunner, port int) *DashboardServer {
|
||||
return &DashboardServer{
|
||||
logger: logger,
|
||||
integrityMonitor: integrityMonitor,
|
||||
healthChecker: healthChecker,
|
||||
port: port,
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the dashboard HTTP server
|
||||
func (ds *DashboardServer) Start() error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Register endpoints
|
||||
mux.HandleFunc("/", ds.handleDashboard)
|
||||
mux.HandleFunc("/api/health", ds.handleAPIHealth)
|
||||
mux.HandleFunc("/api/metrics", ds.handleAPIMetrics)
|
||||
mux.HandleFunc("/api/history", ds.handleAPIHistory)
|
||||
mux.HandleFunc("/api/alerts", ds.handleAPIAlerts)
|
||||
mux.HandleFunc("/static/", ds.handleStatic)
|
||||
|
||||
ds.server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", ds.port),
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
ds.logger.Info("Starting monitoring dashboard",
|
||||
"port", ds.port,
|
||||
"url", fmt.Sprintf("http://localhost:%d", ds.port))
|
||||
|
||||
return ds.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Stop stops the dashboard server
|
||||
func (ds *DashboardServer) Stop() error {
|
||||
if ds.server != nil {
|
||||
return ds.server.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDashboard serves the main dashboard HTML page
|
||||
func (ds *DashboardServer) handleDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
// Get current metrics and health data
|
||||
metrics := ds.integrityMonitor.GetMetrics()
|
||||
healthSummary := ds.integrityMonitor.GetHealthSummary()
|
||||
healthHistory := ds.healthChecker.GetRecentSnapshots(20)
|
||||
|
||||
// Render dashboard template
|
||||
tmpl := ds.getDashboardTemplate()
|
||||
data := struct {
|
||||
Metrics MetricsSnapshot
|
||||
HealthSummary map[string]interface{}
|
||||
HealthHistory []HealthSnapshot
|
||||
Timestamp time.Time
|
||||
}{
|
||||
Metrics: metrics,
|
||||
HealthSummary: healthSummary,
|
||||
HealthHistory: healthHistory,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
ds.logger.Error("Failed to render dashboard", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleAPIHealth provides JSON health endpoint
|
||||
func (ds *DashboardServer) handleAPIHealth(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
healthSummary := ds.integrityMonitor.GetHealthSummary()
|
||||
healthCheckerSummary := ds.healthChecker.GetHealthSummary()
|
||||
|
||||
// Combine summaries
|
||||
response := map[string]interface{}{
|
||||
"integrity_monitor": healthSummary,
|
||||
"health_checker": healthCheckerSummary,
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
ds.logger.Error("Failed to encode health response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAPIMetrics provides JSON metrics endpoint
|
||||
func (ds *DashboardServer) handleAPIMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
metrics := ds.integrityMonitor.GetMetrics()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(metrics); err != nil {
|
||||
ds.logger.Error("Failed to encode metrics response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAPIHistory provides JSON health history endpoint
|
||||
func (ds *DashboardServer) handleAPIHistory(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Get count parameter (default 20)
|
||||
countStr := r.URL.Query().Get("count")
|
||||
count := 20
|
||||
if countStr != "" {
|
||||
if c, err := strconv.Atoi(countStr); err == nil && c > 0 && c <= 100 {
|
||||
count = c
|
||||
}
|
||||
}
|
||||
|
||||
history := ds.healthChecker.GetRecentSnapshots(count)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(history); err != nil {
|
||||
ds.logger.Error("Failed to encode history response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAPIAlerts provides recent alerts for integrity and health monitoring.
|
||||
func (ds *DashboardServer) handleAPIAlerts(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
limit := 20
|
||||
if q := r.URL.Query().Get("limit"); q != "" {
|
||||
if parsed, err := strconv.Atoi(q); err == nil && parsed > 0 && parsed <= 200 {
|
||||
limit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
alerts := ds.integrityMonitor.GetRecentAlerts(limit)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"alerts": alerts,
|
||||
"count": len(alerts),
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
ds.logger.Error("Failed to encode alerts response", "error", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// handleStatic serves static assets (CSS, JS)
|
||||
func (ds *DashboardServer) handleStatic(w http.ResponseWriter, r *http.Request) {
|
||||
// For simplicity, we'll inline CSS and JS in the HTML template
|
||||
// In a production system, you'd serve actual static files
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// getDashboardTemplate returns the HTML template for the dashboard
|
||||
func (ds *DashboardServer) getDashboardTemplate() *template.Template {
|
||||
htmlTemplate := `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MEV Bot - Data Integrity Monitor</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.header .subtitle {
|
||||
opacity: 0.9;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.metric:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-radius: 50%;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
.health-excellent { background: #4CAF50; color: white; }
|
||||
.health-good { background: #8BC34A; color: white; }
|
||||
.health-fair { background: #FF9800; color: white; }
|
||||
.health-poor { background: #F44336; color: white; }
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-healthy { background: #4CAF50; }
|
||||
.status-warning { background: #FF9800; }
|
||||
.status-critical { background: #F44336; }
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.refresh-indicator {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.recovery-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.recovery-action {
|
||||
background: #f8f9fa;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recovery-action-count {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.recovery-action-label {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="container">
|
||||
<h1>🤖 MEV Bot - Data Integrity Monitor</h1>
|
||||
<p class="subtitle">Real-time monitoring of corruption detection and recovery systems</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard">
|
||||
<div class="container">
|
||||
<div class="grid">
|
||||
<!-- Health Score Card -->
|
||||
<div class="card">
|
||||
<h3>System Health</h3>
|
||||
<div class="health-score {{.HealthSummary.health_score | healthClass}}">
|
||||
{{.HealthSummary.health_score | printf "%.1f"}}
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Status</span>
|
||||
<span class="metric-value">
|
||||
<span class="status-indicator {{.HealthSummary.health_score | statusClass}}"></span>
|
||||
{{.HealthSummary.health_score | healthStatus}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Monitor Enabled</span>
|
||||
<span class="metric-value">{{if .HealthSummary.enabled}}✅ Yes{{else}}❌ No{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processing Statistics -->
|
||||
<div class="card">
|
||||
<h3>Processing Statistics</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Total Addresses</span>
|
||||
<span class="metric-value">{{.Metrics.TotalAddressesProcessed | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Corruption Detected</span>
|
||||
<span class="metric-value">{{.Metrics.CorruptAddressesDetected | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Corruption Rate</span>
|
||||
<span class="metric-value">{{.HealthSummary.corruption_rate | printf "%.4f%%"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Avg Corruption Score</span>
|
||||
<span class="metric-value">{{.Metrics.AverageCorruptionScore | printf "%.1f"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Max Corruption Score</span>
|
||||
<span class="metric-value">{{.Metrics.MaxCorruptionScore}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validation Results -->
|
||||
<div class="card">
|
||||
<h3>Validation Results</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Validation Passed</span>
|
||||
<span class="metric-value">{{.Metrics.AddressValidationPassed | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Validation Failed</span>
|
||||
<span class="metric-value">{{.Metrics.AddressValidationFailed | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Success Rate</span>
|
||||
<span class="metric-value">{{.HealthSummary.validation_success_rate | printf "%.2f%%"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Calls -->
|
||||
<div class="card">
|
||||
<h3>Contract Calls</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Successful Calls</span>
|
||||
<span class="metric-value">{{.Metrics.ContractCallsSucceeded | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Failed Calls</span>
|
||||
<span class="metric-value">{{.Metrics.ContractCallsFailed | printf "%,d"}}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Success Rate</span>
|
||||
<span class="metric-value">{{.HealthSummary.contract_call_success_rate | printf "%.2f%%"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recovery Actions -->
|
||||
<div class="chart-container">
|
||||
<h3>Recovery System Activity</h3>
|
||||
<div class="recovery-actions">
|
||||
<div class="recovery-action">
|
||||
<div class="recovery-action-count">{{.Metrics.RetryOperationsTriggered}}</div>
|
||||
<div class="recovery-action-label">Retry Operations</div>
|
||||
</div>
|
||||
<div class="recovery-action">
|
||||
<div class="recovery-action-count">{{.Metrics.FallbackOperationsUsed}}</div>
|
||||
<div class="recovery-action-label">Fallback Used</div>
|
||||
</div>
|
||||
<div class="recovery-action">
|
||||
<div class="recovery-action-count">{{.Metrics.CircuitBreakersTripped}}</div>
|
||||
<div class="recovery-action-label">Circuit Breakers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timestamp">
|
||||
Last updated: {{.Timestamp.Format "2006-01-02 15:04:05 UTC"}}
|
||||
<br>
|
||||
Auto-refresh every 30 seconds
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="refresh-indicator">🔄 Live</div>
|
||||
|
||||
<script>
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
window.location.reload();
|
||||
}, 30000);
|
||||
|
||||
// Add smooth transitions
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const cards = document.querySelectorAll('.card');
|
||||
cards.forEach((card, index) => {
|
||||
card.style.animationDelay = (index * 0.1) + 's';
|
||||
card.style.animation = 'fadeInUp 0.6s ease forwards';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// Create template with custom functions
|
||||
funcMap := template.FuncMap{
|
||||
"healthClass": func(score interface{}) string {
|
||||
s := score.(float64)
|
||||
if s >= 0.9 {
|
||||
return "health-excellent"
|
||||
} else if s >= 0.7 {
|
||||
return "health-good"
|
||||
} else if s >= 0.5 {
|
||||
return "health-fair"
|
||||
}
|
||||
return "health-poor"
|
||||
},
|
||||
"statusClass": func(score interface{}) string {
|
||||
s := score.(float64)
|
||||
if s >= 0.7 {
|
||||
return "status-healthy"
|
||||
} else if s >= 0.5 {
|
||||
return "status-warning"
|
||||
}
|
||||
return "status-critical"
|
||||
},
|
||||
"healthStatus": func(score interface{}) string {
|
||||
s := score.(float64)
|
||||
if s >= 0.9 {
|
||||
return "Excellent"
|
||||
} else if s >= 0.7 {
|
||||
return "Good"
|
||||
} else if s >= 0.5 {
|
||||
return "Fair"
|
||||
}
|
||||
return "Poor"
|
||||
},
|
||||
}
|
||||
|
||||
return template.Must(template.New("dashboard").Funcs(funcMap).Parse(htmlTemplate))
|
||||
}
|
||||
|
||||
// GetDashboardURL returns the dashboard URL
|
||||
func (ds *DashboardServer) GetDashboardURL() string {
|
||||
return fmt.Sprintf("http://localhost:%d", ds.port)
|
||||
}
|
||||
Reference in New Issue
Block a user