Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
329ce35
Update .gitignore to include .env.schema, and add varlock dependency …
nealhorner Apr 26, 2026
47bdd36
Add .env.schema file for environment variable specifications, includi…
nealhorner Apr 26, 2026
01dc9f6
Update .gitignore to include .env.local and env.d.ts for better envir…
nealhorner May 3, 2026
561b881
Update .env.schema to define APP_ENV and add optional database URLs f…
nealhorner May 3, 2026
6487a1d
Update package.json description and add better-auth dependencies in p…
nealhorner May 3, 2026
d8e686a
Update cSpell configuration to include 'varlock' in the list of recog…
nealhorner May 3, 2026
0e1c671
Refactor Prisma configuration for SQLite and Postgres
nealhorner May 3, 2026
02f3ef6
Update package dependencies and enhance README with server environmen…
nealhorner May 3, 2026
dafb588
Rename DATABASE_URL to POSTGRES_DATABASE_URL in CI workflow for clarity
nealhorner May 3, 2026
e84f8af
Refactor database connection handling to use POSTGRES_DATABASE_URL an…
nealhorner May 3, 2026
a1c0629
Auth and onboarding end2end test
nealhorner May 3, 2026
eb529b8
Reformated code
nealhorner May 3, 2026
1d16f5e
Fix user creation logic in seed script to use appUser model and inclu…
nealhorner May 3, 2026
6d3898f
Reformated seed script
nealhorner May 3, 2026
a0bfdc0
Update random name generation logic with better randomness and expand…
nealhorner May 3, 2026
ca97d4b
Add TCP connection readiness check for sidecar server
nealhorner May 3, 2026
04941de
Enhance remote session guard with error handling for /me request
nealhorner May 3, 2026
58ac943
Add error handling for sign-in process to manage network errors
nealhorner May 3, 2026
873b7fc
Improve BETTER_AUTH_SECRET validation and logging for local-only mode
nealhorner May 3, 2026
816ee18
Add BETTER_AUTH_SECRET to Vitest configuration for enhanced authentic…
nealhorner May 3, 2026
d88a6c7
Formatted BETTER_AUTH_SECRET configuration for improved readability
nealhorner May 3, 2026
c611677
Refactor Playwright configuration to support multiple web servers and…
nealhorner May 3, 2026
89d1318
Refactor layout component to use reactive $page store for pathname ch…
nealhorner May 3, 2026
1852bec
Refactor public configuration fetching in layout component to improve…
nealhorner May 3, 2026
115afe5
Refactor guard function in layout component to streamline navigation …
nealhorner May 3, 2026
b485904
Update varlock to version 1.1.0 and integrate varlockVitePlugin in Vi…
nealhorner May 3, 2026
c684c83
Improve error handling in welcome page by adding try-catch for user f…
nealhorner May 3, 2026
bd1d472
Update CI workflow, package scripts, and configuration files; add .pr…
nealhorner May 11, 2026
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
21 changes: 21 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This env file uses @env-spec - see https://varlock.dev/env-spec for more info
#
# @defaultRequired=infer @defaultSensitive=false
# @generateTypes(lang=ts, path=env.d.ts)
# ----------

# @type=enum(development, production)
APP_ENV=development

# Postgres
# @optional @sensitive @example="postgresql://postgres:postgres@localhost:5432/trackr"
POSTGRES_DATABASE_URL=

# SQLite
# @optional @sensitive @example="file:./.local/trackr.sqlite"
SQLITE_DATABASE_URL=


# Auth
# @required @sensitive @example="00000000000000000000000000000000"
BETTER_AUTH_SECRET=
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
# (e.g. PRs from forks). Must match Postgres service env below.
env:
CI_PG_PASSWORD: ${{ secrets.CI_POSTGRES_PASSWORD }}
DATABASE_URL: postgresql://postgres:${{ secrets.CI_POSTGRES_PASSWORD }}@localhost:5432/trackr_test
POSTGRES_DATABASE_URL: postgresql://postgres:${{ secrets.CI_POSTGRES_PASSWORD }}@localhost:5432/trackr_test
services:
postgres:
image: postgres:16
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ packages/prisma/.local/

# Tauri
apps/desktop/src-tauri/gen/schemas/

# Environment variables
!.env.schema
.env.local
.env.*.local
env.d.ts
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["autoincrement", "Tauri", "Trackr", "Turborepo"]
"cSpell.words": ["autoincrement", "Tauri", "Trackr", "Turborepo", "varlock"]
}
86 changes: 86 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Contributing to Trackr

Thanks for your interest in Trackr. This guide covers how to work in the monorepo, what we expect before you open a pull request, and how CI validates changes.

For environment variables, database setup, and day-to-day dev commands, start with the [README](README.md) — especially **Local development**, **Database and Prisma**, and **End-to-end tests**.

## Prerequisites

- **Node.js** 22 or newer (LTS recommended)
- **npm** — the repo pins a version in the root `package.json` (`packageManager`); use [Corepack](https://nodejs.org/api/corepack.html) if you want npm to match automatically
- **PostgreSQL** — required for server development, integration tests, and most E2E flows
- **Desktop / Tauri** — Rust stable toolchain and platform libraries when you work on `apps/desktop` (see CI’s `desktop-build` job for Linux packages)

## Getting started

From the repository root:

```bash
npm install
```

Match CI more closely (clean install from the lockfile):

```bash
npm ci
```

Generate Prisma clients before running server or tests that touch the database:

```bash
npm run build -w @trackr/prisma
```

Apply migrations and seed a dev database when you need auth and sample data (see README for `POSTGRES_DATABASE_URL`):

```bash
npm run migrate:dev -w @trackr/prisma # or migrate:deploy in CI-like environments
npm run seed -w @trackr/prisma
```

Use workspace-scoped scripts when you only need one app, for example:

```bash
npm run dev -w @trackr/server
npm run dev -w @trackr/web
```

## Quality checks (run locally)

Run these from the **repo root** before opening a PR:

| Command | Purpose |
| ---------------------- | ------------------------------------------------------ |
| `npm run format` | Apply Prettier |
| `npm run check-format` | Verify formatting (also enforced via Husky pre-commit) |
| `npm run lint` | Turborepo lint across packages |
| `npm run test` | Unit / Vitest tasks |
| `npm run build` | Production builds (see CI note for desktop on Linux) |

**Integration tests** for the server need a real Postgres database and:

```bash
RUN_INTEGRATION=true npm run test -w @trackr/server
```

**E2E (Playwright):** install Chromium once (`npx playwright install chromium`), build the server if `dist/` is missing, then use the README’s E2E section. Onboarding E2E resets the database — use a disposable dev DB. Root shortcuts: `npm run e2e`, `npm run dev:e2e`, `npm run dev:e2e-desktop`.

## Pull requests

- **Target branch:** `main`
- **Describe the change** in the PR: what problem it solves and any trade-offs
- **Keep scope focused** — unrelated refactors make review harder
- **Update tests** when behavior changes; add coverage when it prevents regressions

CI (`.github/workflows/ci.yml`) runs on pushes and PRs to `main`: `npm ci`, Prisma generate, migrate + seed, lint, tests with `RUN_INTEGRATION=true`, Turbo build (desktop excluded on the generic Linux job), then E2E (desktop excluded on that job). A separate job builds the desktop app with Rust and Tauri dependencies.

Maintainers: configure the **`CI_POSTGRES_PASSWORD`** repository secret as described in the README so the Postgres service uses a strong password. Fork PRs use a documented fallback in the workflow.

## Issues and discussion

- **Bugs and features:** [GitHub Issues](https://github.com/nealhorner/trackr/issues)
- **Repository:** [github.com/nealhorner/trackr](https://github.com/nealhorner/trackr)

## License

Trackr is distributed under the terms in [LICENSE](LICENSE). Read that file before redistributing or relying on permission to modify the software; contribution and use may be limited by those terms.
38 changes: 27 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ This repository contains product and technical planning documents for a platform

## Documentation

If you want to contribute code or documentation, see the [contribution guide](CONTRIBUTING.md) for setup, quality checks, and how CI validates pull requests.

- [`docs/product-requirements.md`](docs/product-requirements.md)
- [`docs/technical-requirements.md`](docs/technical-requirements.md)
- [`docs/mvp-scope.md`](docs/mvp-scope.md)
Expand Down Expand Up @@ -60,6 +62,16 @@ From the repo root:
npm install
```

### Server env quick reference (self-hosted)

- **`POSTGRES_DATABASE_URL`** — Postgres connection string for `apps/server` and Prisma Postgres migrations (see [Database and Prisma](#database-and-prisma-packagesprisma) below).
- **`BETTER_AUTH_SECRET`** — at least 32 characters in production; used by Better Auth for session signing.
- **`BETTER_AUTH_URL`** — public base URL of the API (e.g. `https://trackr.example.com`) so auth cookies and OAuth redirects match your deployment.
- **`BETTER_AUTH_TRUSTED_ORIGINS`** — comma-separated web origins allowed to use cookies (e.g. your Svelte dev server and Tauri: `http://localhost:5173,https://tauri.localhost`).
- **`TRACKR_CONFIG_PATH`** — optional path to a JSON file that pre-fills first-time setup (tenant name, auth toggles). Does not replace secrets in production.
- **`TRACKR_SETUP_SECRET`** — if set, `POST /api/v1/setup/complete` requires header `X-Trackr-Setup-Token` with the same value.
- **OAuth (optional):** `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`, `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET`, `APPLE_CLIENT_ID` / `APPLE_CLIENT_SECRET`, `OKTA_CLIENT_ID` / `OKTA_CLIENT_SECRET` / `OKTA_ISSUER`.

To match CI exactly after cloning (requires a lockfile):

```bash
Expand Down Expand Up @@ -101,17 +113,17 @@ Prisma 7 uses separate schema files for **PostgreSQL** (server) and **SQLite** (

From the repo root after `npm install`:

| Command | What it does |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `npm run build -w @trackr/prisma` | `prisma generate` for **both** Postgres and SQLite clients |
| `npm run migrate:dev -w @trackr/prisma` | Create/apply Postgres migrations in dev (`DATABASE_URL` must point at Postgres; uses `prisma.config.ts`) |
| `npm run migrate:deploy -w @trackr/prisma` | Apply Postgres migrations (CI/production-style) |
| `npm run db:push:sqlite -w @trackr/prisma` | SQLite `db push` for local desktop iteration (uses `file:./.local/trackr.sqlite` under `packages/prisma`) |
| `npm run seed -w @trackr/prisma` | Seed dev tenant, org, project, board columns, and users (`admin@example.com`, `dev@example.com`) — requires Postgres reachable via `DATABASE_URL` and migrations applied |
| Command | What it does |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `npm run build -w @trackr/prisma` | `prisma generate` for **both** Postgres and SQLite clients |
| `npm run migrate:dev -w @trackr/prisma` | Create/apply Postgres migrations in dev (`POSTGRES_DATABASE_URL` must point at Postgres; uses `prisma.config.ts`) |
| `npm run migrate:deploy -w @trackr/prisma` | Apply Postgres migrations (CI/production-style) |
| `npm run db:push:sqlite -w @trackr/prisma` | SQLite `db push` for local desktop iteration (uses `file:./.local/trackr.sqlite` under `packages/prisma`) |
| `npm run seed -w @trackr/prisma` | Seed dev tenant, org, project, board columns, and users (`admin@example.com`, `dev@example.com`) — requires Postgres reachable via `POSTGRES_DATABASE_URL` and migrations applied |

Full workflow notes: [`docs/phases/phase-0-foundations.md`](docs/phases/phase-0-foundations.md) (database section and stack decision links).

**Phase 1 server + web:** the API persists to **PostgreSQL** via Prisma 7’s **driver adapter** (`@prisma/adapter-pg` + `pg` in `apps/server`). Set `DATABASE_URL` if your Postgres is not the default in `prisma.config.ts`. After migrations, run **`npm run seed -w @trackr/prisma`** once. Start the API on port **3000**, then the web app (Vite dev server proxies **`/api`** to the API so session cookies stay same-site):
**Phase 1 server + web:** the API persists to **PostgreSQL** via Prisma 7’s **driver adapter** (`@prisma/adapter-pg` + `pg` in `apps/server`). Set `POSTGRES_DATABASE_URL` if your Postgres is not the default in `prisma.config.ts`. After migrations, run **`npm run seed -w @trackr/prisma`** once. Start the API on port **3000**, then the web app (Vite dev server proxies **`/api`** to the API so session cookies stay same-site):

```bash
npm run build -w @trackr/server && npm run dev -w @trackr/server
Expand Down Expand Up @@ -150,13 +162,17 @@ npm run e2e
npm run e2e -w @trackr/server
```

When you run E2E from `apps/server`, Playwright starts the server via `webServer` using `npm run dev`, which runs **compiled** output (`node dist/index.js`). **Build the server first** if `apps/server/dist` is missing:
When you run E2E from `apps/server`, Playwright starts **API + web** via the root script **`npm run dev:e2e`** (API first, then Vite on port 5173). **Build the server first** if `apps/server/dist` is missing:

```bash
npm run build -w @trackr/server
npm run e2e -w @trackr/server
POSTGRES_DATABASE_URL="postgresql://USER:PASSWORD@127.0.0.1:5432/trackr" npm run e2e -w @trackr/server
```

**Onboarding E2E** (`apps/server/e2e/onboarding.spec.ts`) runs a **full setup → login → welcome** flow against the Svelte app. A **global setup** step runs `prisma migrate reset --force --skip-seed` so the instance starts unconfigured — use a dev database you can wipe. Ensure ports **3000** and **5173** are free. To attach to dev servers you already started instead: `REUSE_E2E_SERVERS=1 npm run e2e -w @trackr/server`.

The legacy **HTML shell smoke** test (`ui-shell-smoke.spec.ts`) still hits **port 3000** only.

### Formatting

```bash
Expand All @@ -171,7 +187,7 @@ CI uses an ephemeral Postgres container. Add a **repository secret** so credenti
1. In the GitHub repo: **Settings → Secrets and variables → Actions → New repository secret**.
2. Name: **`CI_POSTGRES_PASSWORD`**
3. Value: any strong random string used **only for CI** (the workflow builds
`DATABASE_URL=postgresql://postgres:<secret>@localhost:5432/trackr_test` and sets the same value on the Postgres service).
`POSTGRES_DATABASE_URL=postgresql://postgres:<secret>@localhost:5432/trackr_test` and sets the same value on the Postgres service).

Use a password that does **not** contain URL-reserved characters (`@`, `:`, `/`, `#`, `?`) unless you adjust the workflow to percent-encode them. Pull requests from **forks** do not receive repo secrets by default; this workflow falls back to a local CI-only password so the Postgres service still starts on fork PR runs.

Expand Down
33 changes: 33 additions & 0 deletions apps/desktop/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { execSync } from "node:child_process";
import path from "node:path";

/**
* Same Postgres reset as `apps/server/e2e` so desktop remote tests see a fresh instance.
* When running `turbo e2e`, use root `npm run e2e` (concurrency=1) so this does not race
* another package's migrate reset.
*/
export default async function globalSetup(): Promise<void> {
const dbUrl =
process.env.POSTGRES_DATABASE_URL ?? process.env.DATABASE_URL ?? "";
if (!dbUrl) {
console.warn(
"[desktop playwright global-setup] POSTGRES_DATABASE_URL not set — skipping prisma migrate reset.",
);
return;
}

const repoRoot = path.resolve(process.cwd(), "..", "..");
const prismaDir = path.join(repoRoot, "packages", "prisma");

execSync(
"npx prisma migrate reset --force --skip-seed --config prisma.config.ts",
{
cwd: prismaDir,
stdio: "inherit",
env: {
...process.env,
POSTGRES_DATABASE_URL: dbUrl,
},
},
);
}
106 changes: 106 additions & 0 deletions apps/desktop/e2e/onboarding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { test, expect } from "@playwright/test";

import { installTauriE2EBridge } from "./tauri-e2e-bridge";

const API = "http://127.0.0.1:3000";

async function waitForApi(): Promise<void> {
const deadline = Date.now() + 120_000;
while (Date.now() < deadline) {
try {
const r = await fetch(`${API}/health`);
if (r.ok) return;
} catch {
/* not ready */
}
await new Promise((r) => setTimeout(r, 500));
}
throw new Error("API did not become ready at /health");
}

async function completeSetupIfNeeded(): Promise<void> {
const status = await fetch(`${API}/api/v1/setup/status`);
const body = (await status.json()) as {
data?: { needsSetup?: boolean };
};
const needs = body.data?.needsSetup ?? true;
if (!needs) return;

const res = await fetch(`${API}/api/v1/setup/complete`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tenantName: "E2E Desktop Tenant",
adminEmail: "e2e-desktop-remote@example.com",
adminPassword: "E2E_Desktop_9!",
authSettings: { password: true },
}),
});
if (!res.ok) {
throw new Error(`setup failed: ${res.status} ${await res.text()}`);
}
}

test.describe("desktop onboarding (remote)", () => {
test.beforeAll(async () => {
await waitForApi();
await completeSetupIfNeeded();
});

test.beforeEach(async ({ page }) => {
await installTauriE2EBridge(page);
});

test("server URL, connect screen, email sign-in", async ({ page }) => {
const adminEmail = "e2e-desktop-remote@example.com";
const adminPassword = "E2E_Desktop_9!";

await page.goto("/");

await expect(
page.getByRole("heading", { name: /Welcome to Trackr/i }),
).toBeVisible();

await page.getByLabel(/Server URL/i).fill(API);
await page.getByRole("button", { name: /Use remote server/i }).click();

await expect(page).toHaveURL(/\/connect/, { timeout: 15_000 });
await expect(
page.getByRole("heading", { name: /Sign in to remote Trackr/i }),
).toBeVisible();

await page.getByLabel(/^Email$/i).fill(adminEmail);
await page.getByLabel(/^Password$/i).fill(adminPassword);
await page.getByRole("button", { name: /^Sign in$/i }).click();

await expect(page).toHaveURL(/127\.0\.0\.1:1420\/?$/, { timeout: 30_000 });
await expect(
page.getByRole("heading", { name: /^Desktop$/i }),
).toBeVisible();
await expect(page.getByText(/127\.0\.0\.1:3000/)).toBeVisible();
});
});

test.describe("desktop onboarding (local)", () => {
test.beforeEach(async ({ page }) => {
await installTauriE2EBridge(page, { mockLocalApiBaseUrl: API });
});

test("continue locally reaches desktop home", async ({ page }) => {
await page.goto("/");

await expect(
page.getByRole("heading", { name: /Welcome to Trackr/i }),
).toBeVisible();

await page.getByRole("button", { name: /Continue locally/i }).click();

await expect(page.getByText(/workspace label is/i)).toBeVisible({
timeout: 15_000,
});
await expect(page.getByText(/e2e-local-workspace/i)).toBeVisible();
await expect(
page.getByRole("heading", { name: /^Desktop$/i }),
).toBeVisible();
});
});
Loading
Loading