Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ WEB_PORT=3000
# The browser calls the same-origin path /api; Next proxies it server-side to the
# backend at this address (the compose service name). Avoids CORS and per-env URL baking.
API_PROXY_TARGET=http://api:4000
# Public origin used for canonical/Open-Graph absolute URLs and the sitemap.
# Baked into the web build (NEXT_PUBLIC_*). Set the real domain in production.
NEXT_PUBLIC_SITE_URL=http://localhost:3000
59 changes: 46 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
continue-on-error: true

e2e:
name: Dynamic (integration + e2e)
name: Dynamic (integration + e2e + lighthouse)
needs: static
runs-on: ubuntu-latest
env:
Expand All @@ -78,38 +78,71 @@ jobs:
steps:
- uses: actions/checkout@v5

# Node is only needed on the runner for the Lighthouse CI step (the
# integration + e2e suite itself runs in-container). Cache the tests lockfile.
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: tests/package-lock.json

# Buildx enables the gha layer-cache backend used by Compose Bake.
- uses: docker/setup-buildx-action@v4

# Compose reads .env for variable substitution; the example values are
# self-consistent and safe for the ephemeral test stack.
# self-consistent and safe for the ephemeral test stack. WEB_PORT defaults
# to 3000, so `web` is published on localhost:3000 for the Lighthouse run.
- name: Create .env
run: cp .env.example .env

# Build the images (Compose Bake + gha layer cache) and run the in-container
# Playwright suite against fresh db/api/web. The `tests` container's exit code
# is the suite result. Mirrors `make test`.
# Boot the stack detached. `--build` is a fast CACHED replay of the gha layer
# cache; `--wait` blocks until db/api/web healthchecks pass (api migrates +
# seeds on boot against the ephemeral test volume).
- name: Boot the dockerized stack
env:
COMPOSE_BAKE: "1"
run: docker compose ${{ env.COMPOSE_FILES }} up -d --build --wait db api web

# In-container Playwright integration + e2e against the running stack. The
# `tests` container's exit code gates the job; its HTML report lands on the
# host via the bind mount in docker-compose.test.yml. Mirrors `make test`.
- name: Run integration + e2e
env:
COMPOSE_BAKE: "1"
run: docker compose ${{ env.COMPOSE_FILES }} up --build --abort-on-container-exit --exit-code-from tests tests
run: docker compose ${{ env.COMPOSE_FILES }} run --build --rm tests

# Lighthouse benchmark gate on the landing page, reusing the booted stack.
# Runs on the runner (ubuntu ships Chrome) against the published :3000. Fails
# the job if SEO / Accessibility / Best-Practices drop below threshold
# (Performance is a warning). See .lighthouserc.json.
- name: Install Lighthouse CI
working-directory: tests
run: npm ci

- name: Lighthouse benchmark gate (landing page)
if: ${{ !cancelled() }}
working-directory: tests
run: npm run lighthouse

- name: Upload Lighthouse report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: lighthouse-report
path: tests/lighthouse-report
if-no-files-found: ignore
retention-days: 7

- name: Dump stack logs on failure
if: failure()
run: docker compose ${{ env.COMPOSE_FILES }} logs --no-color

# The HTML report is written inside the tests container; copy it out before teardown.
- name: Extract Playwright report
if: ${{ !cancelled() }}
continue-on-error: true
run: docker compose ${{ env.COMPOSE_FILES }} cp tests:/app/tests/playwright-report ./playwright-report

- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: playwright-report
path: tests/playwright-report
if-no-files-found: ignore
retention-days: 7

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ playwright-report/
test-results/
blob-report/
.playwright/
lighthouse-report/
.lighthouseci/

# logs
*.log
Expand Down
24 changes: 24 additions & 0 deletions .lighthouserc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"ci": {
"collect": {
"url": ["http://localhost:3000/"],
"numberOfRuns": 1,
"settings": {
"preset": "desktop",
"chromeFlags": "--no-sandbox --headless=new --disable-gpu"
}
},
"assert": {
"assertions": {
"categories:seo": ["error", { "minScore": 0.95 }],
"categories:accessibility": ["error", { "minScore": 0.95 }],
"categories:best-practices": ["error", { "minScore": 0.9 }],
"categories:performance": ["warn", { "minScore": 0.7 }]
}
},
"upload": {
"target": "filesystem",
"outputDir": "lighthouse-report"
}
}
}
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ test-be: ## Run backend unit tests (engine/RTP, services)
test-e2e: ## Run Playwright e2e against the running web+api containers
cd tests && npm run e2e

.PHONY: lighthouse
lighthouse: env ## Boot the stack and run the Lighthouse benchmark gate on the landing page
$(COMPOSE_TEST) up -d --build --wait db api web
@set -a; . ./.env; set +a; \
url="http://localhost:$${WEB_PORT:-3000}/"; \
echo "Lighthouse benchmark → $$url"; \
( cd tests && npm ci && npx lhci autorun --config=../.lighthouserc.json --collect.url="$$url" ); \
code=$$?; $(COMPOSE_TEST) down -v; exit $$code

## ─── Quality gates ───────────────────────────────────────────────────────
.PHONY: lint
lint: ## Lint backend + frontend
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ services:
API_URL: http://api:4000
WEB_URL: http://web:3000
DATABASE_URL: ${DATABASE_URL}
# Surface the Playwright HTML report + traces on the host so CI can upload them
# (the runner-side Lighthouse gate reaches `web` via the published :3000 port).
volumes:
- ./tests/playwright-report:/app/tests/playwright-report
- ./tests/test-results:/app/tests/test-results
depends_on:
api:
condition: service_healthy
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ services:
dockerfile: frontend/Dockerfile
args:
API_PROXY_TARGET: ${API_PROXY_TARGET:-http://api:4000}
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
restart: unless-stopped
environment:
NODE_ENV: ${NODE_ENV:-production}
Expand Down
66 changes: 66 additions & 0 deletions docs/adr/0009-lighthouse-benchmark-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ADR 0009 — Lighthouse benchmark gate (SEO / Accessibility / Best-Practices)

- Status: Accepted
- Date: 2026-06-11

## Context

CI (ADR‑0008) covered correctness — build, lint, typecheck, unit, and the in‑container
integration/e2e suite — but nothing measured the **quality of the served page**: SEO,
accessibility, and front‑end best practices were unverified and could regress silently. The
convention‑template sibling repo (`mitekk/pool-stars`) already runs a Lighthouse gate in CI;
this ADR brings poker_land to parity and makes "benchmarks" a concrete, enforced signal.

The home page was also an SEO dead end: anonymous visitors to `/` got an invisible splash that
client‑redirected to `/login` — thin content and a flaky audit target.

## Decision

- **Add a Lighthouse CI gate** via [`@lhci/cli`](https://github.com/GoogleChrome/lighthouse-ci)
configured by [`.lighthouserc.json`](../../.lighthouserc.json) at the repo root, mirroring the
pool‑stars template. It audits the landing page (`/`, desktop preset) and **fails the job**
below threshold:

| Category | Level | Min score |
|---|---|---|
| `seo` | **error** | 0.95 |
| `accessibility` | **error** | 0.95 |
| `best-practices` | **error** | 0.90 |
| `performance` | warn | 0.70 |

Performance is a non‑blocking warning — it is the most environment‑sensitive metric on shared
CI runners; the SEO/A11y/Best‑Practices floors are the enforced contract.
- **Make `/` a real public landing.** Anonymous `/` now renders crawlable marketing content with
genuine `<a href="/login">` CTAs (no client redirect), giving search engines indexable content
and Lighthouse a stable target. Authenticated users still get the game.
- **Ship the SEO surface** that the audit (and real crawlers) expect: full metadata
(`metadataBase`, canonical, Open Graph, Twitter), `robots.ts`, `sitemap.ts`, a web manifest,
generated icons + OG image (`next/og`, no committed binaries), and JSON‑LD structured data.
- **Run Lighthouse on the runner, keep the suite in‑container.** The `e2e` job boots the stack
detached (`up -d --wait db api web`), runs the Playwright integration + e2e suite in‑container
(`docker compose run --rm tests` — its exit code still gates the job), then runs `lhci autorun`
**on the runner** against the published `http://localhost:3000/`. The report is uploaded as the
`lighthouse-report` artifact.

## Alternatives considered

- **Run Lighthouse in‑container** (inside the Playwright `tests` image) to stay 100% in‑container
like ADR‑0008's suite — rejected: it requires wrangling a Chrome binary in the image and a
container‑network audit URL (`http://web:3000`), diverging the config from the template.
ubuntu‑latest already ships Chrome, so the runner path is simpler and keeps
`.lighthouserc.json` identical to pool‑stars. The Playwright suite itself stays in‑container —
this is the one deliberate exception to ADR‑0008's "host‑run" rejection, scoped to Lighthouse.
- **Benchmark `/login` instead of `/`** — smaller change, but leaves the actual home page
SEO‑empty; the public landing fixes the root problem.
- **Warn‑only thresholds** — reports scores without enforcing them; rejected in favor of hard
floors so regressions actually block, matching the convention template.

## Consequences

- Push/PR now enforces SEO ≥ 0.95, Accessibility ≥ 0.95, Best‑Practices ≥ 0.90 on the landing
page, with the score report attached as an artifact.
- `/` is publicly indexable with structured data, a sitemap, robots rules, and social cards.
- The `e2e` job restructured from a single `up … tests` into boot → in‑container suite →
runner‑side Lighthouse; the Playwright HTML report now reaches the host via a bind mount in
[`docker-compose.test.yml`](../../docker-compose.test.yml). Reproduce locally with
`make lighthouse`. See [`docs/ci.md`](../ci.md).
62 changes: 36 additions & 26 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
GitHub Actions runs the same quality gate as the local `Makefile`, automatically on
every push and pull request. The workflow lives in
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml); the decision behind it is
recorded in [ADR‑0008](adr/0008-github-actions-ci.md).
recorded in [ADR‑0008](adr/0008-github-actions-ci.md), and the Lighthouse benchmark gate in
[ADR‑0009](adr/0009-lighthouse-benchmark-gate.md).

## Overview

Expand All @@ -12,7 +13,7 @@ Two jobs, mapped onto the existing `make` targets:
| Job | What it does | Local equivalent |
|---|---|---|
| **`static`** | build → lint → typecheck → unit tests (in‑process) | `make lint` + `make typecheck` + `make test-be` |
| **`e2e`** | build & boot the dockerized stack, run the Playwright integration + e2e suite in‑container, tear down | `make test` |
| **`e2e`** | boot the dockerized stack, run the Playwright integration + e2e suite in‑container, then the **Lighthouse benchmark gate** on the runner, tear down | `make test` + `make lighthouse` |

`e2e` is **gated behind `static`** (`needs: static`) — if the cheap static checks fail,
we don't spend time building and booting Docker images.
Expand Down Expand Up @@ -53,36 +54,44 @@ Runs on `ubuntu-latest`, Node from `.nvmrc` (`engines.node >=20`):
6. `npm audit --audit-level=high` — **advisory** (`continue-on-error`): surfaces
HIGH/CRITICAL CVEs without failing the run.

## `e2e` job — integration + e2e (dynamic)
## `e2e` job — integration + e2e + lighthouse (dynamic)

Runs on `ubuntu-latest`. Mirrors `make test` — the suite runs **inside a `tests`
container** (Playwright image, browsers preinstalled) against fresh `db`/`api`/`web`:
Runs on `ubuntu-latest`. The integration + e2e suite runs **inside a `tests` container**
(Playwright image, browsers preinstalled); the Lighthouse benchmark gate runs **on the runner**
(ubuntu ships Chrome) against the published `web` port. Steps:

1. **Buildx** is set up (`docker/setup-buildx-action`) to enable the layer‑cache backend.
2. `cp .env.example .env` — Compose reads `.env` for variable substitution; the example
values are self‑consistent and safe for the ephemeral test stack.
3. **Build & run:**
1. **`setup-node`** (Node from `.nvmrc`, npm cache keyed on `tests/package-lock.json`) — only the
runner‑side Lighthouse step needs Node; the suite itself is containerized.
2. **Buildx** is set up (`docker/setup-buildx-action`) to enable the layer‑cache backend.
3. `cp .env.example .env` — Compose reads `.env` for variable substitution; the example values
are self‑consistent and safe for the ephemeral test stack. `WEB_PORT` defaults to `3000`, so
`web` is published on `localhost:3000` for Lighthouse.
4. **Boot the stack (detached):**
```
COMPOSE_BAKE=1 docker compose \
-f docker-compose.yml -f docker-compose.test.yml -f docker-compose.ci.yml \
up --build --abort-on-container-exit --exit-code-from tests tests
up -d --build --wait db api web
```
- The three `-f` files are passed explicitly so `docker-compose.override.yml` (the dev
target + bind mounts) is **not** merged.
- `docker-compose.test.yml` swaps in an ephemeral DB and adds the `tests` runner, which
`depends_on` `api`/`web` being healthy.
- The api `runner` image **migrates + seeds on boot**, so the fresh DB is provisioned
automatically before tests start.
- `--abort-on-container-exit --exit-code-from tests` makes the `tests` container's exit
code the suite result.
4. **Always:** on failure the stack logs are dumped; the HTML report is copied out of the
`tests` container and uploaded as the **`playwright-report`** artifact; the stack is torn
down with `down -v`.

### Reading the report

When `e2e` fails, open the run's **Artifacts → `playwright-report`**, unzip it, and open
`index.html`. Traces are attached for first‑retry / failed specs (`trace: on-first-retry`).
- The three `-f` files are passed explicitly so `docker-compose.override.yml` (the dev target
+ bind mounts) is **not** merged.
- `--wait` blocks until db/api/web healthchecks pass; the api `runner` image **migrates +
seeds on boot**, so the fresh ephemeral DB is provisioned before tests start.
5. **Integration + e2e (in‑container):** `docker compose … run --rm tests`. The `tests`
container's exit code gates the job; its HTML report lands on the host via the bind mount in
`docker-compose.test.yml`.
6. **Lighthouse benchmark gate:** `npm ci` in `tests/`, then `npm run lighthouse`
(`lhci autorun --config=../.lighthouserc.json`) audits `http://localhost:3000/`. It **fails
the job** if SEO / Accessibility / Best‑Practices fall below threshold (Performance is a
warning). See [ADR‑0009](adr/0009-lighthouse-benchmark-gate.md) and
[`.lighthouserc.json`](../.lighthouserc.json).
7. **Always:** on failure the stack logs are dumped; the **`playwright-report`** and
**`lighthouse-report`** artifacts are uploaded; the stack is torn down with `down -v`.

### Reading the reports

When `e2e` fails, open the run's **Artifacts**: **`playwright-report`** (unzip, open
`index.html`; traces are attached for first‑retry / failed specs) and **`lighthouse-report`**
(the per‑category scores + the specific audits that dropped below threshold).

## Caching

Expand Down Expand Up @@ -114,6 +123,7 @@ Each CI step has a `make` / npm equivalent:
| static: unit tests | `make test-be` |
| static: audit | `make audit` |
| e2e: full dynamic suite | `make test` |
| e2e: Lighthouse benchmark gate | `make lighthouse` |

> `make test` uses `docker-compose.yml` + `docker-compose.test.yml` only — the
> `docker-compose.ci.yml` cache override is CI‑specific and not needed locally.
3 changes: 3 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ FROM base AS build
# Baked into the standalone routes manifest at build time.
ARG API_PROXY_TARGET=http://api:4000
ENV API_PROXY_TARGET=$API_PROXY_TARGET
# Public origin for canonical/OG absolute URLs + sitemap (inlined as NEXT_PUBLIC_*).
ARG NEXT_PUBLIC_SITE_URL=http://localhost:3000
ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
COPY packages packages
COPY frontend frontend
RUN npm run build --workspace @casino/shared \
Expand Down
33 changes: 33 additions & 0 deletions frontend/app/apple-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ImageResponse } from 'next/og';

// Generated apple-touch icon (no binary asset committed). A gold crescent moon
// carved from two overlapping circles on the brand-dark tile.
export const size = { width: 180, height: 180 };
export const contentType = 'image/png';

export default function AppleIcon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#120a24',
}}
>
<div style={{ position: 'relative', width: 112, height: 112, display: 'flex' }}>
<div
style={{ position: 'absolute', top: 0, left: 0, width: 112, height: 112, borderRadius: '50%', background: '#fde68a' }}
/>
<div
style={{ position: 'absolute', top: -8, left: 28, width: 98, height: 98, borderRadius: '50%', background: '#120a24' }}
/>
</div>
</div>
),
{ ...size },
);
}
10 changes: 10 additions & 0 deletions frontend/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading