- Replace raw <textarea> with shadcn Textarea component - Replace raw <button> with shadcn Button (variant='destructive' + icon) - Remove custom className overrides fighting admin dark theme - Add shadcn preference rule to AGENTS.md
10 KiB
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
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). Usesdotenv/configviavitest.setup.ts. - E2E tests:
tests/e2e/*.e2e.spec.ts— Playwright (Chromium only). Auto-startsbun run devviawebServerconfig. 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 bybun run generate:typessrc/payload-generated-schema.ts— regenerated by Payload db-schema generationsrc/app/(payload)/admin/importMap.js— regenerated bybun run generate:importmapsrc/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, GameEventLogs
Access control helpers live in src/utils/access-control/ (isRole, hasRoles). Roles: guest, user, admin, developer.
Event Log System
src/collections/game/GameEventLogs.ts — game-event-logs collection.
- Schema:
system(boolean, defaulttrue— system-generated vs GM/narrative),timestamp,type(text/indexed),message(human-readable),actor(→ users),structure(→ game-structures),targetCollection(select — pick from known collections),targetId(number, polymorphic ref),data(JSON blob). - Access: any logged-in user can read/create. Update restricted to admins/developers. Delete restricted to developers.
- Emit:
emitGameEvent(payload, { type, message, actor?, structure?, targetCollection?, targetId?, data? })insrc/utils/event-log/emit.ts. Silently catches errors (fire-and-forget). Always setssystem: true. - Types:
EventTypesconstants insrc/utils/event-log/eventTypes.ts— add new event categories there without touching the collection. Using these constants gives TypeScript narrowing on thetypefield. - Wiring: Server actions in
actions.tsemit events after successful mutations.GameStructuresafterChange hook catches admin-panel storage edits.StructuresbeforeChange hook emitsstructure:resize. - targetCollection options:
game-structures,structures,resources,assets,vehicles,factions,missions,campaigns,users,technologies,maps. If a new collection is added that should be a valid target, add an option here. - Narrative events: admins/developers can create entries manually in the Payload admin panel with
system: falsefor GM-written narrative events. These are visually distinct in the UI (amber accent, book icon).
UI
EventLedger component (src/components/frontend/storage/EventLedger.tsx):
- Queries the REST API (
/api/game-event-logs) for a givenstructureIdand renders a scrollable feed. - Accepts optional
refreshKeyprop — parent passes an incrementing counter to trigger re-fetch after mutations. - Auto-polls every 30s for background updates.
- Shows actor name prefix ("You" for current user, username for others).
- Filter by event type via multi-select shadcn
DropdownMenuCheckboxItem. - Narrative events (system: false) styled with amber border + book icon.
- On re-fetch, preserves existing entries while loading to avoid flicker.
Adding event logging for new actions
- Add event type constant to
src/utils/event-log/eventTypes.tsif it doesn't exist yet. - If the target collection slug isn't in the
TARGET_COLLECTIONSarray inGameEventLogs.ts, add it. - Call
emitGameEvent(payload, { type: EventTypes.xxx, message: "...", ... })after the successful mutation in the server action. EventLedgerwill pick it up automatically if it's scoped to the samestructureId.
Non-structure events
The EventLedger currently filters by structure.id. For event logs scoped to other entities (e.g., faction finance ledger), a new ledger variant would need to be created that queries by targetCollection + targetId instead.
Narrative Events System
src/collections/world/NarrativeEvents.ts — narrative-events collection. A flowchart-based narrative event editor using @xyflow/react (React Flow) in the Payload admin panel.
- Schema:
name(text, title),summary(textarea),flowData(JSON — React Flow nodes + edges),triggers(group with type select + optional condition JSON). - Access: developer-only (create/update/delete/read).
- Flow editor: custom admin field component at
src/components/admin/narrative-flow/NarrativeFlowEditor.tsx. Renders a React Flow canvas with a toolbar to add node types. - Node types (custom React Flow nodes in
src/components/admin/narrative-flow/):- NarrativeBeat (
NarrativeBeatNode.tsx) — blue, story exposition. One source handle (bottom) + one target handle (top). Fields:title,text. - Choice (
ChoiceNode.tsx) — amber, player decision point. One target handle (top), one source handle per option (right side). Fields:question,options[](each withid,label). - Outcome (
OutcomeNode.tsx) — emerald, terminal result. Target handle only. Fields:title,text,effects[](each withtype,value).
- NarrativeBeat (
- Context menu: right-click any node/edge to open a Radix context menu with a "Delete" action. Deleting a node also removes connected edges.
- Data flow: changes to nodes/edges sync to Payload's form state via
useField().setValue(). Serialization usesJSON.stringifydiffing to avoid loops. - Editing: double-click or click the edit icon on a node to edit its properties via node edit dialog (modal with type-specific fields).
- Dependencies:
@xyflow/react(v12),radix-ui(context-menu primitives),lucide-react(icons). - To add a new node type, create the component + register it in the
nodeTypesobject inNarrativeFlowEditor.tsx.
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-varswarns (prefix unused with_). - Tailwind v4: no
tailwind.config— configured via@tailwindcss/postcssinpostcss.config.mjsand CSS imports. Usecn()from@/lib/utilsfor 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.
Rule: always prefer shadcn/ui components (Dialog, Button, Input, Textarea, Select, etc.) over raw HTML elements or custom-built alternatives. Only build new components when no existing shadcn component fits the need. Do not override shadcn's default dark theme classes with custom bg-* border-* overrides — the admin panel's theme handles styling.
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 databasetest.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.exampleshows MongoDB URI but the app uses PostgreSQL — trustDATABASE_URIformat in.env.testas the real reference.bun run buildpasses--max-old-space-size=8000— the build is memory-intensive.- The
devturboscript uses Turbopack;devanddevsafeuse 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.
.npmrcsetslegacy-peer-deps=truefor dependency resolution compatibility.