278 lines
8.8 KiB
Markdown
278 lines
8.8 KiB
Markdown
# Loam — MTB Build & Tune
|
|
|
|
A Next.js web app and native Android client for mountain bike riders to configure builds, get manufacturer-verified suspension and tire setup starting points, save post-ride reviews, and receive AI-powered tuning recommendations based on personal preference history.
|
|
|
|
The backend is built with Next.js App Router, Prisma/PostgreSQL, and Ollama for local LLM inference. The Android client is built with Kotlin, Jetpack Compose, Ktor, and Koin against the same JSON API.
|
|
|
|
---
|
|
|
|
## Tech stack
|
|
|
|
| Layer | Tech |
|
|
|---|---|
|
|
| Framework | Next.js 16 (App Router) |
|
|
| Language | TypeScript |
|
|
| Database | PostgreSQL |
|
|
| ORM | Prisma |
|
|
| Auth | bcryptjs + jose (JWT in HTTP-only cookies) |
|
|
| Images | sharp (layer compositing) |
|
|
| AI | Ollama (local LLM) |
|
|
| Validation | Zod |
|
|
| Android | Kotlin, Jetpack Compose, Ktor, Koin |
|
|
|
|
---
|
|
|
|
## Features
|
|
|
|
- **Catalog** — Curated frames, forks, shocks, and tires with real-world setup ranges
|
|
- **Setup verification** — Every product carries a research audit trail (manufacturer docs, community sources, confidence levels)
|
|
- **Build composer** — Layered bike image generation from selected components
|
|
- **Per-user learning** — Builds, reviews, and learned preferences are scoped to authenticated users
|
|
- **AI recommendations** — Ollama-powered tuning advice that incorporates the rider's own review history
|
|
- **Post-ride reviews** — Rate suspension feel, grip, rolling speed, and report issues (bottom-outs, harsh hits, wallow, small-bump harshness)
|
|
|
|
---
|
|
|
|
## Getting started
|
|
|
|
### Prerequisites
|
|
|
|
- Node.js 20+
|
|
- PostgreSQL running locally (or a remote instance)
|
|
- Ollama installed and running (optional, for AI recommendations)
|
|
|
|
### 1. Install dependencies
|
|
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
### 2. Configure environment variables
|
|
|
|
```bash
|
|
cp .env.example .env.local
|
|
```
|
|
|
|
Edit `.env.local`:
|
|
|
|
```
|
|
DATABASE_URL=postgresql://user:password@localhost:5432/loam
|
|
JWT_SECRET=change-this-to-a-long-random-string-in-production
|
|
|
|
# Optional — recommended local model shown
|
|
OLLAMA_URL=http://localhost:11434
|
|
OLLAMA_MODEL=qwen3:32b
|
|
```
|
|
|
|
### 3. Set up the database
|
|
|
|
```bash
|
|
# Create and apply migrations
|
|
npm run db:migrate
|
|
|
|
# Seed product catalog
|
|
npm run db:seed
|
|
```
|
|
|
|
### 4. Pull an Ollama model (optional)
|
|
|
|
```bash
|
|
ollama pull qwen3:32b
|
|
```
|
|
|
|
`qwen3:32b` is the recommended local model for the recommendation endpoint on a 32GB VRAM GPU such as the AMD Radeon AI PRO R9700. Use `qwen3:14b` if you want faster responses or need a smaller memory footprint.
|
|
|
|
### 5. Run the dev server
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
Open [http://localhost:3000](http://localhost:3000).
|
|
|
|
---
|
|
|
|
## Docker
|
|
|
|
Build and run the web app plus PostgreSQL:
|
|
|
|
```bash
|
|
docker compose up --build
|
|
```
|
|
|
|
The app is exposed at [http://localhost:3333](http://localhost:3333). The Compose stack provides `DATABASE_URL` and `JWT_SECRET` for the app container.
|
|
|
|
The Docker image installs dependencies with `npm ci --ignore-scripts`; Prisma generation runs later in the builder stage after `prisma/schema.prisma` has been copied into the image. The base image also installs OpenSSL so Prisma's native query engine works on Alpine.
|
|
|
|
### Ollama in a Separate Container
|
|
|
|
The app expects Ollama at `OLLAMA_URL`. If Ollama runs in another container, expose port `11434` and point the app at that service or host address.
|
|
|
|
For an AMD GPU with ROCm, a typical Ollama container launch is:
|
|
|
|
```bash
|
|
docker run --rm -it \
|
|
--device=/dev/kfd \
|
|
--device=/dev/dri \
|
|
--group-add video \
|
|
--group-add render \
|
|
-v ollama:/root/.ollama \
|
|
-p 11434:11434 \
|
|
ollama/ollama:rocm
|
|
```
|
|
|
|
Then pull the recommended model:
|
|
|
|
```bash
|
|
ollama pull qwen3:32b
|
|
```
|
|
|
|
If the app runs in Docker Compose and Ollama is bound to the host on Linux, set:
|
|
|
|
```yaml
|
|
environment:
|
|
OLLAMA_URL: http://host.docker.internal:11434
|
|
OLLAMA_MODEL: qwen3:32b
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
```
|
|
|
|
---
|
|
|
|
## Project structure
|
|
|
|
```
|
|
prisma/
|
|
schema.prisma # User, BikeBuild, RideReview, RiderPreference, Product
|
|
seed.ts # Seeds catalog into Product table
|
|
src/
|
|
app/
|
|
api/
|
|
auth/ # register, login, me, logout
|
|
builds/ # list / create builds (scoped to user)
|
|
reviews/ # create reviews (scoped to user + build ownership)
|
|
ai/recommend # Ollama recommendation prompt
|
|
images/compose # sharp layer compositing
|
|
catalog/ # public catalog + setupVerification
|
|
setup-audit/ # research audit endpoint
|
|
components/
|
|
BikeBuilderApp.tsx
|
|
lib/
|
|
auth.ts # bcrypt, JWT cookie session, requireAuth
|
|
db.ts # per-user Prisma queries
|
|
catalog.ts # static product data with citations
|
|
recommendations.ts # setup math by rider profile
|
|
setupVerification.ts # research audit metadata
|
|
types.ts # shared TypeScript types
|
|
public/
|
|
images/ # product assets + user-generated builds
|
|
android/
|
|
app/src/main/java/com/bikeloam/app/
|
|
data/ # DTOs, Ktor API client, repository
|
|
navigation/ # Compose navigation graph
|
|
ui/ # Compose screens, theme, ViewModels
|
|
app/src/test/ # JVM unit tests for repository + ViewModels
|
|
```
|
|
|
|
---
|
|
|
|
## API routes
|
|
|
|
| Route | Method | Auth | Description |
|
|
|---|---|---|---|
|
|
| `/api/auth/register` | POST | No | Create account, set session cookie |
|
|
| `/api/auth/login` | POST | No | Log in, set session cookie |
|
|
| `/api/auth/me` | GET | No | Return current session user |
|
|
| `/api/auth/logout` | POST | No | Clear session cookie |
|
|
| `/api/builds` | GET | Yes | List current user's builds |
|
|
| `/api/builds` | POST | Yes | Save a new build |
|
|
| `/api/reviews` | POST | Yes | Review a build (must own it) |
|
|
| `/api/ai/recommend` | POST | Yes | AI tuning advice with user preferences |
|
|
| `/api/images/compose` | POST | No | Compose build image from layers |
|
|
| `/api/catalog` | GET | No | Full catalog + setupVerification |
|
|
| `/api/setup-audit` | GET | No | Research audit report |
|
|
|
|
---
|
|
|
|
## Auth system
|
|
|
|
- Passwords hashed with bcrypt (12 rounds)
|
|
- Sessions are JWTs stored in HTTP-only `loam-session` cookies (7-day expiry)
|
|
- `requireAuth()` guards data-modifying routes
|
|
- All builds, reviews, and preferences are scoped to the authenticated `userId`
|
|
|
|
---
|
|
|
|
## Data model overview
|
|
|
|
- **User** — email (unique), passwordHash, name
|
|
- **BikeBuild** — belongs to User; stores component IDs, rider profile, setup JSON, image path
|
|
- **RideReview** — belongs to User + BikeBuild; ratings and issue flags
|
|
- **RiderPreference** — one per User; aggregated from review history (harsh/soft tendency, grip preference)
|
|
- **Product** — seeded catalog of frames, forks, shocks, tires with specs and sources JSON
|
|
|
|
---
|
|
|
|
## Research & citations
|
|
|
|
Every product entry includes a `sources` array and a `SetupVerification` record:
|
|
|
|
- `manufacturer` — official setup guides, owner manuals, product pages
|
|
- `community` — Pinkbike reviews, Reddit discussions, Blister/MTBR test notes
|
|
- Status levels: `verified` → `manufacturer-guided` → `community-informed` → `estimated`
|
|
|
|
The `/api/setup-audit` endpoint returns a full breakdown of confidence levels and missing sources.
|
|
|
|
---
|
|
|
|
## Android client
|
|
|
|
The native Android app lives in [`android/`](android/README.md). It uses the same plain JSON API over HTTP with cookie-based auth:
|
|
|
|
1. Implementing cookie jar storage for the `loam-session` cookie
|
|
2. Calling `/api/auth/login` or `/api/auth/register` to obtain a session
|
|
3. Reusing `/api/builds`, `/api/reviews`, `/api/ai/recommend`, and `/api/catalog` as-is
|
|
|
|
For local emulator development, the debug build points at `http://10.0.2.2:3000`. Start the web backend with `npm run dev`, then build or test the app from the Android project:
|
|
|
|
```bash
|
|
cd android
|
|
./gradlew assembleDebug
|
|
./gradlew testDebugUnitTest
|
|
```
|
|
|
|
The image composition endpoint (`/api/images/compose`) returns a PNG path; mobile clients can either request server-side composition or replicate the layer-stacking logic locally.
|
|
|
|
---
|
|
|
|
## Scripts
|
|
|
|
| Command | Purpose |
|
|
|---|---|
|
|
| `npm run dev` | Start Next.js dev server |
|
|
| `npm run build` | Production build |
|
|
| `npm run db:migrate` | Run Prisma migrations |
|
|
| `npm run db:seed` | Seed product catalog |
|
|
| `npm run db:studio` | Open Prisma Studio |
|
|
| `npm run lint` | Run ESLint |
|
|
|
|
Android commands are run from `android/`:
|
|
|
|
| Command | Purpose |
|
|
|---|---|
|
|
| `./gradlew assembleDebug` | Build the debug APK |
|
|
| `./gradlew installDebug` | Install debug build on a connected device or emulator |
|
|
| `./gradlew testDebugUnitTest` | Run JVM unit tests |
|
|
| `./gradlew connectedDebugAndroidTest` | Run instrumented Android tests |
|
|
|
|
---
|
|
|
|
## Environment variables
|
|
|
|
| Variable | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| `DATABASE_URL` | Yes | — | PostgreSQL connection string |
|
|
| `JWT_SECRET` | Yes | — | Secret for signing session JWTs |
|
|
| `OLLAMA_URL` | No | `http://localhost:11434` | Ollama server base URL |
|
|
| `OLLAMA_MODEL` | No | `llama3.1:8b` | Model name for recommendations. `qwen3:32b` is recommended for a 32GB VRAM local GPU. |
|