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
6 changes: 3 additions & 3 deletions .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/rowetech"
cmd = "go build -o ./tmp/rowetech ./cmd/server"
cmd = "go build -tags dev -o ./tmp/rowetech ./cmd/server"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "static", "internal/database/sqlc", ".next", "out"]
exclude_file = ["go.sum"]
Expand All @@ -23,8 +23,8 @@ tmp_dir = "tmp"
post_cmd = []
pre_cmd = [
"lsof -ti:${PORT:-3000} | xargs kill -9 2>/dev/null || true; sleep 0.5",
"templ generate",
"sqlc generate -f sqlc/sqlc.yaml 2>/dev/null || true",
"go tool templ generate",
"go tool sqlc generate -f sqlc/sqlc.yaml 2>/dev/null || true",
"go mod tidy"
]
rerun = false
Expand Down
5 changes: 5 additions & 0 deletions .envrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export PORT="3000"
export ENV="development"
export LOG_LEVEL="DEBUG"

# Tailscale magic-DNS host for this machine (e.g. prometheus.tail60aac7.ts.net).
# When set, the server logs a clickable http://<host>:<port> access URL at
# startup so the dev server is reachable from any device on your tailnet.
export TAILSCALE_HOSTNAME=""

# Site / SEO (used for meta tags, OG, etc.)
export SITE_NAME="RoweTech Machine & Engineering"
export SITE_URL="http://localhost:3000"
Expand Down
25 changes: 15 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,34 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.23'
# Keep CI's Go in lockstep with the module (go.mod is on 1.26).
go-version-file: go.mod
cache: false
- uses: actions/setup-node@v6
with:
node-version: '20'
- name: Install Go tools
run: |
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
- name: Install npm dependencies
run: npm ci
- name: Generate code
# templ/sqlc are pinned via the `tool` directive in go.mod, so the
# generator version always matches the runtime — no global installs.
run: |
templ generate
sqlc generate -f sqlc/sqlc.yaml
go tool templ generate
go tool sqlc generate -f sqlc/sqlc.yaml
- name: Build CSS
run: make css
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.1.6
# Must be built with Go >= the module's go directive (1.26); older
# builds error: "language version (go1.24) ... lower than targeted (1.26.0)".
version: v2.10.1
args: --timeout=5m
- name: Run tests
run: go test -v -race ./...
- name: Build
run: go test -race ./...
- name: Run tests (dev tag)
run: go test -race -tags dev ./...
- name: Build (production)
run: go build -o bin/rowetech ./cmd/server
- name: Build (dev tag)
run: go build -tags dev -o bin/rowetech-dev ./cmd/server
62 changes: 62 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Agent Guide — RoweTech Machine & Engineering

This file is for AI coding agents (Claude Code, Codex, etc.). The full project
guide is **[CLAUDE.md](./CLAUDE.md)** — read it. This file highlights the rules
that most often trip agents up.

## Build / dev loop

- `make dev` is usually already running (hot reload via Air). It builds with
`-tags dev`, regenerates templ + sqlc, runs `go mod tidy`, and restarts.
- **After any change, check `./tmp/air-combined.log`** for compile / templ / sqlc
errors. Never assume a change built.
- Do **not** manually run `templ generate`, `sqlc generate`, `go build`, or `air`.

## Tooling is pinned via `go tool` (do not reintroduce global installs)

`templ`, `sqlc`, and `goose` are pinned in `go.mod` via the `tool` directive and
invoked as `go tool templ` / `go tool sqlc` / `go tool goose`. This keeps the
generator version in lockstep with the runtime — relying on globally-installed
binaries caused build failures like `undefined: templ.ResolveAttributeValue`.
Bump templ with `make templ-update`.

## Database migrations run on startup

`internal/database.New` applies all pending goose migrations from the embedded
`migrations` package on boot. A fresh DB is migrated automatically; you don't
need `make migrate`. After adding a migration, just restart `make dev`.

## Testing admin / dashboard pages — Dev Auth Bypass

Admin pages (`/admin/*`) are Clerk-gated, but the `-tags dev` build (used by
`make dev`) compiles in a **loopback-only** bypass so you can test them:

```bash
# 302 = dev tag live; 404 = restart `make dev` (air didn't rebuild with -tags dev)
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3000/auth/dev/login"

# Log in as admin and exercise an admin page:
JAR=/tmp/rowetech.jar
curl -sc "$JAR" -o /dev/null "http://localhost:3000/auth/dev/login?return_to=/admin"
curl -s -b "$JAR" -o /dev/null -w "%{http_code}\n" "http://localhost:3000/admin" # 200
```

In a browser (or Chrome DevTools MCP), navigate to
`http://localhost:3000/auth/dev/login?return_to=/admin` to land authenticated in
the admin panel. Optional `?email=<addr>` impersonates a specific admin.

**Never weaken these safety properties** when editing the bypass:
- It lives behind `//go:build dev` (`internal/middleware/auth_dev.go`); the
`//go:build !dev` stub keeps it out of production binaries.
- Production builds (`make build`, `make build-static`) omit `-tags dev`.
- It is loopback-only (checks both `RemoteAddr` and `Host`) and `init()` fatals
under `ENV=production`.
- `make test-dev` runs the `-tags dev` tests; `auth_dev_prod_test.go` asserts the
routes are absent from production builds. Keep both green.

## Conventions

- Logging: `slog` only (never `fmt.Printf` / `log.Printf`).
- Errors: wrap with context — `fmt.Errorf("...: %w", err)`.
- DB access: sqlc-generated queries (`h.db.Queries.*`).
- Templates own their meta tags via `meta.New(...)`.
92 changes: 81 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,80 @@ Never assume code changes succeeded without checking this log.

`make dev` is always running during development. It automatically:
1. Kills existing process on port
2. Regenerates Templ templates
3. Regenerates sqlc queries
2. Regenerates Templ templates (`go tool templ generate`)
3. Regenerates sqlc queries (`go tool sqlc generate`)
4. Runs go mod tidy
5. Rebuilds and restarts server
5. Rebuilds (with `-tags dev`) and restarts server

**You do NOT need to manually run:** `templ generate`, `sqlc generate`, `go build`, or `air`.

### Tooling is pinned via `go tool`

`templ`, `sqlc`, and `goose` are pinned in `go.mod` via the `tool` directive and
invoked everywhere as `go tool templ` / `go tool sqlc` / `go tool goose`. This
guarantees the generator version always matches the runtime version — do **not**
rely on globally-installed `templ`/`sqlc`/`goose` binaries (version drift there
caused real build failures, e.g. `undefined: templ.ResolveAttributeValue`). To
bump templ safely, run `make templ-update` (bumps runtime + tool pin together).

### Migrations run automatically on startup

`internal/database.New` applies all pending goose migrations from the embedded
`migrations` package (`migrations/embed.go`) on every boot. A fresh or
out-of-date DB is brought to schema automatically — you do **not** need to run
`make migrate` by hand for the dev server. `make migrate` still exists for
manual/CLI use.

### Port fallback

If the configured `PORT` is in use, the server binds the next free port (see
`internal/portutil`) and logs `configured port in use, using next free port`.
It also logs clickable `access URL` lines at startup (localhost + the Tailscale
host from `TAILSCALE_HOSTNAME`, if set).

## Dev-Only Auth Bypass (for testing admin/dashboard pages)

Admin pages (`/admin/*`) are normally gated by Clerk. To test them locally
without Clerk, `make dev` builds with `-tags dev` (see `.air.toml`), which
compiles in two **loopback-only** routes:

- `GET /auth/dev/login[?email=<addr>][&return_to=/path]` — sets the `dev_admin`
session cookie (impersonating `email`, defaulting to the first
`ADMIN_EMAILS`/`ADMIN_EMAIL` entry) and redirects to `return_to` (default
`/admin`). `RequireAdminAccess` honors this cookie and skips Clerk.
- `POST /auth/dev/logout` — clears the cookie.

**How to use it (curl):**

```bash
# Is the dev tag live? 302 = yes, 404 = air built without -tags dev (restart make dev)
curl -s -o /dev/null -w "%{http_code}\n" "http://localhost:3000/auth/dev/login"

# Log in as admin, then hit an admin page with the saved cookie:
JAR=/tmp/rowetech.jar
curl -sc $JAR -o /dev/null "http://localhost:3000/auth/dev/login?return_to=/admin"
curl -s -b $JAR -o /dev/null -w "%{http_code}\n" "http://localhost:3000/admin" # 200
```

In a browser, just visit `http://localhost:3000/auth/dev/login` — it sets the
cookie and redirects you into `/admin`.

**Safety (why this can never run in production):**
- `//go:build dev` — `internal/middleware/auth_dev.go` is compiled out of
production binaries entirely; `auth_dev_stub.go` (`//go:build !dev`) provides
no-op `RegisterDevAuthRoutes` / `DevAuthEnabled()=false` / `devAdminBypass`.
- `make build` and `make build-static` (production) **omit** `-tags dev`.
- Loopback-only: both the raw TCP peer (`RemoteAddr`, not `RealIP`) and the
`Host` header must be loopback, or the request gets 403. Defeats
`X-Forwarded-For` spoofing and reverse-proxy bypass.
- `init()` in `auth_dev.go` fatals if `ENV`/`ENVIRONMENT=production`.
- `internal/middleware/auth_dev_prod_test.go` (`//go:build !dev`) asserts no
`/auth/dev/*` routes exist in a production build. Run dev-tagged tests with
`make test-dev`.

**Gotcha:** Air does not reload `.air.toml` on its own. If the dev login route
returns 404, the running `air` was built without `-tags dev` — restart `make dev`.

## Environment Setup

All configuration via `.envrc` (copy from `.envrc.example`):
Expand All @@ -33,6 +100,7 @@ export ENV="development"
export LOG_LEVEL="DEBUG"
export SITE_NAME="RoweTech Machine & Engineering"
export SITE_URL="http://localhost:3000"
export TAILSCALE_HOSTNAME="your-machine.tailnet.ts.net" # optional; logged as a clickable startup URL
export CLERK_SECRET_KEY="sk_test_..."
export CLERK_PUBLISHABLE_KEY="pk_test_..."
```
Expand All @@ -41,15 +109,17 @@ export CLERK_PUBLISHABLE_KEY="pk_test_..."

| Command | Description |
|---------|-------------|
| `make dev` | Start with hot reload (main workflow) |
| `make build` | Build production binary |
| `make dev` | Start with hot reload, built with `-tags dev` (main workflow) |
| `make build` | Build production binary (no dev tag) |
| `make test` | Run tests with race detection |
| `make test-dev` | Run tests including `-tags dev` (auth bypass) |
| `make lint` | Run linters |
| `make generate` | Generate templ and sqlc code |
| `make generate` | Generate templ and sqlc code (via `go tool`) |
| `make css` | Build Tailwind CSS |
| `make css-watch` | Watch Tailwind (separate terminal) |
| `make migrate` | Run database migrations |
| `make setup` | Install development tools |
| `make migrate` | Run database migrations manually (also auto-run on startup) |
| `make templ-update` | Bump templ runtime + tool pin together |
| `make setup` | Install dev tools (air, golangci-lint; templ/sqlc/goose come via `go tool`) |

## Project Structure

Expand Down Expand Up @@ -138,8 +208,7 @@ Use `hx-*` attributes for dynamic updates:
3. Run `make setup` to install tools
4. Copy `.envrc.example` to `.envrc` and configure
5. Run `direnv allow` (or source .envrc)
6. Run `make migrate` to set up database
7. Run `make dev` to start development server
6. Run `make dev` — the database is migrated automatically on startup

## Adding a New Page

Expand All @@ -153,5 +222,6 @@ Use `hx-*` attributes for dynamic updates:
1. Create migration: `make migrate-create NAME=create_tablename`
2. Edit migration file in `migrations/`
3. Add queries in `sqlc/queries/`
4. Run `make migrate`
4. Restart `make dev` — pending migrations apply automatically on startup
(embedded via `migrations/embed.go`). `make migrate` is only for manual/CLI use.
5. sqlc will regenerate on next build
46 changes: 32 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
SHELL := /bin/bash

.PHONY: dev build build-static test lint generate css css-watch migrate migrate-down migrate-status migrate-create setup setup-ci clean run help
.PHONY: dev build build-static test test-dev lint generate css css-watch migrate migrate-down migrate-status migrate-create setup setup-ci clean run help templ-update

BINARY_NAME=rowetech
MIGRATIONS_DIR=migrations
DIST_DIR=dist

# Hot-reload dev server. Air builds with `-tags dev` (see .air.toml), which
# compiles in the loopback-only auth bypass at /auth/dev/login so admin pages
# can be tested without Clerk. See CLAUDE.md "Dev-Only Auth Bypass".
dev:
@if [ -f tmp/air-combined.log ]; then \
mv tmp/air-combined.log tmp/air-combined-$$(date +%Y%m%d-%H%M%S).log; \
fi
@ls -t tmp/air-combined-*.log 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true
@air 2>&1 | tee tmp/air-combined.log

# Production build. Intentionally OMITS `-tags dev`, so the dev auth bypass is
# compiled out entirely (the !dev stub is used instead).
build: generate css
go build -o $(BINARY_NAME) ./cmd/server

Expand All @@ -26,13 +31,17 @@ build-static: generate css
test:
go test -v -race ./...

# Run the dev-tagged tests too (auth bypass loopback gating lives behind -tags dev).
test-dev:
go test -race -tags dev ./...

lint:
golangci-lint run
templ fmt templates/
go tool templ fmt templates/

generate:
templ generate
@if command -v sqlc &> /dev/null; then sqlc generate -f sqlc/sqlc.yaml; fi
go tool templ generate
go tool sqlc generate -f sqlc/sqlc.yaml

css:
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --minify
Expand All @@ -41,33 +50,40 @@ css-watch:
npx @tailwindcss/cli -i static/css/input.css -o static/css/output.css --watch

migrate:
goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" up
go tool goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" up

migrate-down:
goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" down
go tool goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" down

migrate-status:
goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" status
go tool goose -dir $(MIGRATIONS_DIR) sqlite3 "$$DATABASE_URL" status

migrate-create:
ifndef NAME
$(error NAME is required. Usage: make migrate-create NAME=create_users)
endif
goose -dir $(MIGRATIONS_DIR) create $(NAME) sql
go tool goose -dir $(MIGRATIONS_DIR) create $(NAME) sql

# Tool dependencies (templ, sqlc, goose) are pinned via the `tool` directive in
# go.mod and invoked through `go tool <name>` everywhere, so the generator
# version always matches the runtime version. Only air and golangci-lint still
# need a global install since they are not in go.mod.
setup:
go install github.com/air-verse/air@latest
go install github.com/a-h/templ/cmd/templ@latest
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
go install github.com/pressly/goose/v3/cmd/goose@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
npm install

# CI/CD setup (minimal - for Vercel)
# CI/CD setup (minimal - for Vercel). templ/sqlc come from `go tool`.
setup-ci:
go install github.com/a-h/templ/cmd/templ@latest
npm install

# Bump the templ runtime and its tool pin together so the generator version
# always matches the runtime version. Avoids the drift that breaks builds.
templ-update:
go get -u github.com/a-h/templ@latest
go get -tool github.com/a-h/templ/cmd/templ@latest
go mod tidy

clean:
rm -f $(BINARY_NAME)
rm -rf tmp/
Expand All @@ -83,6 +99,7 @@ help:
@echo " build - Build the binary"
@echo " build-static - Build static site for Vercel"
@echo " test - Run tests"
@echo " test-dev - Run tests including -tags dev (auth bypass)"
@echo " lint - Run golangci-lint and templ fmt"
@echo " generate - Generate templ and sqlc code"
@echo " css - Build Tailwind CSS"
Expand All @@ -91,7 +108,8 @@ help:
@echo " migrate-down - Rollback last migration"
@echo " migrate-status - Show migration status"
@echo " migrate-create - Create new migration (NAME=xxx)"
@echo " setup - Install development tools"
@echo " setup - Install development tools (air, golangci-lint)"
@echo " setup-ci - Install CI tools (Vercel)"
@echo " templ-update - Bump templ runtime + tool pin together"
@echo " clean - Remove build artifacts"
@echo " run - Build and run the server"
Loading