1
0
Fork 0

feat: add AGENTS.md with technical documentation and configure email sending via Nodemailer

This commit is contained in:
Jason Fraley 2026-07-27 17:48:43 -04:00
parent ff1f44832e
commit 1eddf741ff
4 changed files with 135 additions and 2 deletions

107
AGENTS.md Normal file
View file

@ -0,0 +1,107 @@
# AGENTS.md — Polaris Task Force
## What this is
Next.js 16 + Payload CMS 3.68.5 app for an Arma 3 unit. PostgreSQL database via `@payloadcms/db-postgres` + Drizzle. Tailwind CSS v4 (no config file — CSS-based). shadcn/ui (new-york style, `lucide` icons). Dark-themed frontend.
## Package manager
**Bun** is the primary package manager (`bun.lock`, `bunfig.toml`). `pnpm-lock.yaml` also exists; use Bun for installs and running scripts.
## Essential commands
```bash
bun install # install deps
bun run dev # dev server (webpack, localhost:3000)
bun run devsafe # clears .next cache then dev
bun run build # production build (--max-old-space-size=8000, webpack)
bun run lint # ESLint (next/core-web-vitals + next/typescript)
bun run test # runs test:int then test:e2e sequentially
bun run test:int # vitest integration tests only
bun run test:e2e # playwright e2e tests only
bun run db # drizzle-kit wrapper (e.g. bun run db migrate)
bun run generate:types # regenerates payload-types.ts
bun run generate:importmap # regenerates payload admin importMap
```
## Type checking / linting order
`bun run lint` runs ESLint. There is no separate `typecheck` script — TypeScript errors surface during `bun run lint` and `bun run build`.
## Test details
- **Integration tests**: `tests/int/**/*.int.spec.ts` — Vitest with jsdom. Requires a live PostgreSQL database (connection from `.env`). Uses `dotenv/config` via `vitest.setup.ts`.
- **E2E tests**: `tests/e2e/*.e2e.spec.ts` — Playwright (Chromium only). Auto-starts `bun run dev` via `webServer` config. Currently minimal (homepage smoke test).
- Run a single integration test: `bun run vitest run tests/int/api.int.spec.ts`
- Run a single e2e test: `bun run playwright test tests/e2e/frontend.e2e.spec.ts`
## Generated files — never edit manually
- `src/payload-types.ts` — regenerated by `bun run generate:types`
- `src/payload-generated-schema.ts` — regenerated by Payload db-schema generation
- `src/app/(payload)/admin/importMap.js` — regenerated by `bun run generate:importmap`
- `src/app/(payload)/layout.tsx` — auto-generated by Payload
## Path aliases
- `@/*` → `./src/*`
- `@payload-config` → `./src/payload.config.ts`
## App structure
- `src/app/(frontend)/` — public-facing pages (dashboard, logistics, home). Layout has sidebar + auth check.
- `src/app/(payload)/` — Payload admin panel and API routes. Auto-generated layout.
- `src/app/my-route/` — example custom API route.
## Collections (Payload CMS)
Organized by domain under `src/collections/`:
- **users/** — Users (auth, username login), Ranks, Profiles, Awards, Qualifications, Assignments, Experience
- **intelligence/** — Missions, Campaigns, Factions, Technologies
- **logistics/** — Assets, Resources, Vehicles, Structures
- **world/** — Maps, NarrativeEvents
- **server/** — MissionFiles, ModLists
- **game/** — GameRules (global), GameStructures, GameHardResources
Access control helpers live in `src/utils/access-control/` (isRole, hasRoles). Roles: `guest`, `user`, `admin`, `developer`.
## Auth
Username-based login (no email login). Users log in via Payload admin with `username` only.
## Database
PostgreSQL via `@payloadcms/db-postgres`. Schema defined in `src/payload-generated-schema.ts`. Migrations in `src/migrations/` (timestamp-named .ts + .json pairs, registered in `migrations/index.ts`). Drizzle config reads `DATABASE_URI` from env.
In development, `postgresAdapter` uses `push: true` to auto-sync schema.
## Code style
- **Prettier**: double quotes, trailing commas (all), 100 char print width, semicolons.
- **ESLint**: `next/core-web-vitals` + `next/typescript`. `@typescript-eslint/no-unused-vars` warns (prefix unused with `_`).
- **Tailwind v4**: no `tailwind.config` — configured via `@tailwindcss/postcss` in `postcss.config.mjs` and CSS imports. Use `cn()` from `@/lib/utils` for class merging.
## UI components
shadcn/ui components live in `src/components/ui/`. Use `bunx shadcn@latest add <component>` to add new ones. Frontend components in `src/components/frontend/`. Payload admin custom components referenced in `payload.config.ts` under `admin.components`.
## Environment
- `.env` — local dev (PostgreSQL connection string + PAYLOAD_SECRET)
- `.env.example` — template (still shows MongoDB URI — outdated, the app uses PostgreSQL)
- `.env.test` — test/staging database
- `test.env` — NODE_OPTIONS for playwright (loaded by playwright config)
## Deploy
`bun run deploy` bumps patch version (via `bun pm version patch`), builds, then runs `build/deploy.sh`. The `build/` directory is gitignored so the deploy script is not in the repo.
## Gotchas
- `.env.example` shows MongoDB URI but the app uses PostgreSQL — trust `DATABASE_URI` format in `.env.test` as the real reference.
- `bun run build` passes `--max-old-space-size=8000` — the build is memory-intensive.
- The `devturbo` script uses Turbopack; `dev` and `devsafe` use webpack. These are different bundlers with different behavior.
- Playwright tests auto-start the dev server — make sure port 3000 is free before running e2e.
- Payload admin layout and importMap are auto-generated — do not edit by hand.
- `.npmrc` sets `legacy-peer-deps=true` for dependency resolution compatibility.

View file

@ -6,6 +6,7 @@
"name": "polaris-task-force", "name": "polaris-task-force",
"dependencies": { "dependencies": {
"@payloadcms/db-postgres": "3.68.5", "@payloadcms/db-postgres": "3.68.5",
"@payloadcms/email-nodemailer": "^3.86.0",
"@payloadcms/next": "3.68.5", "@payloadcms/next": "3.68.5",
"@payloadcms/payload-cloud": "3.68.5", "@payloadcms/payload-cloud": "3.68.5",
"@payloadcms/richtext-lexical": "3.68.5", "@payloadcms/richtext-lexical": "3.68.5",
@ -20,6 +21,7 @@
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/nodemailer": "^8.0.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
@ -27,6 +29,7 @@
"graphql": "^16.12.0", "graphql": "^16.12.0",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"next": "16.0.10", "next": "16.0.10",
"nodemailer": "^9.0.3",
"payload": "3.68.5", "payload": "3.68.5",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "19.2.3", "react": "19.2.3",
@ -495,7 +498,7 @@
"@payloadcms/drizzle": ["@payloadcms/drizzle@3.68.5", "", { "dependencies": { "console-table-printer": "2.12.1", "dequal": "2.0.3", "drizzle-orm": "0.44.7", "prompts": "2.4.2", "to-snake-case": "1.0.0", "uuid": "9.0.0" }, "peerDependencies": { "payload": "3.68.5" } }, "sha512-+izZ+08gPD8L7i0hRYOfdoVspUyer4eEHbc1NdD/hYSwLIw/5vdR+Q7T2wswXuqaj8maZZS7dCLdmVxQlHa44g=="], "@payloadcms/drizzle": ["@payloadcms/drizzle@3.68.5", "", { "dependencies": { "console-table-printer": "2.12.1", "dequal": "2.0.3", "drizzle-orm": "0.44.7", "prompts": "2.4.2", "to-snake-case": "1.0.0", "uuid": "9.0.0" }, "peerDependencies": { "payload": "3.68.5" } }, "sha512-+izZ+08gPD8L7i0hRYOfdoVspUyer4eEHbc1NdD/hYSwLIw/5vdR+Q7T2wswXuqaj8maZZS7dCLdmVxQlHa44g=="],
"@payloadcms/email-nodemailer": ["@payloadcms/email-nodemailer@3.68.5", "", { "dependencies": { "nodemailer": "7.0.9" }, "peerDependencies": { "payload": "3.68.5" } }, "sha512-aMVzT88vnEw5lChq33vlks68IukNHyhU7skWAFhI/B9e6+ILCVx9uhm/uJZ6CRkO2RP5veIuKMuCgGZy2EBcaQ=="], "@payloadcms/email-nodemailer": ["@payloadcms/email-nodemailer@3.86.0", "", { "dependencies": { "nodemailer": "^9.0.1" }, "peerDependencies": { "payload": "3.86.0" } }, "sha512-z8Knd6w1ToPSc1yunage9dQS8pApIBwTK40PMEsQd7Hl79BxbQcEzGrWTKhTiJHu5K4LZJOXjZOtdNWVI07Jyw=="],
"@payloadcms/graphql": ["@payloadcms/graphql@3.68.5", "", { "dependencies": { "graphql-scalars": "1.22.2", "pluralize": "8.0.0", "ts-essentials": "10.0.3", "tsx": "4.20.6" }, "peerDependencies": { "graphql": "16.12.0", "payload": "3.68.5" }, "bin": { "payload-graphql": "bin.js" } }, "sha512-Q+cAD/o9p7rOmLWF3lLFQ+s1+d0mECZjDBC2FV3xDYdawCfVZSMtffM6KRbWQzipeKEcSVnz+KeBsEjdN9o6Ag=="], "@payloadcms/graphql": ["@payloadcms/graphql@3.68.5", "", { "dependencies": { "graphql-scalars": "1.22.2", "pluralize": "8.0.0", "ts-essentials": "10.0.3", "tsx": "4.20.6" }, "peerDependencies": { "graphql": "16.12.0", "payload": "3.68.5" }, "bin": { "payload-graphql": "bin.js" } }, "sha512-Q+cAD/o9p7rOmLWF3lLFQ+s1+d0mECZjDBC2FV3xDYdawCfVZSMtffM6KRbWQzipeKEcSVnz+KeBsEjdN9o6Ag=="],
@ -815,6 +818,8 @@
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
"@types/nodemailer": ["@types/nodemailer@8.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
"@types/pg": ["@types/pg@8.10.2", "", { "dependencies": { "@types/node": "25.0.3", "pg-protocol": "1.10.3", "pg-types": "4.1.0" } }, "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw=="], "@types/pg": ["@types/pg@8.10.2", "", { "dependencies": { "@types/node": "25.0.3", "pg-protocol": "1.10.3", "pg-types": "4.1.0" } }, "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw=="],
@ -1579,7 +1584,7 @@
"node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="],
"nodemailer": ["nodemailer@7.0.9", "", {}, "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ=="], "nodemailer": ["nodemailer@9.0.3", "", {}, "sha512-n+YP+NKwR5zRWa60k3GiQ6Q3B4KXCoAw40dAKeCtYn020iNN74aWK2liXIC3ZEATeGql7we3tE3t8QwhY0eskw=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
@ -2081,6 +2086,10 @@
"@payloadcms/graphql/tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "0.25.11", "get-tsconfig": "4.12.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], "@payloadcms/graphql/tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "0.25.11", "get-tsconfig": "4.12.0" }, "optionalDependencies": { "fsevents": "2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="],
"@payloadcms/payload-cloud/@payloadcms/email-nodemailer": ["@payloadcms/email-nodemailer@3.68.5", "", { "dependencies": { "nodemailer": "7.0.9" }, "peerDependencies": { "payload": "3.68.5" } }, "sha512-aMVzT88vnEw5lChq33vlks68IukNHyhU7skWAFhI/B9e6+ILCVx9uhm/uJZ6CRkO2RP5veIuKMuCgGZy2EBcaQ=="],
"@payloadcms/payload-cloud/nodemailer": ["nodemailer@7.0.9", "", {}, "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ=="],
"@payloadcms/richtext-lexical/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "@payloadcms/richtext-lexical/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "optionalDependencies": { "@types/react": "19.2.7", "@types/react-dom": "19.2.3" }, "peerDependencies": { "react": "19.2.3", "react-dom": "19.2.3" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],

View file

@ -25,6 +25,7 @@
}, },
"dependencies": { "dependencies": {
"@payloadcms/db-postgres": "3.68.5", "@payloadcms/db-postgres": "3.68.5",
"@payloadcms/email-nodemailer": "^3.86.0",
"@payloadcms/next": "3.68.5", "@payloadcms/next": "3.68.5",
"@payloadcms/payload-cloud": "3.68.5", "@payloadcms/payload-cloud": "3.68.5",
"@payloadcms/richtext-lexical": "3.68.5", "@payloadcms/richtext-lexical": "3.68.5",
@ -39,6 +40,7 @@
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/nodemailer": "^8.0.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
@ -46,6 +48,7 @@
"graphql": "^16.12.0", "graphql": "^16.12.0",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"next": "16.0.10", "next": "16.0.10",
"nodemailer": "^9.0.3",
"payload": "3.68.5", "payload": "3.68.5",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "19.2.3", "react": "19.2.3",

View file

@ -29,6 +29,8 @@ import { GameRules } from "@/collections/game/GameRules";
import { GameStructures } from "@/collections/game/GameStructures"; import { GameStructures } from "@/collections/game/GameStructures";
import { GameHardResources } from "@/collections/game/GameHardResources"; import { GameHardResources } from "@/collections/game/GameHardResources";
import { Experience } from "@/collections/users/Experience"; import { Experience } from "@/collections/users/Experience";
import { nodemailerAdapter } from "@payloadcms/email-nodemailer";
import nodemailer from "nodemailer";
const filename = fileURLToPath(import.meta.url); const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename); const dirname = path.dirname(filename);
@ -112,4 +114,16 @@ export default buildConfig({
// payloadCloudPlugin(), // payloadCloudPlugin(),
// storage-adapter-placeholder // storage-adapter-placeholder
], ],
email: nodemailerAdapter({
defaultFromAddress: process.env.EMAIL_FROM_ADDRESS || "",
defaultFromName: process.env.EMAIL_FROM_NAME || "",
transport: nodemailer.createTransport({
host: process.env.EMAIL_HOST || "",
port: Number(process.env.EMAIL_PORT) || 587,
auth: {
user: process.env.EMAIL_USERNAME || "",
pass: process.env.EMAIL_PASSWORD || "",
},
}),
})
}); });