Imported from jaakkos/wind-sport-planner (
AGENTS.md). Install upstream withnpx skills add jaakkos/wind-sport-planner. Copyright stays with the author.
Agent instructions — Fjell Lift (wind-sport-planner)
Git commits — non-negotiable
Do not mention AI, assistants, or AI-based dev tools anywhere in Git commit metadata. This applies no matter who or what produced the diff. Treat it as a hard repo rule: violating it is incorrect work, not a stylistic choice.
In scope (must stay clean):
- Commit subject and body
- Merge or squash messages you write
- Tags or release notes only if they are generated from commit messages (keep source commits clean)
Forbidden (examples, not exhaustive): names or logos of tools (e.g. Cursor, Copilot, ChatGPT, Claude, “AI”, “LLM”), trailers such as Made-with: …, Generated-by: …, “assisted by …”, or Co-authored-by: lines that identify an automated assistant. If you use Git trailers, they must not attribute the change to AI.
Do instead: normal, factual messages — what changed and why — in the same tone as hand-written project history. No attribution of the implementation to any product or model.
What this repo is
Next.js app: magic-link auth (Auth.js), PostgreSQL + PostGIS (Prisma), MapLibre map, Open-Meteo weather, practice areas and session logging. Production is Coolify — README.md and docs/deploy-coolify.md (app fjelllift, public https://fjelllift.com).
Next.js
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ before writing any code. Heed deprecation notices.
Code changes
- Match existing patterns in the file you touch (imports, types, component style). Prefer small, focused diffs; no drive-by refactors or unrelated files.
- Before finishing work that touches app or library code, run what CI runs:
npm run lint,npm run test,npm run build. If you changed flows covered by E2E, runnpm run test:e2e(needs local Postgres migrated and env configured; Playwright can startnext devitself).
Environment & configuration
- Node.js 22.12+ (Prisma 7 and tooling; repo pins 22.14 — use
.nvmrcwithnvm use/fnm use, and match CoolifyNODE_VERSIONindocs/deploy-coolify.md). .npmrcsetslegacy-peer-deps=trueso nodemailer 8 (security fixes) installs cleanly with next-auth v5 beta, which still peer-declares nodemailer 7.npm cirespects this file (CI and Coolify).- Copy
.env.example→.env(and optionally.env.local). Never commit real secrets; keep them in env files or the host’s secret store. AUTH_SECRET: long random string (see.env.example).AUTH_URL: canonical public URL with no trailing slash; wrong values break magic links andnew URL(...)usage (see comments in.env.exampleandsrc/auth.ts).- Email: local dev uses Mailpit (Docker) via
EMAIL_SERVER_*whenRESEND_API_KEYis unset; production (Coolify) uses platform SMTP (EMAIL_SERVER_*,EMAIL_FROM). Do not setRESEND_API_KEYin production. - Optional:
NEXT_PUBLIC_MAPTILER_API_KEYfor MapTiler basemap,NEXT_PUBLIC_*legal contact vars on/privacy.
Local services (Docker)
docker compose up -d: PostGISpostgis/postgis:16-3.4(user/db/password align with.env.example:wind/wind_sport) and Mailpit (SMTP1025, UIhttp://localhost:8025).- After schema changes:
npm run db:dev(iterate) ornpx prisma migrate deploy(apply existing migrations). Thennpm run dev→ http://localhost:3000.
Repository layout
| Area | Location |
|---|---|
| App Router pages & layouts | src/app/ |
| Route handlers (API) | src/app/api/**/route.ts |
| React components | src/components/ |
| Shared logic (weather, heuristics, map helpers, etc.) | src/lib/ |
| Auth.js config | src/auth.ts |
| Prisma schema & migrations | prisma/schema.prisma, prisma/migrations/ |
| Generated Prisma client | src/generated/prisma/ (do not edit by hand) |
| Vitest tests | tests/**/*.test.ts, setup tests/setup.ts |
| Playwright E2E | e2e/ |
Imports use the @/* alias → src/* (see tsconfig.json).
TypeScript, ESLint, styling
- TypeScript:
strictmode; prefer explicit types at boundaries (API payloads, Prisma results you reshape). - ESLint: flat config in
eslint.config.mjs(eslint-config-nextcore-web-vitals + TypeScript +eslint-plugin-react-hooks).npm run lint. Generated client undersrc/generated/prisma/**is ignored — fix issues inprisma/schemaor app code, not generated files. - Hard rules (not warnings):
@typescript-eslint/no-unused-vars,@typescript-eslint/no-unused-expressions,react-hooks/rules-of-hooks,react-hooks/exhaustive-deps. Prefix intentionally-unused parameters with_. - Size caps (errors, application code only):
max-lines600 per file,max-lines-per-function400. If a function is creeping toward the cap, extract a hook intosrc/components/map-hub/hooks/or a pure helper intosrc/lib/rather than raising the limit. Tests undertests/**ande2e/**are exempt from both caps. - Dead-code & cycles:
npm run audit:dead(Knip — unused files/exports/dependencies, also wired into CI) andnpm run audit:cycles(Madge — import cycles insrc/). Run these when you finish a feature so the report stays clean. - Tailwind CSS v4 with PostCSS; global styles in
src/app/globals.css. Follow existing utility and layout patterns on nearby pages.
npm scripts (common)
| Script | Purpose |
|---|---|
npm run dev |
Next dev server |
npm run build |
prisma generate + next build --webpack (keep --webpack unless you intentionally migrate the build) |
npm run start |
Production server after build |
npm run lint |
ESLint |
npm run db:migrate / npm run db:dev |
Prisma migrate deploy / dev migrate |
npm run test / npm run test:watch / npm run test:coverage |
Vitest |
npm run test:e2e |
Playwright (Chromium); playwright:install for browsers |
npm run audit:dead |
Knip — unused files, exports, dependencies (also runs in CI) |
npm run audit:cycles |
Madge — circular import detector for src/ |
Testing
- Vitest (
vitest.config.mts): Node environment, tests intests/**/*.test.ts,tests/setup.tsresets mocks after each test. Coverage is configured forsrc/lib/**/*.ts— add unit tests next to new pure logic insrc/lib/when behavior is non-trivial. - Playwright (
playwright.config.ts): specs ine2e/, loads.env.localthen.env,E2E_PORToverrides port (default3000). CI usesnpx next devas web server; locally it can reuse an already running dev server.
Database (Prisma)
- Edit
prisma/schema.prisma; create migrations withnpm run db:dev(or equivalentprisma migrate dev). Commit migration SQL underprisma/migrations/. - Client output is
src/generated/prisma(generator outputin schema).postinstallrunsprisma generate;npm run buildalso generates. Do not commit hand-edits insidesrc/generated/prisma. - Production applies migrations in Coolify pre-deploy (
npx prisma migrate deploy— seedocs/deploy-coolify.md) — avoid workflows that only migrate locally.
CI and deploy gate
.github/workflows/ci.ymlon push/PR tomain/master:npm ci→ Prisma migrate against service Postgres → lint → Vitest with coverage → production build → Playwright.- Keep CI green before deploying
mainon Coolify (docs/deploy-coolify.md).
API, auth, and server boundaries
- Prefer Route Handlers in
src/app/api/for JSON APIs; validate input with Zod. Domain-specific schemas live next to their domain (e.g.src/lib/practiceArea/schema.ts). Keep route files thin — parse → query/mutate → return. - Use the shared helpers in
src/lib/api/instead of rolling your own:requireUserSessionandisErrorResponse(handler.ts) — auth gate that returns a 401NextResponseyou forward unchanged.parseJsonBody(req, schema)(handler.ts) — Zod-validated body parsing that returns either the parsed value or a 400 response.parseSportParam/parseAtParam/parseOptimalWindHalfWidthDegParam(forecastQuery.ts) — common forecast query-string parsers shared across forecast routes.
- Session/auth through Auth.js (
src/auth.ts,src/app/api/auth/[...nextauth]/route.ts). Respect existing patterns for protected data (Prisma queries scoped by user where applicable).
Project conventions (preserve these)
The codebase has been deliberately reshaped around a few patterns; keep new work consistent with them.
src/components/MapHub.tsxis an orchestrator, not a god component. Heavy state and effects belong in dedicated hooks undersrc/components/map-hub/hooks/(useMapBundle,useForecastRanking,useMapInteractionMode, etc.). Pure rendering of a sub-region belongs in its own component undersrc/components/map-hub/. The orchestrator should mostly be hook calls + JSX.- Pure logic lives in
src/lib/(geo helpers, ranking heuristics, wind formatting, schema validation, API clients). Each module is testable in Node and is the place to add new unit coverage. Prefer small, single-purpose files — split when a module grows past a couple of clearly different concerns. - API client modules live in
src/lib/<domain>/client.tsand are the only place components callfetchfor backend routes. Tests mockfetchagainst these modules instead of mocking it inside components. - Sidebar UX uses
CollapsibleSectionwithsummarytext so collapsed sections still convey state. New sidebar content should follow the existing tab/section pattern (PlanTab,MapTab,YouTab). - No
as anyin application code; use proper types or a Zod-validated boundary instead.
Maps and weather
- Map UI: MapLibre / react-map-gl, heavy UI in
src/components/(e.g. map hub). Styles/helpers insrc/lib/map/. - Weather:
src/lib/weather/(router, Met.no, Open-Meteo). Keep provider boundaries clear when adding sources or debug flags.
Debugging production / Coolify deployments
When a deploy fails or the app errors in prod:
- Coolify UI (WireGuard) —
http://10.20.0.10:8000→ project fjelllift → deployment logs / runtime logs. - SSH (via infra jump) —
docker logson the app container /coolify-proxyfor Traefik/ACME. - API —
COOLIFY_TOKENfrom 1Passwordvamelivo-infra Coolify API token; see vamelivo-infradocs/runbooks/coolify.md.
Do not paste Coolify/API tokens or DB passwords into source files, AGENTS.md, or commit messages.
Cursor Cloud specific instructions
Services overview
| Service | How to start | Port(s) |
|---|---|---|
| PostGIS (PostgreSQL + PostGIS) | docker compose up -d (service db) |
5432 |
| Mailpit (dev email) | docker compose up -d (service mailpit) |
SMTP 1025, UI 8025 |
| Next.js dev server | npm run dev |
3000 |
Docker in the Cloud VM
Docker is not pre-installed. Install Docker CE, fuse-overlayfs, and switch iptables to legacy before starting dockerd:
sudo dockerd &>/tmp/dockerd.log &
sudo chmod 666 /var/run/docker.sock
The daemon config at /etc/docker/daemon.json must use fuse-overlayfs as the storage driver (required by the VM's nested-container kernel).
Startup sequence (after npm install from update script)
- Start Docker daemon (see above).
docker compose up -d— starts PostGIS and Mailpit.npx prisma migrate deploy— applies pending migrations.npm run dev— starts the Next.js dev server on port 3000.
Environment
.envis created from.env.example; setAUTH_SECRET(e.g.openssl rand -base64 32).DATABASE_URLdefaults to the docker-compose PostGIS.- No external API keys are required for basic development. MapTiler key is optional (OSM/OpenTopoMap fallback works without it).
Lint / Test / Build
Standard commands per package.json scripts — see the npm scripts table in the main section above. All three (npm run lint, npm run test, npm run build) should pass cleanly on a fresh setup.
E2E tests (Playwright)
Require Chromium: npm run playwright:install (or npx playwright install chromium --with-deps). PostGIS must be running and migrated. Playwright auto-starts next dev as the web server.