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 @@
Your architecture couldn't handle the load
+{{.LevelName}} - Your architecture couldn't handle the load
- Your architecture scaled successfully and met all performance targets. Well done! + {{.LevelName}} completed successfully! Your architecture scaled and met all performance targets. Well done!