diff --git a/router/handlers/results.go b/router/handlers/results.go index c14dd04..9c653c2 100644 --- a/router/handlers/results.go +++ b/router/handlers/results.go @@ -3,20 +3,145 @@ package handlers import ( "html/template" "net/http" + "strconv" + "strings" ) type ResultHandler struct { Tmpl *template.Template } +type SuccessData struct { + LevelName string + Score int + TargetRPS int + AchievedRPS int + TargetLatency int + ActualLatency float64 + Availability float64 + Feedback []string + LevelID string +} + +type FailureData struct { + LevelName string + Reason string + TargetRPS int + AchievedRPS int + TargetLatency int + ActualLatency float64 + TargetAvail float64 + ActualAvail float64 + FailedReqs []string + LevelID string +} + func (r *ResultHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - data := struct { - Title string - }{ - Title: "Title", + // Default to success page for backward compatibility + data := SuccessData{ + LevelName: "Demo Level", + Score: 85, + TargetRPS: 10000, + AchievedRPS: 10417, + TargetLatency: 200, + ActualLatency: 87, + Availability: 99.9, + Feedback: []string{"All requirements met successfully!"}, + LevelID: "demo", } if err := r.Tmpl.ExecuteTemplate(w, "success.html", data); err != nil { http.Error(w, "Template Error", http.StatusInternalServerError) } } + +type SuccessHandler struct { + Tmpl *template.Template +} + +func (h *SuccessHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + data := SuccessData{ + LevelName: req.URL.Query().Get("level"), + Score: parseInt(req.URL.Query().Get("score"), 85), + TargetRPS: parseInt(req.URL.Query().Get("targetRPS"), 10000), + AchievedRPS: parseInt(req.URL.Query().Get("achievedRPS"), 10417), + TargetLatency: parseInt(req.URL.Query().Get("targetLatency"), 200), + ActualLatency: parseFloat(req.URL.Query().Get("actualLatency"), 87), + Availability: parseFloat(req.URL.Query().Get("availability"), 99.9), + Feedback: parseStringSlice(req.URL.Query().Get("feedback")), + LevelID: req.URL.Query().Get("levelId"), + } + + if data.LevelName == "" { + data.LevelName = "System Design Challenge" + } + if len(data.Feedback) == 0 { + data.Feedback = []string{"All requirements met successfully!"} + } + + if err := h.Tmpl.ExecuteTemplate(w, "success.html", data); err != nil { + http.Error(w, "Template Error", http.StatusInternalServerError) + } +} + +type FailureHandler struct { + Tmpl *template.Template +} + +func (h *FailureHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + data := FailureData{ + LevelName: req.URL.Query().Get("level"), + Reason: req.URL.Query().Get("reason"), + TargetRPS: parseInt(req.URL.Query().Get("targetRPS"), 10000), + AchievedRPS: parseInt(req.URL.Query().Get("achievedRPS"), 2847), + TargetLatency: parseInt(req.URL.Query().Get("targetLatency"), 200), + ActualLatency: parseFloat(req.URL.Query().Get("actualLatency"), 1247), + TargetAvail: parseFloat(req.URL.Query().Get("targetAvail"), 99.9), + ActualAvail: parseFloat(req.URL.Query().Get("actualAvail"), 87.3), + FailedReqs: parseStringSlice(req.URL.Query().Get("failedReqs")), + LevelID: req.URL.Query().Get("levelId"), + } + + if data.LevelName == "" { + data.LevelName = "System Design Challenge" + } + if data.Reason == "" { + data.Reason = "performance" + } + if len(data.FailedReqs) == 0 { + data.FailedReqs = []string{"Latency exceeded target", "Availability below requirement"} + } + + if err := h.Tmpl.ExecuteTemplate(w, "failure.html", data); err != nil { + http.Error(w, "Template Error", http.StatusInternalServerError) + } +} + +// Helper functions +func parseInt(s string, defaultValue int) int { + if s == "" { + return defaultValue + } + if val, err := strconv.Atoi(s); err == nil { + return val + } + return defaultValue +} + +func parseFloat(s string, defaultValue float64) float64 { + if s == "" { + return defaultValue + } + if val, err := strconv.ParseFloat(s, 64); err == nil { + return val + } + return defaultValue +} + +func parseStringSlice(s string) []string { + if s == "" { + return []string{} + } + // Split by pipe character for multiple values + return strings.Split(s, "|") +} diff --git a/router/handlers/simulation.go b/router/handlers/simulation.go index 2e6af2b..641bc2a 100644 --- a/router/handlers/simulation.go +++ b/router/handlers/simulation.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "systemdesigngame/internal/design" "systemdesigngame/internal/level" "systemdesigngame/internal/simulation" @@ -113,21 +114,18 @@ func (h *SimulationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - response := SimulationResponse{ - Success: true, - Metrics: metrics, - Timeline: timeline, - Passed: passed, - Score: score, - Feedback: feedback, - LevelName: levelName, + // Build redirect URL based on success/failure + var redirectURL string + if passed { + // Success page + redirectURL = buildSuccessURL(levelName, score, metrics, feedback, requestBody.LevelID) + } else { + // Failure page + redirectURL = buildFailureURL(levelName, metrics, feedback, requestBody.LevelID) } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) - return - } + // Redirect to appropriate result page + http.Redirect(w, r, redirectURL, http.StatusSeeOther) } // calculateMetrics computes key performance metrics from simulation snapshots @@ -457,3 +455,96 @@ func min(a, b int) int { } return b } + +// buildSuccessURL creates a URL for the success page with simulation results +func buildSuccessURL(levelName string, score int, metrics map[string]interface{}, feedback []string, levelID string) string { + baseURL := "/success" + + // Get level data if available + var targetRPS, targetLatency int + var targetAvail float64 + if levelID != "" { + if lvl, err := level.GetLevelByID(levelID); err == nil { + targetRPS = lvl.TargetRPS + targetLatency = lvl.MaxP95LatencyMs + targetAvail = lvl.RequiredAvailabilityPct + } + } + + // Use defaults if level not found + if targetRPS == 0 { + targetRPS = 10000 + } + if targetLatency == 0 { + targetLatency = 200 + } + if targetAvail == 0 { + targetAvail = 99.9 + } + + params := []string{ + fmt.Sprintf("level=%s", levelName), + fmt.Sprintf("score=%d", score), + fmt.Sprintf("targetRPS=%d", targetRPS), + fmt.Sprintf("achievedRPS=%d", int(metrics["throughput"].(float64))), + fmt.Sprintf("targetLatency=%d", targetLatency), + fmt.Sprintf("actualLatency=%.1f", metrics["latency_avg"].(float64)), + fmt.Sprintf("availability=%.1f", metrics["availability"].(float64)), + fmt.Sprintf("levelId=%s", levelID), + } + + // Add feedback as pipe-separated values + if len(feedback) > 0 { + feedbackStr := strings.Join(feedback, "|") + params = append(params, fmt.Sprintf("feedback=%s", feedbackStr)) + } + + return baseURL + "?" + strings.Join(params, "&") +} + +// buildFailureURL creates a URL for the failure page with simulation results +func buildFailureURL(levelName string, metrics map[string]interface{}, feedback []string, levelID string) string { + baseURL := "/failure" + + // Get level data if available + var targetRPS, targetLatency int + var targetAvail float64 + if levelID != "" { + if lvl, err := level.GetLevelByID(levelID); err == nil { + targetRPS = lvl.TargetRPS + targetLatency = lvl.MaxP95LatencyMs + targetAvail = lvl.RequiredAvailabilityPct + } + } + + // Use defaults if level not found + if targetRPS == 0 { + targetRPS = 10000 + } + if targetLatency == 0 { + targetLatency = 200 + } + if targetAvail == 0 { + targetAvail = 99.9 + } + + params := []string{ + fmt.Sprintf("level=%s", levelName), + fmt.Sprintf("reason=performance"), + fmt.Sprintf("targetRPS=%d", targetRPS), + fmt.Sprintf("achievedRPS=%d", int(metrics["throughput"].(float64))), + fmt.Sprintf("targetLatency=%d", targetLatency), + fmt.Sprintf("actualLatency=%.1f", metrics["latency_avg"].(float64)), + fmt.Sprintf("targetAvail=%.1f", targetAvail), + fmt.Sprintf("actualAvail=%.1f", metrics["availability"].(float64)), + fmt.Sprintf("levelId=%s", levelID), + } + + // Add failed requirements as pipe-separated values + if len(feedback) > 0 { + failedReqsStr := strings.Join(feedback, "|") + params = append(params, fmt.Sprintf("failedReqs=%s", failedReqsStr)) + } + + return baseURL + "?" + strings.Join(params, "&") +} diff --git a/router/router.go b/router/router.go index 7c6af19..a4afe8d 100644 --- a/router/router.go +++ b/router/router.go @@ -16,6 +16,8 @@ func SetupRoutes(tmpl *template.Template) *http.ServeMux { mux.Handle("/play/{levelId}", auth.RequireAuth(&handlers.PlayHandler{Tmpl: tmpl})) mux.Handle("/simulate", auth.RequireAuth(&handlers.SimulationHandler{})) + mux.Handle("/success", auth.RequireAuth(&handlers.SuccessHandler{Tmpl: tmpl})) + mux.Handle("/failure", auth.RequireAuth(&handlers.FailureHandler{Tmpl: tmpl})) mux.HandleFunc("/login", auth.LoginHandler) mux.HandleFunc("/callback", auth.CallbackHandler) mux.HandleFunc("/ws", handlers.Messages) diff --git a/static/commands.js b/static/commands.js index 26357d8..aae06a4 100644 --- a/static/commands.js +++ b/static/commands.js @@ -285,8 +285,15 @@ export class RunSimulationCommand extends Command { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - const result = await response.json(); + // Check if response is a redirect (status 303) + if (response.redirected || response.status === 303) { + // Follow the redirect to the result page + window.location.href = response.url; + return; + } + // Fallback: try to parse as JSON for backward compatibility + const result = await response.json(); console.log('result', result); if (result.passed && result.success) { console.log('Simulation successful:', result); diff --git a/static/failure.html b/static/failure.html index 72579a4..9d5ac89 100644 --- a/static/failure.html +++ b/static/failure.html @@ -641,29 +641,35 @@

SYSTEM OVERLOAD

-

Your architecture couldn't handle the load

+

{{.LevelName}} - Your architecture couldn't handle the load

Critical system failure detected. Your design exceeded operational limits. + {{if .FailedReqs}} +

Failed requirements: + {{range .FailedReqs}} +
• {{.}} + {{end}} + {{end}}
Target RPS
-
10,000
+
{{.TargetRPS | printf "%d"}}
Achieved RPS
-
2,847
+
{{.AchievedRPS | printf "%d"}}
Max Latency
-
200ms
+
{{.TargetLatency}}ms
Actual Latency
-
1,247ms
+
{{.ActualLatency | printf "%.0f"}}ms
@@ -672,6 +678,9 @@
[ERROR] Load balancer timeout after 30s
[ERROR] Cache miss ratio: 89%
[FATAL] System unresponsive - shutting down
+ {{range .FailedReqs}} +
[ERROR] {{.}}
+ {{end}}
@@ -710,11 +719,20 @@ // Hide recovery message after transition setTimeout(() => { - recoveryMessage.classList.remove('show'); + if (typeof recoveryMessage !== 'undefined') { + recoveryMessage.classList.remove('show'); + } }, 3000); }, 1000); }, 15000); + // Add retry function + function retryLevel() { + const levelId = '{{.LevelID}}'; + const retryUrl = levelId ? `/play/${levelId}?retry=true` : '/game?retry=true'; + window.location.href = retryUrl; + } + // Add some random glitch effects (only during failure state) function addRandomGlitch() { if (!document.body.classList.contains('recovering')) { diff --git a/static/success.html b/static/success.html index 7ea1556..1bc5ad0 100644 --- a/static/success.html +++ b/static/success.html @@ -591,25 +591,25 @@ input[name="tab"] {

🏆 Mission Accomplished

- Your architecture scaled successfully and met all performance targets. Well done! + {{.LevelName}} completed successfully! Your architecture scaled and met all performance targets. Well done!

Target RPS
-
10,000
+
{{.TargetRPS | printf "%d"}}
Achieved RPS
-
10,417
+
{{.AchievedRPS | printf "%d"}}
Max Latency
-
200ms
+
{{.TargetLatency}}ms
Actual Latency
-
87ms
+
{{.ActualLatency | printf "%.0f"}}ms
@@ -618,6 +618,9 @@ input[name="tab"] {
[INFO] Load balancer handled traffic with 0% errors
[INFO] Cache hit ratio: 97%
[SUCCESS] SLA met - all objectives achieved
+ {{range .Feedback}} +
[SUCCESS] {{.}}
+ {{end}}
@@ -636,8 +639,10 @@ input[name="tab"] { const btn = document.getElementById('retry-button'); btn.textContent = '⏳ Reloading...'; btn.disabled = true; + const levelId = '{{.LevelID}}'; + const retryUrl = levelId ? `/play/${levelId}?retry=true` : '/game?retry=true'; setTimeout(() => { - window.location.href = '/game?retry=true'; + window.location.href = retryUrl; }, 1500); }