Phase 3: results table home page, run detail with owner notes/visibility, my-runs page

This commit is contained in:
PeterChrz 2026-09-16 22:41:12 -04:00
parent 6af567b207
commit 5ec35c367b
Signed by untrusted user who does not match committer: pch
GPG key ID: 8F0826ECF7302C63
22 changed files with 717 additions and 81 deletions

View file

@ -16,7 +16,7 @@ class VerifyEmailController extends Controller
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
return redirect()->intended(route('home', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
@ -26,6 +26,6 @@ public function __invoke(EmailVerificationRequest $request): RedirectResponse
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
return redirect()->intended(route('home', absolute: false).'?verified=1');
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Livewire;
trait IncludesSorting
{
public ?string $sortBy = null;
public string $sortDirection = 'asc';
public function sort(string $column): void
{
if ($this->sortBy === $column) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortBy = $column;
$this->sortDirection = 'desc';
}
}
}

View file

@ -84,7 +84,19 @@ public function isOwnedBy(?User $user): bool
*/
public function scopeVisibleTo(Builder $query, ?User $user): Builder
{
return $query->where(function (Builder $q) use ($user) {
return $query->where(self::visibleClause($user));
}
/**
* The visibility predicate as a standalone closure, so joined queries
* (e.g. the flat results table) can apply the exact same rule without
* duplicating the logic.
*
* @return \Closure(Builder): Builder
*/
public static function visibleClause(?User $user): \Closure
{
return function (Builder $q) use ($user) {
$q->where('visibility', self::VISIBILITY_PUBLIC);
if ($user !== null) {
@ -92,6 +104,6 @@ public function scopeVisibleTo(Builder $query, ?User $user): Builder
->where('visibility', self::VISIBILITY_PRIVATE)
->where('user_id', $user->id));
}
});
};
}
}

View file

@ -7,12 +7,12 @@
<flux:header container class="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />
<a href="{{ route('dashboard') }}" class="ml-2 mr-5 flex items-center space-x-2 lg:ml-0" wire:navigate>
<a href="{{ route('home') }}" class="ml-2 mr-5 flex items-center space-x-2 lg:ml-0" wire:navigate>
<x-app-logo class="size-8" href="#"></x-app-logo>
</a>
<flux:navbar class="-mb-px max-lg:hidden">
<flux:navbar.item icon="layout-grid" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>
<flux:navbar.item icon="layout-grid" href="{{ route('home') }}" :current="request()->routeIs('home')" wire:navigate>
Dashboard
</flux:navbar.item>
</flux:navbar>
@ -44,10 +44,13 @@ class="h-10 max-lg:hidden [&>div>svg]:size-5"
</flux:navbar>
<!-- Desktop User Menu -->
<flux:dropdown position="top" align="end">
@guest
<flux:button href="{{ route('login') }}" size="sm" wire:navigate>{{ __('Log in') }}</flux:button>
@else
<flux:dropdown position="top" align="end">
<flux:profile
class="cursor-pointer"
:initials="auth()->user()->initials()"
:initials="auth()->user()?->initials() ?? ''"
/>
<flux:menu>
@ -58,13 +61,13 @@ class="cursor-pointer"
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
{{ auth()->user()?->initials() ?? '' }}
</span>
</span>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
<span class="truncate font-semibold">{{ auth()->user()?->name ?? '' }}</span>
<span class="truncate text-xs">{{ auth()->user()?->email ?? '' }}</span>
</div>
</div>
</div>
@ -86,19 +89,20 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
</form>
</flux:menu>
</flux:dropdown>
@endguest
</flux:header>
<!-- Mobile Menu -->
<flux:sidebar stashable sticky class="lg:hidden border-r border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="ml-1 flex items-center space-x-2" wire:navigate>
<a href="{{ route('home') }}" class="ml-1 flex items-center space-x-2" wire:navigate>
<x-app-logo class="size-8" href="#"></x-app-logo>
</a>
<flux:navlist variant="outline">
<flux:navlist.group heading="Platform">
<flux:navlist.item icon="layout-grid" href="{{ route('dashboard') }}" :current="request()->routeIs('dashboard')" wire:navigate>
<flux:navlist.item icon="layout-grid" href="{{ route('home') }}" :current="request()->routeIs('home')" wire:navigate>
Dashboard
</flux:navlist.item>
</flux:navlist.group>

View file

@ -7,13 +7,17 @@
<flux:sidebar sticky stashable class="border-r border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
<flux:sidebar.toggle class="lg:hidden" icon="x-mark" />
<a href="{{ route('dashboard') }}" class="mr-5 flex items-center space-x-2" wire:navigate>
<a href="{{ route('home') }}" class="mr-5 flex items-center space-x-2" wire:navigate>
<x-app-logo class="size-8" href="#"></x-app-logo>
</a>
<flux:navlist variant="outline">
<flux:navlist.group heading="Platform" class="grid">
<flux:navlist.item icon="home" :href="route('dashboard')" :current="request()->routeIs('dashboard')" wire:navigate>Dashboard</flux:navlist.item>
<flux:navlist.item icon="home" :href="route('home')" :current="request()->routeIs('home')" wire:navigate>Results</flux:navlist.item>
@auth
<flux:navlist.item icon="lock-closed" :href="route('my-runs')" :current="request()->routeIs('my-runs')" wire:navigate>My Runs</flux:navlist.item>
<flux:navlist.item icon="layout-grid" :href="route('home')" :current="request()->routeIs('home')" wire:navigate>Dashboard</flux:navlist.item>
@endauth
</flux:navlist.group>
</flux:navlist>
@ -30,10 +34,13 @@
</flux:navlist>
<!-- Desktop User Menu -->
<flux:dropdown position="bottom" align="start">
@guest
<flux:button href="{{ route('login') }}" size="sm" wire:navigate>{{ __('Log in') }}</flux:button>
@else
<flux:dropdown position="bottom" align="start">
<flux:profile
:name="auth()->user()->name"
:initials="auth()->user()->initials()"
:name="auth()->user()?->name ?? ''"
:initials="auth()->user()?->initials() ?? ''"
icon-trailing="chevrons-up-down"
/>
@ -45,13 +52,13 @@
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
{{ auth()->user()?->initials() ?? '' }}
</span>
</span>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
<span class="truncate font-semibold">{{ auth()->user()?->name ?? '' }}</span>
<span class="truncate text-xs">{{ auth()->user()?->email ?? '' }}</span>
</div>
</div>
</div>
@ -73,6 +80,7 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
</form>
</flux:menu>
</flux:dropdown>
@endguest
</flux:sidebar>
<!-- Mobile User Menu -->
@ -81,9 +89,12 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
<flux:spacer />
<flux:dropdown position="top" align="end">
@guest
<flux:button href="{{ route('login') }}" size="sm" wire:navigate>{{ __('Log in') }}</flux:button>
@else
<flux:dropdown position="top" align="end">
<flux:profile
:initials="auth()->user()->initials()"
:initials="auth()->user()?->initials() ?? ''"
icon-trailing="chevron-down"
/>
@ -95,13 +106,13 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
<span
class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white"
>
{{ auth()->user()->initials() }}
{{ auth()->user()?->initials() ?? '' }}
</span>
</span>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-semibold">{{ auth()->user()->name }}</span>
<span class="truncate text-xs">{{ auth()->user()->email }}</span>
<span class="truncate font-semibold">{{ auth()->user()?->name ?? '' }}</span>
<span class="truncate text-xs">{{ auth()->user()?->email ?? '' }}</span>
</div>
</div>
</div>
@ -123,6 +134,7 @@ class="flex h-full w-full items-center justify-center rounded-lg bg-neutral-200
</form>
</flux:menu>
</flux:dropdown>
@endguest
</flux:header>
{{ $slot }}

View file

@ -1,18 +0,0 @@
<x-layouts.app>
<div class="flex h-full w-full flex-1 flex-col gap-4 rounded-xl">
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
<div class="relative aspect-video overflow-hidden rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
<div class="relative h-full flex-1 rounded-xl border border-neutral-200 dark:border-neutral-700">
<x-placeholder-pattern class="absolute inset-0 size-full stroke-gray-900/20 dark:stroke-neutral-100/20" />
</div>
</div>
</x-layouts.app>

View file

@ -28,7 +28,7 @@ public function confirmPassword(): void
session(['auth.password_confirmed_at' => time()]);
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
$this->redirectIntended(default: route('home', absolute: false), navigate: true);
}
}; ?>

View file

@ -39,7 +39,7 @@ public function login(): void
RateLimiter::clear($this->throttleKey());
Session::regenerate();
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
$this->redirectIntended(default: route('home', absolute: false), navigate: true);
}
/**

View file

@ -31,7 +31,7 @@ public function register(): void
Auth::login($user);
$this->redirect(route('dashboard', absolute: false), navigate: true);
$this->redirect(route('home', absolute: false), navigate: true);
}
}; ?>

View file

@ -13,7 +13,7 @@
public function sendVerification(): void
{
if (Auth::user()->hasVerifiedEmail()) {
$this->redirectIntended(default: route('dashboard', absolute: false), navigate: true);
$this->redirectIntended(default: route('home', absolute: false), navigate: true);
return;
}

View file

@ -0,0 +1,125 @@
<?php
use App\Models\BenchmarkRun;
use Flux\Flux;
use Illuminate\Pagination\LengthAwarePaginator;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Component;
use Livewire\WithPagination;
new #[Layout('components.layouts.app')] class extends Component {
use WithPagination;
public array $filters = [
'search' => null,
];
public function toggleVisibility(int $id): void
{
$run = BenchmarkRun::where('user_id', auth()->id())->findOrFail($id);
$run->fill([
'visibility' => $run->visibility === BenchmarkRun::VISIBILITY_PUBLIC
? BenchmarkRun::VISIBILITY_PRIVATE
: BenchmarkRun::VISIBILITY_PUBLIC,
])->save();
Flux::toast(variant: 'success', text: __('Visibility updated.'));
}
public function resetFilters(): void
{
$this->filters = ['search' => null];
$this->resetPage();
}
#[Computed]
public function runs(): LengthAwarePaginator
{
return BenchmarkRun::query()
->where('user_id', auth()->id())
->withCount([
'results',
'results as passed_count' => fn ($q) => $q->where('ok', true),
])
->when($this->filters['search'], fn ($q, $search) => $q->where(function ($q) use ($search) {
$q->where('run_id', 'like', "%{$search}%")
->orWhere('model_id', 'like', "%{$search}%")
->orWhere('label', 'like', "%{$search}%");
}))
->orderByDesc('ran_at')
->orderByDesc('id')
->paginate(25);
}
};
?>
<div class="flex flex-col gap-6">
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('My Runs') }}</flux:heading>
<flux:subheading>{{ __('Runs attributed to you. Toggle visibility or add notes from a run page.') }}</flux:subheading>
</div>
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('All results') }}</flux:button>
</div>
<flux:input
wire:model.live.debounce.300ms="filters.search"
:label="__('Search')"
:placeholder="__('Run, model or label…')"
size="sm"
class="w-72"
clearable
/>
@if ($this->runs->count() > 0)
<flux:table :paginate="$this->runs">
<flux:table.columns>
<flux:table.column>{{ __('Date') }}</flux:table.column>
<flux:table.column>{{ __('Run') }}</flux:table.column>
<flux:table.column>{{ __('Model') }}</flux:table.column>
<flux:table.column>{{ __('Suite') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Tasks') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Passed') }}</flux:table.column>
<flux:table.column>{{ __('Visibility') }}</flux:table.column>
<flux:table.column></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->runs as $run)
<flux:table.row :key="$run->id">
<flux:table.cell>{{ optional($run->ran_at)->format('Y-m-d') }}</flux:table.cell>
<flux:table.cell>
<a href="{{ route('runs.show', $run->run_id) }}" class="font-mono text-xs hover:underline" wire:navigate>{{ $run->run_id }}</a>
</flux:table.cell>
<flux:table.cell>{{ $run->model_id }}</flux:table.cell>
<flux:table.cell><flux:badge size="xs">{{ $run->suite }}</flux:badge></flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ $run->results_count }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ $run->passed_count }}</flux:table.cell>
<flux:table.cell>
<flux:badge size="xs" color="{{ $run->visibility === 'public' ? 'emerald' : 'rose' }}">
{{ __($run->visibility) }}
</flux:badge>
</flux:table.cell>
<flux:table.cell>
<flux:button
size="xs"
variant="ghost"
wire:click="toggleVisibility({{ $run->id }})"
icon="{{ $run->visibility === 'public' ? 'lock-closed' : 'lock-open' }}"
>
{{ $run->visibility === 'public' ? __('Make private') : __('Make public') }}
</flux:button>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
@else
<flux:callout variant="brand">
<flux:callout.heading>{{ __('No runs yet') }}</flux:callout.heading>
<flux:callout.text>{{ __('Runs you import or submit will appear here.') }}</flux:callout.text>
</flux:callout>
@endif
</div>

View file

@ -0,0 +1,203 @@
<?php
use App\Livewire\IncludesSorting;
use App\Models\BenchmarkRun;
use App\Models\RunResult;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
new class extends Component {
use IncludesSorting;
use WithPagination;
public array $filters = [
'search' => null,
'suite' => null,
'level' => null,
'model' => null,
'result' => null,
];
private const SORTABLE = [
'ran_at', 'model_id', 'suite', 'task', 'section', 'level', 'ok',
'decode', 'user_tps', 'ttft', 't_answer', 'wall', 'total',
];
public function resetFilters(): void
{
$this->filters = [
'search' => null,
'suite' => null,
'level' => null,
'model' => null,
'result' => null,
];
$this->resetPage();
}
#[Computed]
public function results(): LengthAwarePaginator
{
return RunResult::query()
->join('benchmark_runs', 'run_results.benchmark_run_id', '=', 'benchmark_runs.id')
->select('run_results.*',
'benchmark_runs.ran_at',
'benchmark_runs.model_id',
'benchmark_runs.model_quant',
'benchmark_runs.engine',
'benchmark_runs.run_id',
'benchmark_runs.visibility',
'benchmark_runs.user_id as run_user_id')
->tap(fn (Builder $q) => $q->where(BenchmarkRun::visibleClause(auth()->user())))
->tap(fn (Builder $q) => $this->applySorting($q))
->tap(fn (Builder $q) => $this->applyFilters($q))
->with('run:id,run_id')
->paginate(25);
}
#[Computed]
public function suites(): array
{
return RunResult::query()->distinct()->orderBy('suite')->pluck('suite')->all();
}
#[Computed]
public function models(): array
{
return BenchmarkRun::visibleTo(auth()->user())
->distinct()->orderBy('model_id')->pluck('model_id')->all();
}
private function applySorting(Builder $query): Builder
{
$column = in_array($this->sortBy, self::SORTABLE, true) ? $this->sortBy : 'ran_at';
return $query->orderBy($column, $this->sortDirection === 'asc' ? 'asc' : 'desc')
->orderBy('run_results.id', 'desc');
}
private function applyFilters(Builder $query): Builder
{
return $query
->when($this->filters['search'], fn (Builder $q, string $search) => $q->where(function (Builder $q) use ($search) {
$q->where('run_results.task', 'like', "%{$search}%")
->orWhere('benchmark_runs.model_id', 'like', "%{$search}%")
->orWhere('benchmark_runs.label', 'like', "%{$search}%");
}))
->when($this->filters['suite'], fn (Builder $q, string $suite) => $q->where('run_results.suite', $suite))
->when($this->filters['level'], fn (Builder $q, string $level) => $q->where('run_results.level', $level))
->when($this->filters['model'], fn (Builder $q, string $model) => $q->where('benchmark_runs.model_id', $model))
->when($this->filters['result'] !== null && $this->filters['result'] !== '', fn (Builder $q) => $q->where('run_results.ok', (bool) $this->filters['result']));
}
};
?>
<div>
<div class="flex items-center justify-between">
<div>
<flux:heading size="xl">{{ __('Benchmark Results') }}</flux:heading>
<flux:subheading>{{ __('Every datapoint festbench captures — sortable.') }}</flux:subheading>
</div>
</div>
<div class="mt-6 flex flex-wrap items-end gap-3">
<flux:input
wire:model.live.debounce.300ms="filters.search"
:label="__('Search')"
:placeholder="__('Task, model or label…')"
size="sm"
class="w-64"
clearable
/>
<flux:select wire:model.live="filters.suite" :label="__('Suite')" placeholder="{{ __('All suites') }}" size="sm" class="w-40">
@foreach ($this->suites as $suite)
<flux:select.option value="{{ $suite }}">{{ $suite }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.level" :label="__('Level')" placeholder="{{ __('All levels') }}" size="sm" class="w-36">
@foreach (['off', 'low', 'medium', 'xhigh'] as $level)
<flux:select.option value="{{ $level }}">{{ $level }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.model" :label="__('Model')" placeholder="{{ __('All models') }}" size="sm" class="w-56">
@foreach ($this->models as $model)
<flux:select.option value="{{ $model }}">{{ $model }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model.live="filters.result" :label="__('Result')" placeholder="{{ __('Any') }}" size="sm" class="w-32">
<flux:select.option value="1">{{ __('Pass') }}</flux:select.option>
<flux:select.option value="0">{{ __('Fail') }}</flux:select.option>
</flux:select>
<flux:button size="sm" variant="ghost" wire:click="resetFilters" icon="x-mark">{{ __('Clear') }}</flux:button>
</div>
@if ($this->results->count() > 0)
<div class="mt-6">
<flux:table :paginate="$this->results">
<flux:table.columns>
<flux:table.column sortable :sorted="$sortBy === 'ran_at'" :direction="$sortDirection" wire:click="sort('ran_at')">{{ __('Date') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'model_id'" :direction="$sortDirection" wire:click="sort('model_id')">{{ __('Model') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'suite'" :direction="$sortDirection" wire:click="sort('suite')">{{ __('Suite') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'task'" :direction="$sortDirection" wire:click="sort('task')">{{ __('Task') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'level'" :direction="$sortDirection" wire:click="sort('level')">{{ __('Level') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'ok'" :direction="$sortDirection" wire:click="sort('ok')">{{ __('Pass') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'user_tps'" :direction="$sortDirection" wire:click="sort('user_tps')" class="text-right">{{ __('User tok/s') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'decode'" :direction="$sortDirection" wire:click="sort('decode')" class="text-right">{{ __('Decode') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'ttft'" :direction="$sortDirection" wire:click="sort('ttft')" class="text-right">{{ __('TTFT') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'wall'" :direction="$sortDirection" wire:click="sort('wall')" class="text-right">{{ __('Wall') }}</flux:table.column>
<flux:table.column sortable :sorted="$sortBy === 'total'" :direction="$sortDirection" wire:click="sort('total')" class="text-right">{{ __('Tokens') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->results as $result)
<flux:table.row :key="$result->id">
<flux:table.cell>{{ optional($result->ran_at)->format('Y-m-d') }}</flux:table.cell>
<flux:table.cell>
<a href="{{ route('runs.show', $result->run_id) }}" class="flex items-center hover:underline" wire:navigate>
<span class="truncate">{{ $result->model_id }}</span>
@if ($result->model_quant)
<flux:badge size="xs" class="ml-1" color="zinc">{{ $result->model_quant }}</flux:badge>
@endif
</a>
</flux:table.cell>
<flux:table.cell><flux:badge size="xs">{{ $result->suite }}</flux:badge></flux:table.cell>
<flux:table.cell>{{ $result->task }}</flux:table.cell>
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
<flux:table.cell>
@if ($result->ok)
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
@elseif ($result->ok === null)
<span class="text-zinc-400">—</span>
@else
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
@endif
</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->user_tps, 1) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->decode, 1) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->total) }}</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>
@else
<div class="mt-6">
<flux:callout variant="brand">
<flux:callout.heading>{{ __('No results') }}</flux:callout.heading>
<flux:callout.text>{{ __('No benchmark results match the current filters.') }}</flux:callout.text>
</flux:callout>
</div>
@endif
</div>

View file

@ -0,0 +1,173 @@
<?php
use App\Models\BenchmarkRun;
use Flux\Flux;
use Illuminate\Auth\Access\AuthorizationException;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Component;
new #[Layout('components.layouts.app')] class extends Component {
public ?string $notes = null;
public ?string $runId = null;
public function mount(string $run): void
{
$this->runId = $run;
}
#[Computed]
public function run(): BenchmarkRun
{
$run = BenchmarkRun::where('run_id', $this->runId)
->visibleTo(auth()->user())
->firstOrFail();
if ($run->isOwnedBy(auth()->user()) && $this->notes === null) {
$this->notes = $run->notes;
}
return $run;
}
public function saveNotes(): void
{
$this->authorizeOwner();
$this->validate([
'notes' => ['nullable', 'string', 'max:65535'],
]);
$this->run->fill(['notes' => $this->notes])->save();
Flux::toast(variant: 'success', text: __('Notes saved.'));
}
public function toggleVisibility(): void
{
$this->authorizeOwner();
$this->run->fill([
'visibility' => $this->run->visibility === BenchmarkRun::VISIBILITY_PUBLIC
? BenchmarkRun::VISIBILITY_PRIVATE
: BenchmarkRun::VISIBILITY_PUBLIC,
])->save();
Flux::toast(variant: 'success', text: __('Visibility updated.'));
}
private function authorizeOwner(): void
{
if (! $this->run->isOwnedBy(auth()->user())) {
throw new AuthorizationException;
}
}
};
?>
<div class="flex flex-col gap-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<flux:heading size="xl">{{ $this->run->suite }} <span class="text-zinc-400">·</span> <span class="font-mono text-lg">{{ $this->run->model_id }}</span></flux:heading>
<flux:subheading>
@if ($this->run->model_quant)<flux:badge color="zinc" size="xs">{{ $this->run->model_quant }}</flux:badge> @endif
@if ($this->run->engine)<flux:badge color="sky" size="xs">{{ $this->run->engine }}</flux:badge> @endif
@if ($this->run->label)<flux:badge color="amber" size="xs">{{ $this->run->label }}</flux:badge> @endif
@if ($this->run->visibility === 'private')<flux:badge color="rose" size="xs">{{ __('Private') }}</flux:badge> @endif
{{ optional($this->run->ran_at)->format('Y-m-d H:i') }}
</flux:subheading>
</div>
<div class="flex items-center gap-2">
<flux:button variant="ghost" icon="arrow-left" href="{{ route('home') }}" wire:navigate>{{ __('Back') }}</flux:button>
@if ($this->run->isOwnedBy(auth()->user()))
<flux:button
size="sm"
:variant="$this->run->visibility === 'public' ? 'subtle' : 'primary'"
wire:click="toggleVisibility"
icon="{{ $this->run->visibility === 'public' ? 'lock-closed' : 'lock-open' }}"
>
{{ $this->run->visibility === 'public' ? __('Make private') : __('Make public') }}
</flux:button>
@endif
</div>
</div>
<flux:card>
<flux:heading size="md">{{ __('Run metadata') }}</flux:heading>
<div class="mt-4 grid grid-cols-2 gap-x-8 gap-y-2 text-sm md:grid-cols-4">
<div><span class="text-zinc-500">{{ __('Engine build') }}</span><div class="font-mono">{{ $this->run->engine_build ?? '—' }}</div></div>
<div><span class="text-zinc-500">{{ __('Context') }}</span><div class="font-mono">{{ number_format($this->run->n_ctx ?? 0) }}</div></div>
<div><span class="text-zinc-500">{{ __('Slots') }}</span><div class="font-mono">{{ $this->run->slots ?? '—' }}</div></div>
<div><span class="text-zinc-500">{{ __('GPU') }}</span><div>{{ ($this->run->gpu_count ? $this->run->gpu_count.'x ' : '').($this->run->gpu ?? 'unknown') }}</div></div>
<div class="col-span-2"><span class="text-zinc-500">{{ __('Image digest') }}</span><div class="break-all font-mono text-xs">{{ $this->run->image_digest ?? '—' }}</div></div>
@if ($this->run->suite_versions)
<div class="col-span-2"><span class="text-zinc-500">{{ __('Suite versions') }}</span><div class="break-all font-mono text-xs">{{ json_encode($this->run->suite_versions) }}</div></div>
@endif
@if ($this->run->validators)
<div class="col-span-2"><span class="text-zinc-500">{{ __('Validators') }}</span><div class="break-all font-mono text-xs">{{ json_encode($this->run->validators) }}</div></div>
@endif
</div>
</flux:card>
@if ($this->run->isOwnedBy(auth()->user()))
<flux:card>
<flux:heading size="md">{{ __('Your notes') }}</flux:heading>
<flux:subheading>{{ __("Festbench data can't be modified — add your own context here.") }}</flux:subheading>
<flux:textarea wire:model="notes" rows="4" class="mt-4" placeholder="{{ __('Hardware details, environment, caveats…') }}" />
<div class="mt-3">
<flux:button size="sm" variant="primary" wire:click="saveNotes">{{ __('Save notes') }}</flux:button>
</div>
</flux:card>
@endif
<div>
<flux:heading size="md">{{ __('Task results') }}</flux:heading>
<div class="mt-3">
<flux:table>
<flux:table.columns>
<flux:table.column>{{ __('Section') }}</flux:table.column>
<flux:table.column>{{ __('Task') }}</flux:table.column>
<flux:table.column>{{ __('Level') }}</flux:table.column>
<flux:table.column>{{ __('Pass') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('User tok/s') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Decode') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('TTFT') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('t_answer') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Wall') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Reasoning') }}</flux:table.column>
<flux:table.column class="text-right">{{ __('Visible') }}</flux:table.column>
<flux:table.column>{{ __('Finish') }}</flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($this->run->results()->orderBy('section')->orderBy('task')->get() as $result)
<flux:table.row :key="$result->id">
<flux:table.cell>{{ $result->section ?? '—' }}</flux:table.cell>
<flux:table.cell>{{ $result->task }}</flux:table.cell>
<flux:table.cell>{{ $result->level ?? '—' }}</flux:table.cell>
<flux:table.cell>
@if ($result->ok)
<flux:icon name="check-circle" variant="solid" class="size-4 text-emerald-500" />
@elseif ($result->ok === null)
<span class="text-zinc-400">—</span>
@else
<flux:icon name="x-circle" variant="solid" class="size-4 text-red-500" />
@endif
</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->user_tps, 1) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->decode, 1) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->ttft, 2) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->t_answer, 2) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->wall, 2) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->reasoning) }}</flux:table.cell>
<flux:table.cell class="text-right font-mono text-sm">{{ number_format($result->visible) }}</flux:table.cell>
<flux:table.cell><flux:badge size="xs" color="{{ $result->finish === 'stop' ? 'emerald' : 'amber' }}">{{ $result->finish ?? '—' }}</flux:badge></flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
</div>
</div>
</div>

View file

@ -58,7 +58,7 @@ public function resendVerificationNotification(): void
$user = Auth::user();
if ($user->hasVerifiedEmail()) {
$this->redirectIntended(default: route('dashboard', absolute: false));
$this->redirectIntended(default: route('home', absolute: false));
return;
}

View file

@ -21,7 +21,7 @@
<nav class="flex items-center justify-end gap-4">
@auth
<a
href="{{ url('/dashboard') }}"
href="{{ url('/') }}"
class="inline-block px-5 py-1.5 dark:text-[#EDEDEC] border-[#19140035] hover:border-[#1915014a] border text-[#1b1b18] dark:border-[#3E3E3A] dark:hover:border-[#62605b] rounded-sm text-sm leading-normal"
>
Dashboard

View file

@ -3,15 +3,13 @@
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;
Route::get('/', function () {
return view('welcome');
})->name('home');
Route::livewire('/', 'results.index')->name('home');
Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified'])
->name('dashboard');
Route::livewire('runs/{run}', 'runs.show')->name('runs.show');
Route::middleware(['auth', 'verified'])->group(function () {
Route::livewire('my-runs', 'my-runs.index')->name('my-runs');
Route::middleware(['auth'])->group(function () {
Route::redirect('settings', 'settings/profile');
Volt::route('settings/profile', 'settings.profile')->name('settings.profile');

View file

@ -29,7 +29,7 @@ public function test_users_can_authenticate_using_the_login_screen(): void
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
->assertRedirect(route('home', absolute: false));
$this->assertAuthenticated();
}

View file

@ -39,7 +39,7 @@ public function test_email_can_be_verified(): void
Event::assertDispatched(Verified::class);
$this->assertTrue($user->fresh()->hasVerifiedEmail());
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
$response->assertRedirect(route('home', absolute: false).'?verified=1');
}
public function test_email_is_not_verified_with_invalid_hash(): void

View file

@ -32,7 +32,7 @@ public function test_password_can_be_confirmed(): void
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
->assertRedirect(route('home', absolute: false));
}
public function test_password_is_not_confirmed_with_invalid_password(): void

View file

@ -28,7 +28,7 @@ public function test_new_users_can_register(): void
$response
->assertHasNoErrors()
->assertRedirect(route('dashboard', absolute: false));
->assertRedirect(route('home', absolute: false));
$this->assertAuthenticated();
}

View file

@ -1,27 +1,17 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class DashboardTest extends TestCase
{
use RefreshDatabase;
test('guests are redirected from my-runs to the login page', function () {
$response = $this->get(route('my-runs'));
public function test_guests_are_redirected_to_the_login_page(): void
{
$response = $this->get('/dashboard');
$response->assertRedirect('/login');
}
$response->assertRedirect(route('login'));
});
public function test_authenticated_users_can_visit_the_dashboard(): void
{
$user = User::factory()->create();
$this->actingAs($user);
test('authenticated users can visit the home page', function () {
$this->actingAs(User::factory()->create());
$response = $this->get('/dashboard');
$response->assertStatus(200);
}
}
$response = $this->get(route('home'));
$response->assertStatus(200);
});

View file

@ -0,0 +1,117 @@
<?php
use App\Models\BenchmarkRun;
use App\Models\RunResult;
use App\Models\User;
use Livewire\Volt\Volt;
test('guests see public task results on the home page', function () {
$publicRun = BenchmarkRun::factory()->create(['model_id' => 'public/model']);
RunResult::factory()->count(3)->create(['benchmark_run_id' => $publicRun->id]);
BenchmarkRun::factory()->private()->hasResults(2)->create(['model_id' => 'private/model']);
$response = $this->get(route('home'));
$response->assertOk()
->assertSee('public/model')
->assertDontSee('private/model');
});
test('authenticated users see their private runs mixed in', function () {
$me = User::factory()->create();
BenchmarkRun::factory()->private()->hasResults(2)->create([
'model_id' => 'my-private/model', 'user_id' => $me->id,
]);
BenchmarkRun::factory()->private()->hasResults(1)->create(['model_id' => 'other-private/model']);
$this->actingAs($me);
$this->get(route('home'))->assertOk()
->assertSee('my-private/model')
->assertDontSee('other-private/model');
});
test('home page sorting orders by user tok/s', function () {
$run = BenchmarkRun::factory()->create();
RunResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'slow-task', 'user_tps' => 5]);
RunResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'fast-task', 'user_tps' => 95]);
Volt::test('results.index', ['sortBy' => 'user_tps', 'sortDirection' => 'asc'])
->assertSeeInOrder(['slow-task', 'fast-task']);
});
test('home page filters narrow by suite', function () {
$run = BenchmarkRun::factory()->create();
RunResult::factory()->create(['benchmark_run_id' => $run->id, 'suite' => 'softbench', 'task' => 'soft-task']);
RunResult::factory()->create(['benchmark_run_id' => $run->id, 'suite' => 'infrabench', 'task' => 'infra-task']);
Volt::test('results.index', ['filters' => ['search' => null, 'suite' => 'softbench', 'level' => null, 'model' => null, 'result' => null]])
->assertSee('soft-task')
->assertDontSee('infra-task');
});
test('run detail page shows metadata and results', function () {
$run = BenchmarkRun::factory()->create(['model_id' => 'detail/model', 'engine' => 'vllm']);
RunResult::factory()->create(['benchmark_run_id' => $run->id, 'task' => 'visible-task']);
$this->get(route('runs.show', $run->run_id))->assertOk()
->assertSee('detail/model')
->assertSee('visible-task')
->assertSee('vllm');
});
test('private runs are hidden from guests on the detail page', function () {
$run = BenchmarkRun::factory()->private()->create();
$this->get(route('runs.show', $run->run_id))->assertNotFound();
});
test('owners can edit notes from the run detail page', function () {
$me = User::factory()->create();
$run = BenchmarkRun::factory()->create(['user_id' => $me->id]);
$this->actingAs($me);
Volt::test('runs.show', ['run' => $run->run_id])
->set('notes', 'ran on the spare GPU while idle')
->call('saveNotes');
expect($run->fresh()->notes)->toBe('ran on the spare GPU while idle');
});
test('non-owners cannot edit notes', function () {
$intruder = User::factory()->create();
$run = BenchmarkRun::factory()->create(['user_id' => User::factory()->create()->id]);
$this->actingAs($intruder);
Volt::test('runs.show', ['run' => $run->run_id])
->set('notes', 'nice try')
->call('saveNotes');
expect($run->fresh()->notes)->toBeNull();
});
test('owner can toggle visibility from run detail page', function () {
$me = User::factory()->create();
$run = BenchmarkRun::factory()->create(['user_id' => $me->id]);
$this->actingAs($me);
Volt::test('runs.show', ['run' => $run->run_id])->call('toggleVisibility');
expect($run->fresh()->visibility)->toBe('private');
});
test('my runs page lists only owned runs with counts', function () {
$me = User::factory()->create();
$run = BenchmarkRun::factory()->create(['user_id' => $me->id, 'model_id' => 'mine/model']);
RunResult::factory()->count(4)->create(['benchmark_run_id' => $run->id]);
BenchmarkRun::factory()->create(['user_id' => User::factory()->create()->id, 'model_id' => 'theirs/model']);
$this->actingAs($me);
$this->get(route('my-runs'))->assertOk()
->assertSee('mine/model')
->assertDontSee('theirs/model');
});