Skip to content
Draft
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
26 changes: 20 additions & 6 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
fly.toml
/node_modules
# Dependencies (reinstalled in Docker)
node_modules

# Build artifacts (regenerated in Docker)
.cache
/build
/.react-router

# Environment files (provided at runtime)
.env

# Version control (not needed in image)
.git
.github

# Testing (not needed in production)
test
test-results

# System files
*.log
.DS_Store
.env
/.cache
/public/build
/build
31 changes: 1 addition & 30 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Playwright Tests & Chromatic
name: Playwright Tests

# This is an unusual job because it's triggered by deploy events rather than
# PR/push. The if condition means we only run on deployment_status events where
Expand Down Expand Up @@ -36,32 +36,3 @@ jobs:
name: test-results
path: test-results/
retention-days: 30

chromatic:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to switch to a local regression test like the console and docs

name: Run Chromatic
if:
github.event_name == 'deployment_status' && github.event.deployment_status.state ==
'success'
needs: playwright
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Download Playwright test results
uses: actions/download-artifact@v4
with:
name: test-results
path: ./test-results
- name: Run Chromatic
run:
npx chromatic --playwright --project-token ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
--exit-zero-on-changes
env:
CHROMATIC_ARCHIVE_LOCATION: ./test-results
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,4 @@ test-results/

/app/components/icons

// chromatic
build-archive.log
test-results/

.react-router/
34 changes: 34 additions & 0 deletions .infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ Note: Infrastructure configuration is stored in this repository until a point in
we have RFD infrastructure that is separate from `cio`. At that point, this infrastructure
should be owned by the RFD service.

## Storage Provider Configuration

The application supports two storage backends for serving static assets: GCS (Google Cloud
Storage) and S3 (AWS S3 or S3-compatible services).

### Common Environment Variables

| Variable | Description |
| ------------------ | -------------------------------------------------------------------------- |
| `STORAGE_PROVIDER` | Storage backend to use: `gcs` (default) or `s3` |
| `STORAGE_URL_TTL` | Pre-signed URL expiration time in seconds (optional, defaults to 24 hours) |

### GCS Configuration

| Variable | Description |
| ------------------ | ------------------------------ |
| `STORAGE_URL` | Base URL of the GCS CDN bucket |
| `STORAGE_KEY_NAME` | Name of the signing key |
| `STORAGE_KEY` | Base64-encoded signing key |

### S3 Configuration

| Variable | Description |
| ----------------------- | ------------------------------------------------------------ |
| `S3_BUCKET` | S3 bucket name |
| `AWS_REGION` | AWS region (standard AWS SDK variable) |
| `AWS_ACCESS_KEY_ID` | AWS access key (standard AWS SDK variable, or use IAM roles) |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key (standard AWS SDK variable, or use IAM roles) |
| `AWS_ENDPOINT_URL` | Custom endpoint for S3-compatible services (optional) |

The S3 integration uses the AWS SDK default credential chain, so credentials can be provided
via environment variables, IAM instance roles, ECS task roles, or other standard AWS
methods.

### GCP Infrastructure

Image storage and serving is handled by
Expand Down
85 changes: 85 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository.

## Project Overview

RFD Site is Oxide Computer Company's web frontend for browsing, searching, and reading RFDs
(Requests for Discussion). Built with React Router v7 (formerly Remix) and deployed on
Vercel.

## Commands

```bash
npm run dev # Start dev server on localhost:3000
npm run build # Production build (react-router build)
npm run test # Run unit tests with Vitest
npm run tsc # Type check without emitting
npm run lint # ESLint
npm run fmt # Format with Prettier
npm run fmt:check # Check formatting
npm run e2ec # Run Playwright E2E tests (Chrome)
```

### Running a Single Test

```bash
npm run test -- path/to/file.test.ts # Run specific test file
npm run test -- --grep "test name" # Run tests matching pattern
npx playwright test --project=chrome test.ts # Run specific E2E test
```

### Local RFD Authoring Mode

Preview RFDs from a local clone of the rfd repo:

```bash
LOCAL_RFD_REPO=~/oxide/rfd npm run dev
```

This mode reads RFD files directly from the specified directory without needing API
credentials.

## Architecture

### Data Flow: Local vs Remote Mode

The app operates in two modes controlled by `LOCAL_RFD_REPO` env var:

- **Local mode** (`app/services/rfd.local.server.ts`): Reads AsciiDoc files directly from a
local rfd repo clone. Used for authoring/previewing.
- **Remote mode** (`app/services/rfd.remote.server.ts`): Fetches from the rfd-api backend.
Used in production with OAuth authentication.

The unified interface in `app/services/rfd.server.ts` abstracts this, calling either backend
based on `isLocalMode()`.

### Routing

Uses React Router v7 file-based routing (`@react-router/fs-routes`). Routes are in
`app/routes/`:

- `_index.tsx` - RFD listing page
- `rfd.$slug.tsx` - Individual RFD view
- `auth.*.tsx` - OAuth flows (GitHub, Google)
- `api.*.tsx` - API endpoints

### Content Rendering

RFDs are written in AsciiDoc and rendered with `@oxide/react-asciidoc`. Custom block
renderers live in `app/components/AsciidocBlocks/` (Mermaid diagrams, syntax-highlighted
code listings, images).

### Path Alias

`~/` maps to `./app/` (configured in tsconfig.json).

## Key Dependencies

- `@oxide/react-asciidoc` - AsciiDoc renderer
- `@oxide/rfd.ts` - TypeScript client for rfd-api
- `@oxide/design-system` - Oxide's component library
- `@tanstack/react-query` - Data fetching for PR discussions
- `shiki` - Syntax highlighting
- `mermaid` - Diagram rendering
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM node:22-alpine AS development-dependencies-env
COPY . /app
WORKDIR /app
RUN npm ci

FROM node:22-alpine AS production-dependencies-env
COPY ./package.json package-lock.json /app/
WORKDIR /app
RUN npm ci --omit=dev

FROM node:22-alpine AS build-env
COPY . /app/
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
WORKDIR /app
RUN npm run build

FROM node:22-alpine
ENV NODE_ENV=production
COPY ./package.json package-lock.json /app/
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
COPY --from=build-env /app/build /app/build
COPY --from=build-env /app/public /app/public
WORKDIR /app
CMD ["npm", "run", "start"]
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,54 @@ combined branch that contains both.
When running in a non-local mode, the following settings must be specified:

- `SESSION_SECRET` - Key that will be used to signed cookies
- `RFD_API` - Backend RFD API to communicate with (i.e. https://api.server.com)
- `RFD_API_CLIENT_ID` - OAuth client id create via the RFD API
- `RFD_API_CLIENT_SECRET` - OAuth client secret create via the RFD API

#### Authentication

##### API URL Configuration

The RFD API URL can be configured in two ways:

- `RFD_API` - Single URL for both server-to-server calls and OAuth redirects (legacy,
simplest)
- `RFD_API_BACKEND_URL` + `RFD_API_FRONTEND_URL` - Split URLs for deployments where rfd-site
uses internal networking to reach rfd-api while users access a public endpoint

When using split URLs:

- `RFD_API_BACKEND_URL` - URL for server-to-server API calls (e.g., internal load balancer)
- `RFD_API_FRONTEND_URL` - URL for OAuth redirects where user's browser is directed

You can mix configurations: set one of the new vars and use `RFD_API` as fallback for the
other. Existing deployments using only `RFD_API` will continue to work unchanged.

##### OAuth Credentials

- `RFD_API_CLIENT_ID` - OAuth client id created via the RFD API
- `RFD_API_CLIENT_SECRET` - OAuth client secret created via the RFD API
- `RFD_API_GOOGLE_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/google/callback`
- `RFD_API_GITHUB_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/github/callback`
- `RFD_API_MLINK_SECRET` - Client secret for magic link (email) authentication

- `AUTH_PROVIDERS` - Comma-delimited list of enabled authentication providers. Valid values
are `github`, `google`, and `email`. If not set, no providers are enabled and login will
be unavailable. Each listed provider must have its required environment variables set or
the app will fail to start. Examples:
- `AUTH_PROVIDERS=github,google` - Enable only GitHub and Google OAuth
- `AUTH_PROVIDERS=email` - Enable only email (magic link) authentication

#### Storage

- `STORAGE_URL` - Url of bucket for static assets
- `STORAGE_KEY_NAME` - Name of the key defined in `STORAGE_KEY`
- `STORAGE_KEY` - Key for generating signed static asset urls

See [`.infra/README.md`](.infra/README.md) for S3 and additional storage provider
configuration.

#### GitHub Integration

- `GITHUB_APP_ID` - App id for fetching GitHub PR discussions
- `GITHUB_INSTALLATION_ID` - Installation id of GitHub App
- `GITHUB_PRIVATE_KEY` - Private key of the GitHub app for discussion fetching
Expand Down
24 changes: 14 additions & 10 deletions app/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type SmallRfdItems = {
}

export default function Header({ currentRfd }: { currentRfd?: RfdItem }) {
const { user, rfds, localMode, inlineComments } = useRootLoaderData()
const { user, rfds, localMode, inlineComments, features } = useRootLoaderData()

const fetcher = useFetcher()

Expand All @@ -59,7 +59,7 @@ export default function Header({ currentRfd }: { currentRfd?: RfdItem }) {
return false // Returning false prevents default behaviour in Firefox
}, [open])

useKey('mod+k', toggleSearchMenu, { global: true })
useKey('mod+k', toggleSearchMenu, { global: true, enabled: features.search })

return (
<div className="sticky top-0 z-20">
Expand All @@ -78,14 +78,18 @@ export default function Header({ currentRfd }: { currentRfd?: RfdItem }) {
</div>

<div className="flex gap-2">
<button
className="text-tertiary bg-secondary border-secondary elevation-1 hover:bg-hover flex h-8 w-8 items-center justify-center rounded border"
onClick={toggleSearchMenu}
aria-label="Search"
>
<Icon name="search" size={16} />
</button>
<Search open={open} onClose={() => setOpen(false)} />
{features.search && (
<>
<button
className="text-tertiary bg-secondary border-secondary elevation-1 hover:bg-hover flex h-8 w-8 items-center justify-center rounded border"
onClick={toggleSearchMenu}
aria-label="Search"
>
<Icon name="search" size={16} />
</button>
<Search open={open} onClose={() => setOpen(false)} />
</>
)}
<NewRfdButton />
<ThemeDropdown />

Expand Down
4 changes: 2 additions & 2 deletions app/components/NewRfdButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Modal from './Modal'

const NewRfdButton = () => {
const dialog = useDialogStore()
const newRfdNumber = useRootLoaderData().newRfdNumber
const { newRfdNumber, config } = useRootLoaderData()

return (
<>
Expand All @@ -31,7 +31,7 @@ const NewRfdButton = () => {
<p>
There is a prototype script in the rfd{' '}
<a
href="https://github.com/oxidecomputer/rfd"
href={config.repository.url}
className="text-accent-tertiary hover:text-accent-secondary"
>
repository
Expand Down
Loading
Loading