diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aefc162 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.git +.github +auth.json +.env +.env.* +!.env.example +vendor +node_modules +storage/framework/cache/data/* +storage/framework/sessions/* +storage/framework/views/* +storage/logs/* +docker/develop.compose.yaml +Caddyfile.dev +dev.sh +*.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1c9cf79..16d1eea 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ yarn-error.log /.nova /.vscode /.zed + +.env.deploy diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7f52ea3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +# syntax=docker/dockerfile:1 + +# Stage 1: PHP dependencies (Flux Pro auth injected as a BuildKit secret) +FROM docker.io/library/composer:2 AS composer-builder +WORKDIR /app +COPY composer.json composer.lock ./ +RUN --mount=type=secret,id=composer_auth,target=/tmp/auth.json \ + composer install \ + --no-dev \ + --no-interaction \ + --no-plugins \ + --no-scripts \ + --prefer-dist \ + --optimize-autoloader +COPY . . +RUN composer dump-autoload --optimize --no-dev + +# Stage 2: frontend assets — needs vendor/livewire/flux CSS present +FROM docker.io/library/node:20-alpine AS node-builder +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +COPY --from=composer-builder /app/vendor ./vendor +RUN npm run build + +# Stage 3: production image — FrankenPHP serves the app (no nginx, no supervisor) +FROM docker.io/dunglas/frankenphp:1.12-php8.5 + +RUN install-php-extensions pdo_mysql intl zip opcache + +WORKDIR /app + +COPY --from=composer-builder /app /app +COPY --from=node-builder /app/public/build /app/public/build + +COPY docker/Caddyfile /etc/caddy/Caddyfile +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh \ + && mkdir -p storage/framework/{sessions,views,cache/data} storage/logs bootstrap/cache \ + && chown -R www-data:www-data /app/storage /app/bootstrap/cache + +EXPOSE 8080 + +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6802a6d --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# festbench-web + +Web viewer for [FestBench](../llm_benchmark) results — LLM benchmark datapoints +submitted by the `festbench` CLI, displayed in a fast, sortable table. + +Stack: Laravel 12 · Livewire 4 + Volt · Flux UI (Pro) · FrankenPHP · MySQL/MariaDB + +## Features + +- **Home** — every datapoint festbench captures (per-task rows across all runs), + server-side sortable and filterable (suite, level, model, pass/fail, search). +- **Run detail** (`/runs/{run_id}`) — full provenance: model identity, engine + build, GPU, image digest, validators, suite versions, plus per-task metrics. +- **Ownership overlay** — festbench-submitted data is immutable; owners can add + notes and toggle public/private visibility. Immutability is enforced in the + model (`BenchmarkRun::OVERLAY_FIELDS`) and covered by tests. +- **My Runs** — list your own runs, flip visibility, see pass counts. +- `festbench:import` — import a local festbench `benchmarks.db` (SQLite): + `php artisan festbench:import results/benchmarks.db --user=you@example.com` + +## Visibility rules + +Runs are public by default (matching the CLI's redaction philosophy). Guests see +public runs only; authenticated users additionally see their own private runs. + +## Local development + +Requirements: `frankenphp` binary on PATH, Node 20+, Docker/Podman. + +```bash +docker compose -f docker/develop.compose.yaml up -d # MariaDB on 127.0.0.1:3306 +./dev.sh # frankenphp :8080 + vite HMR +``` + +Composer runs through frankenphp's embedded PHP (the host php.ini disables +most extensions): + +```bash +frankenphp php-cli $(pwd)/vendor/bin/composer install +frankenphp php-cli $(pwd)/vendor/bin/pest +frankenphp php-cli $(pwd)/vendor/bin/pint +frankenphp php-cli artisan +``` + +The dev DB deliberately lives in a **container, reached over TCP** — the same +topology as production, where MariaDB runs on a nearby host. Point `.env` +`DB_HOST`/`DB_PORT` at wherever the DB actually lives. + +## Production (Coolify) + +Build pack: **Dockerfile**. The image is a single FrankenPHP process — no +nginx, no php-fpm, no supervisor. + +- `Dockerfile` — 3 stages: composer (Flux Pro auth as a BuildKit **secret**), + node assets, final `dunglas/frankenphp:1.12-php8.5` + `install-php-extensions + pdo_mysql intl zip opcache`. +- `docker/entrypoint.sh` — retries `artisan migrate --force`, then `artisan + optimize`, then `exec frankenphp run`. +- `docker/Caddyfile` — listens on `{$SERVER_NAME::8080}`; Coolify maps its + port to it. Static files served directly by Caddy, Laravel via `php_server`. + +Secrets/env to set in Coolify: + +| Variable | Notes | +|---|---| +| `APP_KEY` | generate once, keep stable | +| `DB_HOST` / `DB_PORT` / `DB_DATABASE` / `DB_USERNAME` / `DB_PASSWORD` | the nearby MariaDB | +| `SESSION_DRIVER=database`, `CACHE_STORE=database` | no redis needed | + +Flux Pro authentication: add a secret named `composer_auth` +(file contents = the project's `auth.json`) and configure Coolify to build with +`--secret id=composer_auth,src=…` (or `docker build --secret` in CI). The +license key is never committed. + +Health check: `GET /up`. + +## Submitting results (CLI integration — separate session) + +The schema (`benchmark_runs`, `run_results`) mirrors festbench's SQLite exactly, +promotes the sortable columns, and stores the **complete source row as JSON** +(`run_results.payload`) so new festbench fields require no migration. The +authed ingestion endpoint for `festbench --submit` is future work; the +`festbench:import` command is the reference implementation of that mapping. + +## Deferred + +- `scenario_runs` (scenariobench's agentic metrics: turns, submissions, oracle + verdicts) — intentionally not imported yet. +- Worker mode (`FRANKENPHP_CONFIG="worker ..."`) — available as a one-env-var + toggle when throughput demands it. \ No newline at end of file diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 0000000..c4e35c9 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,21 @@ +# Production config. Coolify sets PORT (and optionally SERVER_NAME); the app +# listens on :8080 which Coolify maps to the exposed port. +{ + frankenphp +} + +{$SERVER_NAME::8080} { + root * /app/public + encode zstd br gzip + + # php_server serves existing static files directly and falls back to + # index.php for everything else (Laravel routing). + php_server { + try_files {path} index.php + } + + log { + output stdout + format console + } +} \ No newline at end of file diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..03759c0 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +cd /app + +# Storage must be writable at runtime (Coolify may mount a fresh volume). +mkdir -p storage/framework/sessions storage/framework/views storage/framework/cache/data storage/logs bootstrap/cache +chown -R www-data:www-data storage bootstrap/cache + +# Migrate before serving, but never leave the container crash-looping on a +# temporarily unavailable DB: retry a few times, then start anyway and let the +# health endpoint report the state. +i=0 +until php artisan migrate --force --no-ansi; do + i=$((i+1)) + if [ "$i" -ge 5 ]; then + echo "festbench-web: migrations failed after 5 attempts, starting anyway" >&2 + break + fi + echo "festbench-web: migrate failed (attempt $i), retrying in 3s…" >&2 + sleep 3 +done + +php artisan optimize --no-ansi || true + +exec frankenphp run --config /etc/caddy/Caddyfile \ No newline at end of file