From 076e35aced9303973da71ddd0ebded13f6c98833 Mon Sep 17 00:00:00 2001 From: ginnoir Date: Tue, 2 Jun 2026 19:53:18 -0500 Subject: [PATCH] docs: add contributing guide, env reference, and operations runbook --- docs/CONTRIBUTING.md | 158 +++++++++++++++++++++++++++++++++++++++++++ docs/ENV.md | 112 ++++++++++++++++++++++++++++++ docs/RUNBOOK.md | 158 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 428 insertions(+) create mode 100644 docs/CONTRIBUTING.md create mode 100644 docs/ENV.md create mode 100644 docs/RUNBOOK.md diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..39a3c01 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,158 @@ +# Contributing + +## Prerequisites + +- **Node.js** 22 LTS (`node --version` should print `v22.x.x`) +- **pnpm** 10 (`pnpm --version` should print `10.x.x`) +- **Docker** with the Compose plugin (used for the local Postgres + MinIO containers) + +## Setup + +```bash +git clone https://github.com/ginnoir/famapp.git +cd famapp +pnpm install +cp .env.example .env +# Edit .env — minimum required for local dev: +# NEXT_PUBLIC_APP_URL=http://127.0.0.1:3000 +# DATABASE_URL=postgres://famapp:famapp@localhost:5432/famapp +# ENABLE_DEV_LOGIN=true +# AUTH_SECRET= +``` + +Start everything (database + migrations + seed + dev server): + +```bash +pnpm dev:local +``` + +Then open `http://localhost:3000/login` and click **Dev login**. See `docs/dev-login.md` for the full local setup including HTTPS/PWA testing. + +## Available scripts + + + +| Command | Description | +| --------------------- | ----------------------------------------------------------------------------------------- | +| `pnpm dev` | Next.js dev server (localhost only) | +| `pnpm dev:network` | Dev server bound to `0.0.0.0` (LAN access for phone testing) | +| `pnpm dev:local` | Full local stack — starts DB container, runs migrations, seeds, launches dev server | +| `pnpm dev:reset` | Tear down local DB and start fresh (destructive — deletes all local data) | +| `pnpm build` | Production Next.js build | +| `pnpm start` | Start the production build locally | +| `pnpm lint` | ESLint check | +| `pnpm lint:fix` | ESLint with auto-fix | +| `pnpm format` | Prettier — format all files | +| `pnpm format:check` | Prettier — check only (used in CI) | +| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) | +| `pnpm test:e2e` | Playwright end-to-end tests | +| `pnpm db:generate` | Generate a new Drizzle migration from schema changes | +| `pnpm db:migrate` | Apply pending Drizzle migrations | +| `pnpm db:seed` | Seed the database with dev fixtures | +| `pnpm db:studio` | Open Drizzle Studio (local DB browser) | +| `pnpm gen:icons` | Regenerate PWA icon set from source | +| `pnpm vapid:generate` | Generate VAPID key pair for Web Push | +| `pnpm release` | Interactive release (prompts for semver bump, tags, publishes changelog + GitHub Release) | +| `pnpm release:patch` | Non-interactive patch release | +| `pnpm release:minor` | Non-interactive minor release | +| `pnpm release:major` | Non-interactive major release | +| `pnpm release:dry` | Dry-run release — preview without writing | + + + +## Running tests + +### Type check + lint (CI equivalent) + +```bash +pnpm typecheck +pnpm lint +pnpm format:check +``` + +### End-to-end tests + +The app must be running first (`pnpm dev:local`). Generate a Playwright auth state file, then run tests: + +```powershell +# Generate auth state (run once after starting the app) +New-Item -ItemType Directory -Force tests\.auth | Out-Null +@' +const { chromium } = require('@playwright/test'); +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto('http://127.0.0.1:3000/login'); + await page.getByRole('button', { name: 'Dev login' }).click(); + await page.waitForURL('http://127.0.0.1:3000/'); + await page.context().storageState({ path: 'tests/.auth/dev-user.json' }); + await browser.close(); +})(); +'@ | node - + +# Run tests +$env:PLAYWRIGHT_STORAGE_STATE='tests/.auth/dev-user.json' +pnpm test:e2e +``` + +### Writing tests + +- Unit tests: Vitest under `tests/unit/` — only where it pays off (utilities, pure logic). +- E2E tests: Playwright under `tests/e2e/` — one happy-path test per module. Do not write brittle selector-heavy tests for trivial CRUD. + +## Code style + +- **TypeScript strict** — no `any` without a written reason. +- **No comments** unless the _why_ is non-obvious. Names carry intent. +- **Immutable** — always return new objects; never mutate in place. +- **Module isolation** — a module imports from `_core` and `lib/` only; never from a sibling module. +- **File size** — 200–400 lines typical, 800 hard cap. + +Formatting and lint run automatically on staged files via `lint-staged` at commit time. You can also run them manually with `pnpm lint:fix` and `pnpm format`. + +## Commit format + +Commits must follow [Conventional Commits](https://www.conventionalcommits.org/). `commitlint` enforces this at the `commit-msg` hook. + +``` +: + +[optional body] +``` + +Allowed types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `revert` + +Examples: + +``` +feat: add plant species search to garden module +fix: calendar event end time off by one day +chore: bump next to 15.6 +``` + +## Adding a module + +Every feature lives under `src/modules//`. A new module needs: + +- `schema.ts` — Drizzle tables +- `server/` — server actions and queries +- `components/` — React components +- `manifest.ts` — registers the module with the core registry (nav, entity types, dashboard widget, quick-add actions) + +See `CLAUDE.md` for the full architectural brief. The module loader in `src/modules/_core/` discovers manifests automatically — no changes to core code required for a new module. + +## Schema changes + +1. Edit the relevant `schema.ts`. +2. Run `pnpm db:generate` to create a new migration file under `drizzle/`. +3. Commit the migration alongside the schema change. +4. Never edit a shipped migration — always add a new one. + +## PR checklist + +- [ ] `pnpm typecheck` passes +- [ ] `pnpm lint` passes +- [ ] `pnpm build` succeeds +- [ ] New Drizzle migration committed if schema changed +- [ ] E2E test added or updated if a user-visible flow changed +- [ ] `docs/tasks/09-pre-deploy-checklist.md` reviewed if touching auth or env diff --git a/docs/ENV.md b/docs/ENV.md new file mode 100644 index 0000000..50463e0 --- /dev/null +++ b/docs/ENV.md @@ -0,0 +1,112 @@ +# Environment Variables + + + +## Quick start + +```bash +cp .env.example .env +# Fill in required values, then: +pnpm dev:local +``` + +## Core + +| Variable | Required | Description | Example / Default | +| --------------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------ | +| `NEXT_PUBLIC_APP_URL` | Yes | Public URL of the app — used in OIDC redirect URIs and share links | `https://fam.yourdomain.com` | +| `DATABASE_URL` | Yes | Postgres connection string | `postgres://famapp:famapp@localhost:5432/famapp` | +| `AUTH_SECRET` | Yes | Auth.js signing secret — generate with `openssl rand -base64 32` | — | + +## OIDC (Authentik) + +Required in production. Obtain from the Authentik admin panel after bootstrapping the provider — see `deploy/authentik/README.md`. + +| Variable | Required | Description | Example | +| ------------------------- | ---------- | --------------------------------- | --------------------------------------------------- | +| `AUTH_OIDC_ISSUER` | Yes (prod) | Authentik OIDC issuer URL | `https://auth.yourdomain.com/application/o/famapp/` | +| `AUTH_OIDC_CLIENT_ID` | Yes (prod) | OIDC client ID from Authentik | — | +| `AUTH_OIDC_CLIENT_SECRET` | Yes (prod) | OIDC client secret from Authentik | — | + +## Dev login + +Local development only. Never set `ENABLE_DEV_LOGIN=true` in production — a startup assertion in `src/lib/dev-login-config.ts` will crash the container if you do. + +| Variable | Required | Description | Default | +| -------------------- | -------- | ------------------------------------------------- | ------------------ | +| `ENABLE_DEV_LOGIN` | No | Show a one-click **Dev login** button on `/login` | `false` | +| `DEV_LOGIN_EMAIL` | No | Email for the auto-created dev session | `dev@famapp.local` | +| `DEV_LOGIN_NAME` | No | Display name for the dev user | `Dev User` | +| `DEV_HOUSEHOLD_NAME` | No | Household created for the dev user | `Home` | + +See `docs/dev-login.md` for the full local run procedure. + +## Web Push (VAPID) + +All three are required together. Generate them once with `pnpm vapid:generate` and copy all three lines into `.env`. + +| Variable | Required | Description | +| ------------------- | -------- | --------------------------------------------- | ------------------------ | +| `VAPID_PUBLIC_KEY` | Yes | VAPID public key — also passed to the browser | +| `VAPID_PRIVATE_KEY` | Yes | VAPID private key — server only | +| `VAPID_SUBJECT` | Yes | Contact URI for push servers | `mailto:you@example.com` | + +## ntfy (optional) + +Leave both blank to disable the ntfy notification channel. Web Push is the primary channel. + +| Variable | Required | Description | Example | +| ------------ | -------- | ------------------------ | ----------------- | +| `NTFY_URL` | No | ntfy server base URL | `https://ntfy.sh` | +| `NTFY_TOPIC` | No | ntfy topic to publish to | `famapp-alerts` | + +## Logging + +| Variable | Required | Description | Values | +| ----------- | -------- | ------------------ | ----------------------------------------------------------- | +| `LOG_LEVEL` | No | Pino log verbosity | `trace`, `debug`, `info`, `warn`, `error` (default: `info`) | + +## MinIO object storage + +Required when the garden module is enabled. The dev compose stack starts a local MinIO instance automatically via `pnpm dev:local`. + +| Variable | Required | Description | Default | +| --------------------- | -------- | ------------------------------------ | ----------------------- | +| `MINIO_ENDPOINT` | Yes | MinIO server base URL | `http://localhost:9000` | +| `MINIO_ROOT_USER` | Yes | MinIO root access key | `famapp` | +| `MINIO_ROOT_PASSWORD` | Yes | MinIO root secret key | `changeme` | +| `MINIO_BUCKET` | No | Bucket used for garden image uploads | `garden` | + +## OpenPlantBook + +Optional. Enables plant species lookup in the garden module. Free account at . + +| Variable | Required | Description | +| ----------------------------- | -------- | -------------------- | +| `OPENPLANTBOOK_CLIENT_ID` | No | OAuth2 client ID | +| `OPENPLANTBOOK_CLIENT_SECRET` | No | OAuth2 client secret | + +## Release tooling + +Only needed on the machine that cuts releases (`pnpm release`). + +| Variable | Required | Description | +| -------------- | ------------- | ------------------------------------------------ | +| `GITHUB_TOKEN` | Yes (release) | Personal access token — creates a GitHub Release | + +## Production-only compose variables + +Used by `deploy/compose.example.yaml`. Set in `deploy/.env` on the server — not in the local `.env`. + +| Variable | Description | +| ------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `FAMAPP_IMAGE` | Docker image tag to deploy (e.g. `ghcr.io/ginnoir/famapp:v0.4.7`) | +| `FAMAPP_PORT` | Host port to bind (default: `3000`) | +| `FAMAPP_PULL_POLICY` | Docker pull policy (default: `always`) | +| `FAMAPP_DB_USER` / `FAMAPP_DB_PASSWORD` / `FAMAPP_DB_NAME` | Postgres credentials for the famapp database | +| `AUTHENTIK_DB_USER` / `AUTHENTIK_DB_PASSWORD` / `AUTHENTIK_DB_NAME` | Postgres credentials for the Authentik database | +| `AUTHENTIK_SECRET_KEY` | Authentik signing key — generate with `openssl rand -base64 60` | +| `AUTHENTIK_IMAGE_TAG` | Authentik server image tag (default: `2024.12.3`) | +| `RUN_MIGRATIONS` | Set `false` to skip auto-migration on start (default: `true`) | + + diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md new file mode 100644 index 0000000..ab35869 --- /dev/null +++ b/docs/RUNBOOK.md @@ -0,0 +1,158 @@ +# Runbook + +Operational reference for famapp in production. See `deploy/README.md` for the one-time host setup and `docs/tasks/09-pre-deploy-checklist.md` for the pre-deploy checklist. + +## Cutting a release + +From `main` on the dev machine, with a clean working tree: + +```bash +pnpm release:patch # or :minor / :major +# — bumps package.json version +# — prepends to CHANGELOG.md +# — creates and pushes a signed git tag (v0.x.y) +# — creates a GitHub Release with generated notes +# Requires GITHUB_TOKEN in .env +``` + +CI (`release.yml`) then builds and pushes the Docker image to GHCR: + +- `ghcr.io/ginnoir/famapp:v0.x.y` +- `ghcr.io/ginnoir/famapp:0.x` (minor alias) +- `ghcr.io/ginnoir/famapp:latest` + +## Deploying a release + +On the home server, in `/srv/famapp/deploy/`: + +```bash +# Pin the new tag +sed -i 's|FAMAPP_IMAGE=.*|FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.x.y|' .env + +# Pull and restart only the app container +docker compose pull famapp +docker compose up -d famapp + +# Watch the boot log — migrations run before the server starts +docker compose logs -f famapp +``` + +The container entrypoint runs `node scripts/migrate.mjs` automatically. A healthy boot ends with a log line like `ready on http://0.0.0.0:3000`. + +## Health check + +```bash +# Container status +docker compose ps + +# App response (200 = healthy) +curl -sf https://fam.yourdomain.com/ -o /dev/null -w "%{http_code}\n" + +# Recent app logs +docker compose logs --tail=100 famapp + +# Database connectivity +docker compose exec famapp-db pg_isready -U famapp -d famapp +``` + +## Rollback + +1. Find the previous working tag in `CHANGELOG.md` or `docker images`. +2. Pin it in `deploy/.env`: + ```bash + sed -i 's|FAMAPP_IMAGE=.*|FAMAPP_IMAGE=ghcr.io/ginnoir/famapp:v0.x.y|' .env + ``` +3. Restart the container: + ```bash + docker compose up -d famapp + ``` + +If the rollback target predates a migration that has already been applied to the database, restore from backup first — see **Backups** below. To skip auto-migration on a given start (rarely needed): + +```bash +RUN_MIGRATIONS=false docker compose up -d famapp +``` + +## Backups + +The `famapp-backup` container runs nightly `pg_dump` for both `famapp` and `authentik` databases into the `backups` volume. + +```bash +# Check last backup run +docker compose logs famapp-backup | tail -20 + +# List backup files +docker compose exec famapp-backup ls -lh /backups + +# Manual backup now +docker compose exec famapp-backup sh /scripts/backup.sh + +# Restore from a backup file +docker compose exec famapp-backup sh /scripts/restore.sh famapp_2026-06-01.sql.gz +``` + +See `deploy/backups/README.md` for retention policy and full restore details. + +## Common issues + +### App container exits immediately + +```bash +docker compose logs famapp +``` + +Likely causes: + +- **Missing required env var** — the app throws on startup if `AUTH_SECRET`, `DATABASE_URL`, or `AUTH_OIDC_*` are absent. +- **`ENABLE_DEV_LOGIN=true` in production** — `src/lib/dev-login-config.ts` throws an assertion error on import. Remove the variable from the production env. +- **Database not ready** — Postgres healthcheck should prevent this, but if the DB is slow to start, increase `start_period` in `compose.yaml`. + +### Migrations fail on boot + +```bash +docker compose logs famapp | grep -i migration +``` + +- Schema is ahead of code: roll back the image or forward-migrate manually. +- Database unreachable: confirm `famapp-db` is healthy (`docker compose ps`). + +### OIDC login fails + +1. Confirm `AUTH_OIDC_ISSUER` matches the Authentik provider URL exactly (trailing slash matters). +2. Confirm the OIDC client redirect URI in Authentik includes `https://fam.yourdomain.com/api/auth/callback/oidc`. +3. Check Authentik logs: `docker compose logs authentik-server | tail -50`. + +### Push notifications not arriving + +1. Confirm `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY`, and `VAPID_SUBJECT` are all set in the production env. +2. Verify the browser's push subscription is still valid (Settings → notifications → re-enable). +3. If using ntfy as a secondary channel, confirm `NTFY_URL` and `NTFY_TOPIC` are set. + +### MinIO / garden image uploads failing + +```bash +docker compose logs famapp-minio | tail -30 +``` + +- Confirm `MINIO_ENDPOINT` is `http://famapp-minio:9000` in the compose env (not `localhost`). +- Confirm the `garden` bucket exists — create it manually via the MinIO console at port `9001` if it is missing. + +### Out of disk space + +```bash +df -h /var/lib/docker +docker system prune --volumes # removes stopped containers, dangling images, unused volumes +``` + +Old backup files accumulate in the `backups` volume. The retention script (`deploy/backups/retain.sh`) runs nightly — check its logs if the volume keeps growing. + +## Authentik admin access + +The Authentik admin UI is at `https://auth.yourdomain.com`. Log in with the superuser credentials set during bootstrap — see `deploy/authentik/README.md`. + +To reset the Authentik admin password from the CLI: + +```bash +docker compose exec authentik-server ak create_recovery_key 1 akadmin +# prints a one-time recovery URL +```