2 Commits
v0.1 ... v0.12

Author SHA1 Message Date
64e3c1966d feat(search): support platform and benchmark config filters
Some checks failed
Build and Publish Docker Image / deploy (push) Failing after 1m2s
Add platform handling to submissions and persist a normalized value (`windows`, `linux`, `macos`) with a default of `windows` when omitted.

Extend search/index filtering to support `thread`, `platform`, `intensity`, and `durationSecs` alongside existing text/CPU token matching, and wire these params through request parsing, page data, and navigation URLs.

Update API/README docs and examples to reflect the new submission inputs and search capabilities so users can run more precise queries.feat(search): support platform and benchmark config filters

Add platform handling to submissions and persist a normalized value (`windows`, `linux`, `macos`) with a default of `windows` when omitted.

Extend search/index filtering to support `thread`, `platform`, `intensity`, and `durationSecs` alongside existing text/CPU token matching, and wire these params through request parsing, page data, and navigation URLs.

Update API/README docs and examples to reflect the new submission inputs and search capabilities so users can run more precise queries.
2026-04-15 20:23:37 +03:00
f21728e1ef ci(workflow): install Docker before Docker image build
Some checks failed
Build and Publish Docker Image / deploy (push) Failing after 56s
Replace the Docker availability check with an installation step in the
docker publish workflow. This ensures image builds can run on runners
where Docker is not preinstalled.ci(workflow): install Docker before Docker image build

Replace the Docker availability check with an installation step in the
docker publish workflow. This ensures image builds can run on runners
where Docker is not preinstalled.
2026-04-15 19:57:11 +03:00
10 changed files with 311 additions and 62 deletions

View File

@@ -25,8 +25,8 @@ jobs:
- name: Run Tests
run: go test ./...
- name: Verify Docker Availability
run: docker version
- name: Install Docker
run: curl -fsSL https://get.docker.com | sh
- name: Build Docker Image
run: |

View File

@@ -5,7 +5,7 @@ Production-oriented Go web application for ingesting CPU benchmark results, stor
## Features
- `POST /api/submit` accepts either `application/json` or `multipart/form-data`.
- `GET /api/search` performs case-insensitive token matching against submitter/general fields and CPU brand strings.
- `GET /api/search` performs case-insensitive token matching against submitter/general fields and CPU brand strings, with explicit thread-mode, platform, intensity, and duration filters.
- `GET /` renders the latest submissions with search and pagination.
- BadgerDB stores each submission under a reverse-timestamp key so native iteration returns newest records first.
- A startup-loaded in-memory search index prevents full DB deserialization for every query.
@@ -17,6 +17,7 @@ Each stored submission contains:
- `submissionID`: server-generated UUID
- `submitter`: defaults to `Anonymous` if omitted
- `platform`: normalized to `windows`, `linux`, or `macos`; defaults to `windows` if omitted
- `submittedAt`: server-side storage timestamp
- Benchmark payload fields:
- `config`
@@ -84,15 +85,21 @@ Accepted content types:
JSON requests support either:
1. A wrapper envelope with `submitter` and nested `benchmark`
1. A wrapper envelope with `submitter`, `platform`, and nested `benchmark`
2. A raw benchmark JSON body, with optional submitter provided via:
- query string `?submitter=...`
- header `X-Submitter`
- top-level `submitter` field
- query string `?platform=...`
- header `X-Platform`
- top-level `platform` field
`platform` is stored for every submission. Supported values are `windows`, `linux`, and `macos`. If the client does not send it, the server defaults to `windows`.
Multipart requests support:
- `submitter` text field
- `platform` text field
- benchmark JSON as one of these file fields: `benchmark`, `file`, `benchmarkFile`
- or benchmark JSON as text fields: `benchmark`, `payload`, `result`, `data`
@@ -102,6 +109,7 @@ Example success response:
{
"success": true,
"submissionID": "8f19d442-1be0-4989-97cf-3f8ee6b61548",
"platform": "windows",
"submitter": "Workstation-Lab-A",
"submittedAt": "2026-04-15T15:45:41.327225Z"
}
@@ -113,11 +121,15 @@ Query parameters:
- `text`: token-matches submitter and general searchable fields
- `cpu`: token-matches `cpuInfo.brandString`
- `thread`: `single` or `multi`
- `platform`: `windows`, `linux`, or `macos`
- `intensity`: exact match on `config.intensity`
- `durationSecs`: exact match on `config.durationSecs`
Example:
```bash
curl "http://localhost:8080/api/search?text=intel&cpu=13700"
curl "http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&intensity=10&durationSecs=30"
```
### `GET /`
@@ -127,13 +139,17 @@ Query parameters:
- `page`
- `text`
- `cpu`
- `thread`
- `platform`
- `intensity`
- `durationSecs`
Examples:
```text
http://localhost:8080/
http://localhost:8080/?page=2
http://localhost:8080/?text=anonymous&cpu=ryzen
http://localhost:8080/?text=anonymous&cpu=ryzen&thread=multi&platform=windows&intensity=10&durationSecs=20
```
## Request Examples
@@ -149,6 +165,7 @@ You can also submit one of the provided sample payloads directly:
```bash
curl -X POST "http://localhost:8080/api/submit?submitter=Example-CLI" \
-H "Content-Type: application/json" \
-H "X-Platform: windows" \
--data-binary @example_jsons/5800X/cpu-bench-result.json
```
@@ -157,6 +174,7 @@ Or as multipart:
```bash
curl -X POST "http://localhost:8080/api/submit" \
-F "submitter=Example-Multipart" \
-F "platform=linux" \
-F "benchmark=@example_jsons/i9/cpu-bench-result.json;type=application/json"
```
@@ -168,7 +186,7 @@ curl -X POST "http://localhost:8080/api/submit" \
- canonical submission payload
- normalized general search text
- normalized CPU brand text
- Searches scan the in-memory ordered slice rather than reopening and deserializing Badger values for every request.
- Searches scan the in-memory ordered slice rather than reopening and deserializing Badger values for every request, and apply explicit platform, thread-mode, intensity, and duration filters in memory.
## Docker

View File

@@ -12,8 +12,5 @@ services:
PAGE_SIZE: "50"
SHUTDOWN_TIMEOUT: 10s
volumes:
- badger-data:/data
- ./badger-data:/data
restart: unless-stopped
volumes:
badger-data:

View File

@@ -1,9 +1,9 @@
GET http://localhost:8080/api/search?text=intel&cpu=13700
GET http://localhost:8080/api/search?text=intel&cpu=13700&thread=multi&platform=windows&intensity=10&durationSecs=30
###
GET http://localhost:8080/api/search?text=anonymous
GET http://localhost:8080/api/search?text=anonymous&thread=single&platform=linux&intensity=1&durationSecs=10
###
GET http://localhost:8080/?page=1&text=lab&cpu=ryzen
GET http://localhost:8080/?page=1&text=lab&cpu=ryzen&thread=multi&platform=windows&intensity=10&durationSecs=20

View File

@@ -3,6 +3,7 @@ Content-Type: application/json
{
"submitter": "Workstation-Lab-A",
"platform": "windows",
"benchmark": {
"config": {
"durationSecs": 20,

View File

@@ -6,6 +6,10 @@ Content-Disposition: form-data; name="submitter"
Intel-Test-Rig
--BenchBoundary
Content-Disposition: form-data; name="platform"
linux
--BenchBoundary
Content-Disposition: form-data; name="benchmark"; filename="cpu-bench-result.json"
Content-Type: application/json

View File

@@ -60,6 +60,7 @@ type BenchmarkResult struct {
type Submission struct {
SubmissionID string `json:"submissionID"`
Submitter string `json:"submitter"`
Platform string `json:"platform"`
SubmittedAt time.Time `json:"submittedAt"`
BenchmarkResult
}
@@ -111,6 +112,27 @@ func NormalizeSubmitter(submitter string) string {
return submitter
}
func NormalizePlatform(platform string) string {
switch strings.ToLower(strings.TrimSpace(platform)) {
case "", "windows":
return "windows"
case "linux":
return "linux"
case "macos":
return "macos"
default:
return ""
}
}
func ValidatePlatform(platform string) error {
if NormalizePlatform(platform) == "" {
return errors.New("platform must be one of windows, linux, or macos")
}
return nil
}
func ThreadModeLabel(multiCore bool) string {
if multiCore {
return "Multi-threaded"

View File

@@ -21,6 +21,10 @@ type indexedSubmission struct {
submission *model.Submission
searchText string
cpuText string
platform string
threadMode string
intensity int
duration int
}
type Store struct {
@@ -61,10 +65,11 @@ func (s *Store) Count() int {
return len(s.orderedIDs)
}
func (s *Store) SaveSubmission(result model.BenchmarkResult, submitter string) (*model.Submission, error) {
func (s *Store) SaveSubmission(result model.BenchmarkResult, submitter, platform string) (*model.Submission, error) {
submission := &model.Submission{
SubmissionID: uuid.NewString(),
Submitter: model.NormalizeSubmitter(submitter),
Platform: model.NormalizePlatform(platform),
SubmittedAt: time.Now().UTC(),
BenchmarkResult: result,
}
@@ -111,9 +116,11 @@ func (s *Store) ListSubmissions(page, pageSize int) ([]model.Submission, int) {
return results, total
}
func (s *Store) SearchSubmissions(text, cpu string) []model.Submission {
func (s *Store) SearchSubmissions(text, cpu, thread, platform string, intensity, durationSecs int) []model.Submission {
queryText := normalizeSearchText(text)
cpuText := normalizeSearchText(cpu)
thread = normalizeThreadFilter(thread)
platform = normalizePlatformFilter(platform)
s.mu.RLock()
defer s.mu.RUnlock()
@@ -133,6 +140,22 @@ func (s *Store) SearchSubmissions(text, cpu string) []model.Submission {
continue
}
if thread != "" && record.threadMode != thread {
continue
}
if platform != "" && record.platform != platform {
continue
}
if intensity > 0 && record.intensity != intensity {
continue
}
if durationSecs > 0 && record.duration != durationSecs {
continue
}
results = append(results, *model.CloneSubmission(record.submission))
}
@@ -173,6 +196,10 @@ func newIndexedSubmission(submission *model.Submission) *indexedSubmission {
submission: model.CloneSubmission(submission),
searchText: buildSearchText(submission),
cpuText: normalizeSearchText(submission.CPUInfo.BrandString),
platform: model.NormalizePlatform(submission.Platform),
threadMode: normalizeThreadMode(submission.Config.MultiCore),
intensity: submission.Config.Intensity,
duration: submission.Config.DurationSecs,
}
}
@@ -180,10 +207,12 @@ func buildSearchText(submission *model.Submission) string {
parts := []string{
submission.SubmissionID,
submission.Submitter,
submission.Platform,
submission.CPUInfo.BrandString,
submission.CPUInfo.VendorID,
model.ThreadModeLabel(submission.Config.MultiCore),
strconv.Itoa(submission.Config.DurationSecs),
strconv.Itoa(submission.Config.Intensity),
strconv.Itoa(submission.CPUInfo.PhysicalCores),
strconv.Itoa(submission.CPUInfo.LogicalCores),
strconv.FormatInt(submission.Duration, 10),
@@ -231,6 +260,42 @@ func matchesSearch(target, query string) bool {
return true
}
func normalizeThreadFilter(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all":
return ""
case "single":
return "single"
case "multi":
return "multi"
default:
return ""
}
}
func normalizeThreadMode(multiCore bool) string {
if multiCore {
return "multi"
}
return "single"
}
func normalizePlatformFilter(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all":
return ""
case "windows":
return "windows"
case "linux":
return "linux"
case "macos":
return "macos"
default:
return ""
}
}
func pageBounds(page, pageSize, total int) (int, int, int) {
if pageSize <= 0 {
pageSize = 50

View File

@@ -28,20 +28,25 @@ type App struct {
}
type indexPageData struct {
Submissions []model.Submission
QueryText string
QueryCPU string
Page int
TotalPages int
TotalCount int
ShowingFrom int
ShowingTo int
PrevURL string
NextURL string
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"`
@@ -49,6 +54,7 @@ type jsonSubmissionEnvelope struct {
type flatSubmissionEnvelope struct {
Submitter string `json:"submitter"`
Platform string `json:"platform"`
model.BenchmarkResult
}
@@ -96,14 +102,18 @@ 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 != "" {
matches := a.store.SearchSubmissions(text, cpu)
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
@@ -119,16 +129,20 @@ func (a *App) handleIndex(w http.ResponseWriter, r *http.Request) {
showingFrom, showingTo := visibleBounds(page, a.pageSize, len(submissions), totalCount)
data := indexPageData{
Submissions: submissions,
QueryText: text,
QueryCPU: cpu,
Page: page,
TotalPages: totalPageCount,
TotalCount: totalCount,
ShowingFrom: showingFrom,
ShowingTo: showingTo,
PrevURL: buildIndexURL(max(1, page-1), text, cpu),
NextURL: buildIndexURL(max(1, min(totalPageCount, page+1)), text, cpu),
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 {
@@ -144,14 +158,21 @@ func (a *App) handleHealth(w http.ResponseWriter, _ *http.Request) {
}
func (a *App) handleSearch(w http.ResponseWriter, r *http.Request) {
results := a.store.SearchSubmissions(r.URL.Query().Get("text"), r.URL.Query().Get("cpu"))
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, err := parseSubmissionRequest(r)
result, submitter, platform, err := parseSubmissionRequest(r)
if err != nil {
writeJSON(w, http.StatusBadRequest, errorResponse{Error: err.Error()})
return
@@ -162,7 +183,13 @@ func (a *App) handleSubmit(w http.ResponseWriter, r *http.Request) {
return
}
submission, err := a.store.SaveSubmission(result, submitter)
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
@@ -172,15 +199,16 @@ func (a *App) handleSubmit(w http.ResponseWriter, r *http.Request) {
"success": true,
"submissionID": submission.SubmissionID,
"submitter": submission.Submitter,
"platform": submission.Platform,
"submittedAt": submission.SubmittedAt,
})
}
func parseSubmissionRequest(r *http.Request) (model.BenchmarkResult, string, error) {
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)
return model.BenchmarkResult{}, "", "", fmt.Errorf("parse content type: %w", err)
}
switch mediaType {
@@ -189,55 +217,60 @@ func parseSubmissionRequest(r *http.Request) (model.BenchmarkResult, string, err
case "multipart/form-data":
return parseMultipartSubmission(r)
default:
return model.BenchmarkResult{}, "", fmt.Errorf("unsupported content type %q", mediaType)
return model.BenchmarkResult{}, "", "", fmt.Errorf("unsupported content type %q", mediaType)
}
}
func parseJSONSubmission(r *http.Request) (model.BenchmarkResult, string, error) {
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)
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, 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 model.BenchmarkResult{}, "", "", fmt.Errorf("decode benchmark JSON: %w", err)
}
return flat.BenchmarkResult, firstNonEmpty(flat.Submitter, submitter), nil
return flat.BenchmarkResult, firstNonEmpty(flat.Submitter, submitter), firstNonEmpty(flat.Platform, platform, "windows"), nil
}
func parseMultipartSubmission(r *http.Request) (model.BenchmarkResult, string, error) {
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)
return model.BenchmarkResult{}, "", "", fmt.Errorf("parse multipart form: %w", err)
}
payload, err := readMultipartPayload(r)
if err != nil {
return model.BenchmarkResult{}, "", err
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 model.BenchmarkResult{}, "", "", fmt.Errorf("decode benchmark JSON: %w", err)
}
return result, r.FormValue("submitter"), nil
return result, r.FormValue("submitter"), firstNonEmpty(r.FormValue("platform"), "windows"), nil
}
func readMultipartPayload(r *http.Request) ([]byte, error) {
@@ -326,7 +359,7 @@ func visibleBounds(page, pageSize, visibleCount, total int) (int, int) {
return from, to
}
func buildIndexURL(page int, text, cpu string) string {
func buildIndexURL(page int, text, cpu, thread, platform string, intensity, durationSecs int) string {
if page < 1 {
page = 1
}
@@ -339,6 +372,18 @@ func buildIndexURL(page int, text, cpu string) string {
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()
}
@@ -361,6 +406,30 @@ func firstNonEmpty(values ...string) string {
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 {

View File

@@ -12,7 +12,7 @@
<p class="text-sm uppercase tracking-[0.35em] text-cyan-300">CPU Benchmark Platform</p>
<div class="mt-4 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div class="max-w-3xl">
<h1 class="text-4xl font-bold tracking-tight">Submission browser and API for local CPU benchmark runs</h1>
<h1 class="text-4xl font-bold tracking-tight">Simple CPU Benchmark Server</h1>
<p class="mt-3 text-sm text-slate-300">
Browse recent benchmark submissions, filter by submitter or CPU brand, and inspect per-core throughput details.
</p>
@@ -31,7 +31,7 @@
<main class="mx-auto max-w-7xl px-6 py-8">
<section class="-mt-12 rounded-3xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-300/30">
<form method="get" action="/" class="grid gap-4 lg:grid-cols-[2fr_2fr_auto]">
<form method="get" action="/" class="grid gap-4 lg:grid-cols-[2fr_2fr_1fr_1fr_1fr_1fr_auto]">
<label class="block">
<span class="mb-2 block text-sm font-medium text-slate-700">General search</span>
<input
@@ -52,6 +52,51 @@
class="w-full rounded-xl border-slate-300 bg-slate-50 text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
>
</label>
<label class="block">
<span class="mb-2 block text-sm font-medium text-slate-700">Thread mode</span>
<select
name="thread"
class="w-full rounded-xl border-slate-300 bg-slate-50 text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
>
<option value="" {{ if eq .QueryThread "" }}selected{{ end }}>All</option>
<option value="single" {{ if eq .QueryThread "single" }}selected{{ end }}>Single-threaded</option>
<option value="multi" {{ if eq .QueryThread "multi" }}selected{{ end }}>Multi-threaded</option>
</select>
</label>
<label class="block">
<span class="mb-2 block text-sm font-medium text-slate-700">Platform</span>
<select
name="platform"
class="w-full rounded-xl border-slate-300 bg-slate-50 text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
>
<option value="" {{ if eq .QueryPlatform "" }}selected{{ end }}>All</option>
<option value="windows" {{ if eq .QueryPlatform "windows" }}selected{{ end }}>Windows</option>
<option value="linux" {{ if eq .QueryPlatform "linux" }}selected{{ end }}>Linux</option>
<option value="macos" {{ if eq .QueryPlatform "macos" }}selected{{ end }}>macOS</option>
</select>
</label>
<label class="block">
<span class="mb-2 block text-sm font-medium text-slate-700">Intensity</span>
<input
type="number"
min="1"
name="intensity"
value="{{ if gt .QueryIntensity 0 }}{{ .QueryIntensity }}{{ end }}"
placeholder="10"
class="w-full rounded-xl border-slate-300 bg-slate-50 text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
>
</label>
<label class="block">
<span class="mb-2 block text-sm font-medium text-slate-700">Duration (s)</span>
<input
type="number"
min="1"
name="durationSecs"
value="{{ if gt .QueryDuration 0 }}{{ .QueryDuration }}{{ end }}"
placeholder="20"
class="w-full rounded-xl border-slate-300 bg-slate-50 text-sm shadow-sm focus:border-cyan-500 focus:ring-cyan-500"
>
</label>
<div class="flex gap-3 lg:justify-end">
<button
type="submit"
@@ -79,7 +124,7 @@
{{ range .Submissions }}
<details class="group overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-sm transition hover:shadow-md">
<summary class="list-none cursor-pointer">
<div class="grid gap-4 p-6 lg:grid-cols-[1.5fr_2fr_repeat(3,minmax(0,1fr))] lg:items-center">
<div class="grid gap-4 p-6 lg:grid-cols-[1.2fr_2fr_repeat(6,minmax(0,1fr))] lg:items-center">
<div>
<p class="text-xs uppercase tracking-[0.25em] text-slate-400">Submitter</p>
<p class="mt-2 text-lg font-semibold text-slate-900">{{ .Submitter }}</p>
@@ -102,11 +147,23 @@
<p class="text-xs uppercase tracking-[0.25em] text-slate-400">Mode</p>
<p class="mt-2 inline-flex rounded-full bg-slate-100 px-3 py-1 text-sm font-medium text-slate-700">{{ modeLabel .Config.MultiCore }}</p>
</div>
<div>
<p class="text-xs uppercase tracking-[0.25em] text-slate-400">Platform</p>
<p class="mt-2 inline-flex rounded-full bg-cyan-50 px-3 py-1 text-sm font-medium text-cyan-800">{{ .Platform }}</p>
</div>
<div>
<p class="text-xs uppercase tracking-[0.25em] text-slate-400">Intensity</p>
<p class="mt-2 text-xl font-semibold text-slate-900">{{ .Config.Intensity }}</p>
</div>
<div>
<p class="text-xs uppercase tracking-[0.25em] text-slate-400">Run Time</p>
<p class="mt-2 text-xl font-semibold text-slate-900">{{ .Config.DurationSecs }}s</p>
</div>
</div>
</summary>
<div class="border-t border-slate-100 bg-slate-50 px-6 py-6">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-6">
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Started</p>
<p class="mt-2 text-sm font-medium text-slate-800">{{ formatTime .StartedAt }}</p>
@@ -120,11 +177,27 @@
<p class="mt-2 text-sm font-medium text-slate-800">{{ formatInt64 .TotalOps }}</p>
</div>
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Benchmark config</p>
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Duration</p>
<p class="mt-2 text-sm font-medium text-slate-800">
{{ .Config.DurationSecs }}s • intensity {{ .Config.Intensity }} • coreFilter {{ .Config.CoreFilter }}
{{ .Config.DurationSecs }} seconds
</p>
</div>
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Intensity</p>
<p class="mt-2 text-sm font-medium text-slate-800">
{{ .Config.Intensity }}
</p>
</div>
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Core Filter</p>
<p class="mt-2 text-sm font-medium text-slate-800">
{{ .Config.CoreFilter }}
</p>
</div>
<div class="rounded-2xl bg-white p-4 shadow-sm">
<p class="text-xs uppercase tracking-[0.2em] text-slate-400">Platform</p>
<p class="mt-2 text-sm font-medium text-slate-800">{{ .Platform }}</p>
</div>
</div>
<div class="mt-6 overflow-x-auto rounded-2xl border border-slate-200 bg-white">