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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
data
.env
*.db
data-demo
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ vitest.config.*.timestamp*
.vitest

test-output

# Sample data from DEMO_MODE (see CONTRIBUTING.md)
/data-demo
68 changes: 68 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Contributing to openRun

Thanks for helping. openRun is a self-hosted running app for runners and their clubs, and it is small enough that one change can make a real difference.

## Ways to help

- **Try it and say what's missing.** The [demo](https://openrun-demo.fly.dev) resets every night, so click anything. Open an [issue](https://github.com/stretch0/openRun/issues) for bugs or ideas.
- **Run it for your club** and tell us what got in the way.
- **Pick up an issue.** Ones labelled [`good first issue`](https://github.com/stretch0/openRun/labels/good%20first%20issue) are small and self-contained. Say on the issue that you're working on it.
- **Bigger changes:** open an issue first, so we can agree the approach before you spend time on it. [ROADMAP.md](ROADMAP.md) has what's planned, and [docs/PRIVACY.md](docs/PRIVACY.md) explains the privacy rules every feature has to follow.

Security problems go through [private reporting](SECURITY.md), not public issues.

## Getting set up

You need Node 22 or later.

```bash
npm install
npm run dev
```

The API runs on http://localhost:3000 and the web app on http://localhost:4200. The first account you create becomes the admin.

To start with sample data instead (a runner with six months of runs, and a club with members, RSVPs and coffee orders), use the demo mode with its own data folder. It **replaces everything** in the database it's pointed at, so keep it away from `./data`:

```bash
DEMO_MODE=true DATA_DIR=./data-demo npm run dev
```

Then sign in with **Try the demo**.

[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) explains how the pieces fit together. In short:

| Where | What |
| --------------------- | -------------------------------------------------------------- |
| `apps/api` | Hono API server, which also serves the built web app |
| `apps/web` | React PWA |
| `apps/web-e2e` | Playwright tests against the production build |
| `libs/schemas` | Zod request and response shapes shared by the API and web app |
| `libs/domain` | Pure logic (pace maths, schedules, highlights) with unit tests |
| `libs/db` | Drizzle schema and database client |
| `libs/db-migrations` | Generated SQL migrations |
| `libs/activity-files` | FIT and GPX parsing |

## Conventions

- Shared request and response shapes go in `libs/schemas`, and both the API and the web app use them.
- Pure logic goes in `libs/domain` with unit tests, not in routes or components.
- Schema changes: edit `libs/db/src/schema`, then run `npm run db:generate -- --name=<description>` and commit the generated SQL. Never edit a migration that's already on `main`.
- Relative imports in the Node libraries use `.js` extensions.
- Privacy first: runs are private by default, and clubs only see what a member chose to send them. If a change touches what other people can see, say so in the pull request.
- Interface text is plain and short, in British English.

## Before you open a pull request

```bash
npx nx affected -t lint typecheck test build
npm run e2e
```

CI runs these too, plus CodeQL, a dependency review and an image scan. The end-to-end tests need Playwright's Chromium (`npx playwright install chromium`), or point `CHROMIUM_PATH` at a Chromium you already have.

Keep pull requests to one change, with tests for new behaviour. Describe what changed and why, and add screenshots for anything visible. Pull requests are squash-merged into `main`.

If the change affects the screenshots, retake them from a running demo with `node scripts/screenshots.mjs <url>` (see [docs/DEMO.md](docs/DEMO.md)).

By contributing, you agree that your work is licensed under the project's [AGPL-3.0-or-later licence](LICENSE).
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ PORT=3005 npm run dev

The repo is an [Nx](https://nx.dev) monorepo. See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for how it fits together.

Want to help? See [CONTRIBUTING.md](CONTRIBUTING.md).

## Licence

[AGPL-3.0-or-later](LICENSE). If you run a modified openRun for other people, you must share your changes.
2 changes: 2 additions & 0 deletions docs/DEMO.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ node scripts/screenshots.mjs http://localhost:3000
```

It needs Playwright's Chromium (`npx playwright install chromium`), or set `CHROMIUM_PATH`.

The repository's social preview card comes from the same screenshots. After retaking them, run `node scripts/social-preview.mjs` and upload `docs/images/social-preview.png` under Settings → General → Social preview (GitHub has no API for it).
Binary file added docs/images/social-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions scripts/social-preview.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Renders the repository's social preview card (1280x640) from the README
// screenshots, to docs/images/social-preview.png. Run it after
// scripts/screenshots.mjs, then upload the card in the repository's
// Settings → General → Social preview (GitHub has no API for it).
//
// node scripts/social-preview.mjs
import { readFileSync } from 'node:fs';
import { chromium } from '@playwright/test';

const images = new URL('../docs/images/', import.meta.url);
const dataUrl = (name) =>
`data:image/png;base64,${readFileSync(new URL(name, images)).toString('base64')}`;
const logo = readFileSync(
new URL('../apps/web/public/favicon.svg', import.meta.url),
'utf8',
);

const phone = (name, tilt, lift) => `
<div class="phone" style="transform: translateY(${lift}px) rotate(${tilt}deg)">
<img src="${dataUrl(name)}" alt="">
</div>`;

const html = `<!doctype html>
<html><head><meta charset="utf-8"><style>
* { box-sizing: border-box; margin: 0; }
body {
width: 1280px; height: 640px; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%);
color: #1c1917; display: flex; align-items: center;
}
.text { width: 560px; padding: 0 0 0 80px; }
.brand { display: flex; align-items: center; gap: 18px; margin-bottom: 28px; }
.brand svg { width: 72px; height: 72px; }
.brand span { font-size: 56px; font-weight: 700; letter-spacing: -1px; }
h1 { font-size: 40px; line-height: 1.15; font-weight: 700; letter-spacing: -0.5px; }
p { margin-top: 20px; font-size: 24px; line-height: 1.4; color: #44403c; }
.tags { margin-top: 28px; display: flex; gap: 10px; flex-wrap: wrap; }
.tags span {
background: #0d9488; color: #fff; border-radius: 999px;
padding: 6px 16px; font-size: 18px; font-weight: 600;
}
.phones { position: relative; flex: 1; height: 100%; display: flex;
align-items: center; justify-content: center; gap: 24px; padding-right: 40px; }
.phone {
width: 200px; border-radius: 28px; overflow: hidden; background: #fff;
box-shadow: 0 24px 48px rgba(15, 118, 110, 0.25), 0 0 0 6px #1c1917;
}
.phone img { display: block; width: 100%; }
</style></head><body>
<div class="text">
<div class="brand">${logo}<span>openRun</span></div>
<h1>The self-hosted running app for you and your run club</h1>
<p>Run log, watch sync, RSVPs, weather and the coffee order. Your runs stay on your server.</p>
<div class="tags"><span>Open source</span><span>Docker</span><span>PWA</span></div>
</div>
<div class="phones">
${phone('runs-light.png', -4, 20)}
${phone('run-light.png', 0, -20)}
${phone('club-run-light.png', 4, 20)}
</div>
</body></html>`;

const browser = await chromium.launch({
executablePath: process.env.CHROMIUM_PATH || undefined,
});
const page = await browser.newPage({
viewport: { width: 1280, height: 640 },
deviceScaleFactor: 1,
});
await page.setContent(html);
await page.screenshot({ path: new URL('social-preview.png', images).pathname });
await browser.close();
console.log('docs/images/social-preview.png');
Loading