Skip to content
Open
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
190 changes: 190 additions & 0 deletions .claude/skills/pr-screenshot/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
name: pr-screenshot
description: "Capture and attach frontend screenshots and videos to pull requests for quantfive/django-react-intro. Auto-triggers when creating a PR with UI changes under web/src or web/public. Uses Playwright to render the affected page and uploads assets to the PR. IMPORTANT: Use this skill whenever a PR changes visible frontend behavior. Trigger on: 'make a pr', 'create a pr', 'open a pull request', or any request that results in a PR with frontend file changes."
user_invocable: true
codepress_generated: true
---

## When to Trigger

Run this skill automatically when a pull request changes visible frontend behavior, including:

- `web/src/**/*.{js,jsx,css,svg}`
- `web/public/**/*.{html,css,ico,json,png,svg}`
- `web/package.json` or `web/yarn.lock` when the dependency change affects the UI
- `server/server/views.py` or templates when the change affects the page served at `/`

Do not run it for backend-only changes, database changes, infrastructure or CI changes, documentation-only changes, or test-only changes with no visible UI effect.

## What to Capture

This repository has one frontend app and one public route:

| Changed files | What to screenshot | URL |
| --- | --- | --- |
| `web/src/containers/home/**` | Home page content and branding | `/` |
| `web/src/containers/App/**` | Application shell and route behavior | `/` |
| `web/src/containers/home/stylesheets/**`, `web/src/index.css` | Home page layout and styling | `/` |
| `web/src/index.js`, `web/public/**` | Bootstrapped home page | `/` |
| `server/server/views.py` or the frontend template | Django-served frontend shell | `/` |

The frontend is a Create React App application using `react-scripts@1.0.10`, React 15, Redux, and React Router. The current route is public and does not require authentication or API mocks. The current home-page readiness checks are the `Welcome to React` heading and the image with alt text `logo`; when a PR changes a different element, replace those checks with a semantic locator for the changed feature.

Use a screenshot for static layout, color, typography, and component changes. Use a video when the change includes an animation, transition, hover or click interaction, loading state, or other time-based behavior. Capture both when reviewers need a static preview and an interaction demonstration.

## Dev Server

### Option A: CodePress container capture

If the repository has `.claude/skills/start-app-server/SKILL.md` and `.codepress/start-app-server/recipe.json`, follow that skill to build and start the full-stack container, then use `take_app_server_screenshot` against `/` with the recipe port. Prefer a viewport of 1440 by 1000 and wait up to 1000 milliseconds after load so the page is settled.

The existing `.codepress/dev-server/recipe.json` describes the `web` CRA dev server at port 3000 and points to `.codepress/dev-server/Dockerfile.web`. That Dockerfile is a thin Live Dev Server image which expects the CodePress Live Dev Server source mount; do not pass it directly to `build_and_start_app_server` without that mount. If the Live Dev Server transport is available, use its `web` entry; otherwise use Option B.

### Option B: Local Playwright capture

The durable Playwright config is `web/e2e/playwright.config.js`. It starts the CRA server on port 3000 with the repo's Yarn 1 lockfile:

```bash
cd web
npx --yes yarn@1.22.22 install --frozen-lockfile
npx --yes yarn@1.22.22 playwright install chromium
npx --yes yarn@1.22.22 playwright test \
--config e2e/playwright.config.js \
e2e/tests/_pr-capture.spec.js \
--workers=1
```

The CRA toolchain is from 2017. Use Node 18 for the frontend runtime; the host Node 25 runtime is incompatible with its `websocket-driver` dependency and fails with `No such module: http_parser`. The temporary capture spec must be removed after the run.

The frontend home route is standalone, so no Django server is needed for the current screenshot contract. If a future change adds a backend request, start the Django app according to the repo-local app-server recipe and add the required route mocks or service URL to the spec rather than hiding a failing request.

## Capture Spec Template

Create a temporary file at `web/e2e/tests/_pr-capture.spec.js`:

```javascript
const { test, expect } = require('@playwright/test');

test('capture the changed home-page feature', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });

const feature = page.getByRole('heading', { name: 'Welcome to React' });
await expect(feature).toBeVisible();
await feature.scrollIntoViewIfNeeded();
await page.waitForTimeout(500);

await page.screenshot({
path: '/tmp/pr-screenshots/pr-screenshot-home.png',
fullPage: false,
});
});
```

Keep the assertion focused on the changed feature body, not only the page shell. Use `getByRole`, `getByText`, or another stable semantic locator because the repository currently has no `data-testid` convention. Set a mobile viewport of 390 by 844 as an additional capture when the PR changes responsive behavior.

For interaction changes, use this video pattern and close the page after the final state so the recording is flushed:

```javascript
test.use({
video: { mode: 'on', size: { width: 1280, height: 720 } },
viewport: { width: 1280, height: 720 },
});

test('record the changed home-page interaction', async ({ page }, testInfo) => {
await page.goto('/', { waitUntil: 'networkidle' });
await page.getByRole('heading', { name: 'Welcome to React' }).waitFor({ state: 'visible' });
// Perform the user interaction here and wait for its final visible state.
await page.waitForTimeout(750);
await page.close();

const video = testInfo.attachments.find((attachment) => attachment.name === 'video');
if (video && video.path) {
const fs = require('fs');
fs.mkdirSync('/tmp/pr-screenshots', { recursive: true });
fs.copyFileSync(video.path, '/tmp/pr-screenshots/pr-video-home.webm');
}
});
```

## Running the Spec

Kill stale local servers before capture, then remove the temporary spec and output:

```bash
cd web
lsof -ti :3000 2>/dev/null | xargs kill 2>/dev/null || true
rm -rf /tmp/pr-screenshots
mkdir -p /tmp/pr-screenshots
npx --yes yarn@1.22.22 playwright test \
--config e2e/playwright.config.js \
e2e/tests/_pr-capture.spec.js \
--workers=1
rm -f e2e/tests/_pr-capture.spec.js
rm -rf /tmp/pr-screenshots
```

Before reporting success, confirm each screenshot is larger than 10 KB, the changed feature is visible, and the capture is not only a blank shell, loading state, or page header. If the server fails, inspect the full Playwright web-server output; common causes here are the wrong Node runtime, a stale port 3000 process, or an old CRA dependency failure.

## Before and After Capture

For visual changes, capture the same spec on the current branch and on the merge base so reviewers can compare the result:

1. Capture the current branch into a clean output directory and keep the files with their commit SHA.
2. Set `BASE_SHA=$(git merge-base origin/master HEAD)` and create a detached temporary worktree at that SHA.
3. Copy the temporary capture spec and any local environment file needed for the render into the worktree, install the `web` Yarn dependencies, and run the same config.
4. Save the base screenshots with a `-before` suffix, remove the temporary worktree, and keep after-only evidence if the base tree cannot render.

Never reuse a stale output directory between the before and after passes. A new page or a fixture drift is a valid reason to omit the before image; it is not a reason to discard a valid after capture.

## Upload and Embed in the PR

In a CodePress cloud session, use `upload_pr_asset` for each PNG, GIF, and original video. Embed the returned permanent URL in the PR description, wrapping images as a link so reviewers can open the full resolution.

For the local fallback, upload assets to the `pr-assets` release in `quantfive/django-react-intro`:

```bash
gh release upload pr-assets /tmp/pr-screenshots/pr-screenshot-*.png \
--repo quantfive/django-react-intro --clobber
gh release upload pr-assets /tmp/pr-screenshots/pr-video-*.webm \
--repo quantfive/django-react-intro --clobber
```

Add a `## Demo` section to the PR body and identify the capture commit. Use a before/after table when both images exist:

```markdown
## Demo

<!-- pr-screenshot: captured at abc1234 -->

| Before | After |
| --- | --- |
| [![before](BEFORE_URL)](BEFORE_URL) | [![after](AFTER_URL)](AFTER_URL) |
```

Convert WebM interaction recordings to GIF for inline playback and link the original WebM separately. Do not use HTML video tags in GitHub markdown.

## GitHub Release Setup

The release fallback is only needed when `upload_pr_asset` is unavailable. Create it once with:

```bash
gh release create pr-assets \
--repo quantfive/django-react-intro \
--title "PR Assets" \
--notes "Screenshots and assets referenced in pull requests." \
--latest=false
```

## Tips

- Prefer a bounded viewport over `fullPage` so the changed feature remains readable.
- Capture every route affected by the diff; this repository currently has only `/`.
- Add a 390 by 844 capture for mobile or responsive changes.
- Keep videos under 10 seconds and trim loading pre-roll when possible.
- Skip screenshots for structural-only changes such as selector renames with no visual effect.

## Known Issues at Bootstrap Time

- The existing Live Dev Server Dockerfile is source-mount dependent and cannot be used as a standalone `build_and_start_app_server` image.
- The legacy CRA dependency tree requires Node 18; Node 25 fails before the app starts because its `websocket-driver` expects the removed `http_parser` binding.
- A real screenshot was captured successfully from an isolated Node 18 container at `/`, with a 1440 by 1000 viewport and a 22 KB PNG result. The route returned HTTP 200 and rendered the `Welcome to React` heading and logo.
104 changes: 104 additions & 0 deletions .claude/skills/start-app-server/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
name: start-app-server
description: "Start the quantfive/django-react-intro app server in a Docker container and validate it responds. Uses a pre-validated recipe with no discovery or guessing. Triggers on: 'spin up the server', 'run my app', 'start the server', 'verify app server', 'test my server', 'run app server', 'spin up the app'."
user_invocable: true
codepress_generated: true
---

# Start App Server — quantfive/django-react-intro

Fast-path skill for starting this repository's app server. Discovery and one repair were completed during bootstrap on 2026-08-06T18:37:57Z; execute the recipe below rather than rediscovering the stack.

The machine-readable recipe lives at `.codepress/start-app-server/recipe.json`.

## Tools

Use these tools directly:

- `build_and_start_app_server` — build the image and start the container
- `forward_app_request` — send an HTTP request into the running container
- `get_app_server_logs` — inspect container stdout and stderr
- `stop_app_server` — stop and remove a container when cleanup is required

## Static Context

- **Stack**: Django 1.11.5 with a Create React App 1.0.10 frontend; Node 18 builds the frontend and Python 3.6 runs Django.
- **Dockerfile**: `Dockerfile`
- **Port**: `8000`
- **Validation**: `GET /` with status in `[200, 301, 302, 404]`
- **Services**: none; the app uses repository-local SQLite.
- **Required secrets**: none.

## Step 1: Drift Check

Compare the current inputs with the recipe checksums:

```bash
git hash-object Dockerfile # expected prefix: 55b86d98d1d0e26a
git hash-object server/Pipfile # expected prefix: a4af380b1d34c482
```

If either value differs, proceed with the current files but report the drift. The build is still the source of truth for whether the server works.

## Step 2: Fetch Secrets

This app has no vault-backed secrets. Use an empty environment map and continue.

## Step 3: Build and Start

Call:

```text
build_and_start_app_server(
workspaceDir=<absolute path to the repository root>,
port=8000,
dockerfilePath="Dockerfile",
envVars={}
)
```

The Dockerfile installs both dependency trees inside the image. It builds `web/` with Yarn 1.22.22, copies the resulting assets to `server/static/build`, installs the locked Python dependencies from `server/Pipfile.lock`, applies the initial SQLite migrations, and starts Django on `0.0.0.0:8000`.

If retrying after a fix in the same session, pass `existingContainerId` from the previous attempt so the old container is replaced cleanly.

## Step 4: Validate

If the start tool reports that the health check is ready, send:

```text
forward_app_request(
containerId=<container id>,
path="/",
method="GET"
)
```

Accept status `200`, `301`, `302`, or `404`. The expected healthy response for this repository is `HTTP 200` with the React HTML shell and `/static/js/` and `/static/css/` asset references.

If health is timed out, poll `GET /` up to 12 times at 5-second intervals. Inspect `get_app_server_logs` before the final retry. A crash, a non-allowlisted 4xx, a 5xx response, or exhaustion of the poll budget is a failure.

## Step 5: Report

Report the container ID, port `8000`, the `forward_app_request` command for the root route, and the `stop_app_server` command. Leave a successfully started container running so the caller can continue verification. Stop only an explicitly requested teardown or a failed container that would otherwise leak.

## Known Fixes

- The legacy CRA `postbuild` script copies into `../server/static/build`; the Dockerfile creates `server/static` before `yarn build`.
- Django's SQLite path is inside `/app/server`, while the container runs as UID 65534; the Dockerfile chowns that tree before startup.
- The old frontend uses Yarn 1.22.22 and is built with Node 18. The install command uses `--ignore-engines` because the repository now includes the Playwright package for screenshot capture.
- Django auth/admin routes need the initial SQLite schema, so migrations run during the image build before the app directory is chowned for the runtime user.
- The app binds to `0.0.0.0:8000` in the foreground so the container proxy can reach it.

## Repair on Failure

Before repairing, read `repair_count` from `.codepress/start-app-server/recipe.json`. If it is already 3 or higher, stop and report that the recipe needs a fresh bootstrap rather than attempting another repair.

For a repair attempt:

1. Read the full build error and `get_app_server_logs` output.
2. Fix all related issues in one edit pass; do not rebuild after only the first symptom.
3. Retry `build_and_start_app_server` with the previous `existingContainerId`.
4. If the recipe itself changes, increment `repair_count`, set `origin` to `repair`, update `bootstrapped_at`, recompute the two input checksums, and append the fix to `known_fixes`.
5. Re-run the HTTP validation before treating the repair as successful.

Never put credentials in the recipe or Dockerfile. Do not solve a failed HTTP check by weakening the verification status list.
Loading