package web import ( "encoding/json" "fmt" "html/template" "io" "mime" "net/http" "net/url" "strconv" "strings" "time" "cpu-benchmark-server/lib/model" "cpu-benchmark-server/lib/store" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" ) const maxSubmissionBytes = 4 << 20 type App struct { store *store.Store templates *template.Template pageSize int } type indexPageData struct { Submissions []model.Submission QueryText string QueryCPU string QueryThread string QueryPlatform string QueryIntensity int QueryDuration int Page int TotalPages int TotalCount int ShowingFrom int ShowingTo int PrevURL string NextURL string } type jsonSubmissionEnvelope struct { Submitter string `json:"submitter"` Platform string `json:"platform"` Benchmark *model.BenchmarkResult `json:"benchmark"` Result *model.BenchmarkResult `json:"result"` Data *model.BenchmarkResult `json:"data"` } type flatSubmissionEnvelope struct { Submitter string `json:"submitter"` Platform string `json:"platform"` model.BenchmarkResult } type errorResponse struct { Error string `json:"error"` } func New(store *store.Store, pageSize int) (*App, error) { funcs := template.FuncMap{ "formatInt64": formatInt64, "formatFloat": formatFloat, "formatTime": formatTime, "modeLabel": model.ThreadModeLabel, } templates, err := template.New("index.html").Funcs(funcs).ParseFiles("templates/index.html") if err != nil { return nil, err } return &App{ store: store, templates: templates, pageSize: pageSize, }, nil } func (a *App) Routes() http.Handler { router := chi.NewRouter() router.Use(middleware.RequestID) router.Use(middleware.RealIP) router.Use(middleware.Logger) router.Use(middleware.Recoverer) router.Use(middleware.Timeout(30 * time.Second)) router.Get("/", a.handleIndex) router.Get("/healthz", a.handleHealth) router.Get("/api/search", a.handleSearch) router.Post("/api/submit", a.handleSubmit) return router } func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) { page := parsePositiveInt(r.URL.Query().Get("page"), 1) text := strings.TrimSpace(r.URL.Query().Get("text")) cpu := strings.TrimSpace(r.URL.Query().Get("cpu")) thread := strings.TrimSpace(r.URL.Query().Get("thread")) platform := strings.TrimSpace(r.URL.Query().Get("platform")) intensity := parsePositiveInt(r.URL.Query().Get("intensity"), 0) durationSecs := parsePositiveInt(r.URL.Query().Get("durationSecs"), 0) var ( submissions []model.Submission totalCount int ) if text != "" || cpu != "" || thread != "" || platform != "" || intensity > 0 || durationSecs > 0 { matches := a.store.SearchSubmissions(text, cpu, thread, platform, intensity, durationSecs) totalCount = len(matches) start, end, normalizedPage := pageBounds(page, a.pageSize, totalCount) page = normalizedPage submissions = matches[start:end] } else { var count int submissions, count = a.store.ListSubmissions(page, a.pageSize) totalCount = count _, _, page = pageBounds(page, a.pageSize, totalCount) } totalPageCount := totalPages(totalCount, a.pageSize) showingFrom, showingTo := visibleBounds(page, a.pageSize, len(submissions), totalCount) data := indexPageData{ Submissions: submissions, QueryText: text, QueryCPU: cpu, QueryThread: normalizeThreadFilter(thread), QueryPlatform: normalizePlatformFilter(platform), QueryIntensity: intensity, QueryDuration: durationSecs, Page: page, TotalPages: totalPageCount, TotalCount: totalCount, ShowingFrom: showingFrom, ShowingTo: showingTo, PrevURL: buildIndexURL(max(1, page-1), text, cpu, thread, platform, intensity, durationSecs), NextURL: buildIndexURL(max(1, min(totalPageCount, page+1)), text, cpu, thread, platform, intensity, durationSecs), } if err := a.templates.ExecuteTemplate(w, "index.html", data); err != nil { http.Error(w, fmt.Sprintf("render template: %v", err), http.StatusInternalServerError) } } func (a *App) handleHealth(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]any{ "status": "ok", "submissions": a.store.Count(), }) } func (a *App) handleSearch(w http.ResponseWriter, r *http.Request) { results := a.store.SearchSubmissions( r.URL.Query().Get("text"), r.URL.Query().Get("cpu"), r.URL.Query().Get("thread"), r.URL.Query().Get("platform"), parsePositiveInt(r.URL.Query().Get("intensity"), 0), parsePositiveInt(r.URL.Query().Get("durationSecs"), 0), ) writeJSON(w, http.StatusOK, results) } func (a *App) handleSubmit(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, maxSubmissionBytes) result, submitter, platform, err := parseSubmissionRequest(r) if err != nil { writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) return } if err := result.Validate(); err != nil { writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) return } platform = model.NormalizePlatform(platform) if err := model.ValidatePlatform(platform); err != nil { writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()}) return } submission, err := a.store.SaveSubmission(result, submitter, platform) if err != nil { writeJSON(w, http.StatusInternalServerError, errorResponse{Error: fmt.Sprintf("store submission: %v", err)}) return } writeJSON(w, http.StatusCreated, map[string]any{ "success": true, "submissionID": submission.SubmissionID, "submitter": submission.Submitter, "platform": submission.Platform, "submittedAt": submission.SubmittedAt, }) } func parseSubmissionRequest(r *http.Request) (model.BenchmarkResult, string, string, error) { contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil && contentType != "" { return model.BenchmarkResult{}, "", "", fmt.Errorf("parse content type: %w", err) } switch mediaType { case "", "application/json": return parseJSONSubmission(r) case "multipart/form-data": return parseMultipartSubmission(r) default: return model.BenchmarkResult{}, "", "", fmt.Errorf("unsupported content type %q", mediaType) } } func parseJSONSubmission(r *http.Request) (model.BenchmarkResult, string, string, error) { body, err := io.ReadAll(r.Body) if err != nil { return model.BenchmarkResult{}, "", "", fmt.Errorf("read request body: %w", err) } submitter := firstNonEmpty( r.URL.Query().Get("submitter"), r.Header.Get("X-Submitter"), ) platform := firstNonEmpty( r.URL.Query().Get("platform"), r.Header.Get("X-Platform"), ) var nested jsonSubmissionEnvelope if err := json.Unmarshal(body, &nested); err == nil { submitter = firstNonEmpty(nested.Submitter, submitter) platform = firstNonEmpty(nested.Platform, platform) for _, candidate := range []*model.BenchmarkResult{nested.Benchmark, nested.Result, nested.Data} { if candidate != nil { return *candidate, submitter, platform, nil } } } var flat flatSubmissionEnvelope if err := json.Unmarshal(body, &flat); err != nil { return model.BenchmarkResult{}, "", "", fmt.Errorf("decode benchmark JSON: %w", err) } return flat.BenchmarkResult, firstNonEmpty(flat.Submitter, submitter), firstNonEmpty(flat.Platform, platform, "windows"), nil } func parseMultipartSubmission(r *http.Request) (model.BenchmarkResult, string, string, error) { if err := r.ParseMultipartForm(maxSubmissionBytes); err != nil { return model.BenchmarkResult{}, "", "", fmt.Errorf("parse multipart form: %w", err) } payload, err := readMultipartPayload(r) if err != nil { return model.BenchmarkResult{}, "", "", err } var result model.BenchmarkResult if err := json.Unmarshal(payload, &result); err != nil { return model.BenchmarkResult{}, "", "", fmt.Errorf("decode benchmark JSON: %w", err) } return result, r.FormValue("submitter"), firstNonEmpty(r.FormValue("platform"), "windows"), nil } func readMultipartPayload(r *http.Request) ([]byte, error) { for _, field := range []string{"benchmark", "file", "benchmarkFile"} { file, _, err := r.FormFile(field) if err == nil { defer file.Close() payload, readErr := io.ReadAll(file) if readErr != nil { return nil, fmt.Errorf("read multipart benchmark file: %w", readErr) } return payload, nil } if err != http.ErrMissingFile { return nil, fmt.Errorf("read multipart benchmark file: %w", err) } } for _, field := range []string{"benchmark", "payload", "result", "data"} { if value := strings.TrimSpace(r.FormValue(field)); value != "" { return []byte(value), nil } } return nil, fmt.Errorf("multipart request must include benchmark JSON in a file field or text field named benchmark") } func parsePositiveInt(raw string, fallback int) int { if raw == "" { return fallback } value, err := strconv.Atoi(raw) if err != nil || value <= 0 { return fallback } return value } func pageBounds(page, pageSize, total int) (int, int, int) { if pageSize <= 0 { pageSize = 50 } totalPages := totalPages(total, pageSize) if totalPages == 0 { return 0, 0, 1 } if page < 1 { page = 1 } if page > totalPages { page = totalPages } start := (page - 1) * pageSize end := min(total, start+pageSize) return start, end, page } func totalPages(total, pageSize int) int { if total == 0 || pageSize <= 0 { return 0 } pages := total / pageSize if total%pageSize != 0 { pages++ } return pages } func visibleBounds(page, pageSize, visibleCount, total int) (int, int) { if total == 0 || visibleCount == 0 { return 0, 0 } from := (page-1)*pageSize + 1 to := from + visibleCount - 1 return from, to } func buildIndexURL(page int, text, cpu, thread, platform string, intensity, durationSecs int) string { if page < 1 { page = 1 } values := url.Values{} values.Set("page", strconv.Itoa(page)) if strings.TrimSpace(text) != "" { values.Set("text", text) } if strings.TrimSpace(cpu) != "" { values.Set("cpu", cpu) } if normalizedThread := normalizeThreadFilter(thread); normalizedThread != "" { values.Set("thread", normalizedThread) } if normalizedPlatform := normalizePlatformFilter(platform); normalizedPlatform != "" { values.Set("platform", normalizedPlatform) } if intensity > 0 { values.Set("intensity", strconv.Itoa(intensity)) } if durationSecs > 0 { values.Set("durationSecs", strconv.Itoa(durationSecs)) } return "/?" + values.Encode() } func writeJSON(w http.ResponseWriter, status int, payload any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) encoder := json.NewEncoder(w) encoder.SetIndent("", " ") _ = encoder.Encode(payload) } func firstNonEmpty(values ...string) string { for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } return "" } func normalizeThreadFilter(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "single": return "single" case "multi": return "multi" default: return "" } } func normalizePlatformFilter(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "windows": return "windows" case "linux": return "linux" case "macos": return "macos" default: return "" } } func formatInt64(value int64) string { negative := value < 0 if negative { value = -value } digits := strconv.FormatInt(value, 10) if len(digits) <= 3 { if negative { return "-" + digits } return digits } var builder strings.Builder if negative { builder.WriteByte('-') } pre := len(digits) % 3 if pre > 0 { builder.WriteString(digits[:pre]) if len(digits) > pre { builder.WriteByte(',') } } for i := pre; i < len(digits); i += 3 { builder.WriteString(digits[i : i+3]) if i+3 < len(digits) { builder.WriteByte(',') } } return builder.String() } func formatFloat(value float64) string { return fmt.Sprintf("%.2f", value) } func formatTime(value time.Time) string { if value.IsZero() { return "-" } return value.Format("2006-01-02 15:04:05 MST") } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b }