From 1eddf741ff4ab1378ed3a3847df1949be3c2e3c9 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Mon, 27 Jul 2026 17:48:43 -0400 Subject: [PATCH] feat: add AGENTS.md with technical documentation and configure email sending via Nodemailer --- AGENTS.md | 107 ++++++++++++++++++++++++++++++++++++++++++ bun.lock | 13 ++++- package.json | 3 ++ src/payload.config.ts | 14 ++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4c247a9 --- /dev/null +++ b/AGENTS.md @@ -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 ` 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. diff --git a/bun.lock b/bun.lock index a5c3933..9c2a8a8 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "polaris-task-force", "dependencies": { "@payloadcms/db-postgres": "3.68.5", + "@payloadcms/email-nodemailer": "^3.86.0", "@payloadcms/next": "3.68.5", "@payloadcms/payload-cloud": "3.68.5", "@payloadcms/richtext-lexical": "3.68.5", @@ -20,6 +21,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.1.18", + "@types/nodemailer": "^8.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cross-env": "^10.1.0", @@ -27,6 +29,7 @@ "graphql": "^16.12.0", "lucide-react": "^0.561.0", "next": "16.0.10", + "nodemailer": "^9.0.3", "payload": "3.68.5", "postcss": "^8.5.6", "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/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=="], @@ -815,6 +818,8 @@ "@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/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=="], - "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=="], @@ -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/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=="], "@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=="], diff --git a/package.json b/package.json index 22ae182..5d68f2c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@payloadcms/db-postgres": "3.68.5", + "@payloadcms/email-nodemailer": "^3.86.0", "@payloadcms/next": "3.68.5", "@payloadcms/payload-cloud": "3.68.5", "@payloadcms/richtext-lexical": "3.68.5", @@ -39,6 +40,7 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/postcss": "^4.1.18", + "@types/nodemailer": "^8.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cross-env": "^10.1.0", @@ -46,6 +48,7 @@ "graphql": "^16.12.0", "lucide-react": "^0.561.0", "next": "16.0.10", + "nodemailer": "^9.0.3", "payload": "3.68.5", "postcss": "^8.5.6", "react": "19.2.3", diff --git a/src/payload.config.ts b/src/payload.config.ts index b589421..b34153e 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -29,6 +29,8 @@ import { GameRules } from "@/collections/game/GameRules"; import { GameStructures } from "@/collections/game/GameStructures"; import { GameHardResources } from "@/collections/game/GameHardResources"; import { Experience } from "@/collections/users/Experience"; +import { nodemailerAdapter } from "@payloadcms/email-nodemailer"; +import nodemailer from "nodemailer"; const filename = fileURLToPath(import.meta.url); const dirname = path.dirname(filename); @@ -112,4 +114,16 @@ export default buildConfig({ // payloadCloudPlugin(), // 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 || "", + }, + }), + }) });