215 lines
18 KiB
Markdown
215 lines
18 KiB
Markdown
# AGENTS.md — Polaris Task Force
|
|
|
|
## What this is
|
|
|
|
Next.js 16 + Payload CMS 3.79.1 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 # BROKEN under Next 16 — see typecheck below
|
|
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
|
|
|
|
`bun run lint` is **broken** under Next 16 — `next lint` was removed and errors out with `Invalid project directory provided, no such directory: .../lint`. Don't rely on it. The reliable typecheck is:
|
|
|
|
```bash
|
|
npx tsc --noEmit 2>&1 | grep -E "error TS" | grep -v "\.next/"
|
|
```
|
|
|
|
Known pre-existing errors, don't chase them: `src/utils/access-control/hasLogisticsQualification.ts` (lines 12, 19 — `'user' is possibly 'null'`) and `src/payload-generated-schema.ts` (line 1883, generated file — moves when the schema is regenerated). Anything else is yours.
|
|
|
|
## 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 (guests get `LandingPage`, no shell).
|
|
- `src/app/login/` — standalone login route, intentionally OUTSIDE the gated `(frontend)` group. Own dark `<html>` layout that reuses `(frontend)/styles.css`.
|
|
- `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, Shipments
|
|
- **banking/** — BankAccounts, BankTransactions, LedgerEntries
|
|
- **world/** — Maps, NarrativeEvents
|
|
- **server/** — MissionFiles, ModLists
|
|
- **game/** — GameRules (global), GameStructures, GameHardResources, GameEventLogs, GameVehicles
|
|
|
|
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, default `true` — 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? })` in `src/utils/event-log/emit.ts`. Silently catches errors (fire-and-forget). Always sets `system: true`.
|
|
- **Types**: `EventTypes` constants in `src/utils/event-log/eventTypes.ts` — add new event categories there without touching the collection. Using these constants gives TypeScript narrowing on the `type` field.
|
|
- **Wiring**: Server actions in `actions.ts` emit events after successful mutations. `GameStructures` afterChange hook catches admin-panel storage edits. `Structures` beforeChange hook emits `structure:resize`.
|
|
- **targetCollection options**: `game-structures`, `structures`, `resources`, `assets`, `vehicles`, `game-vehicles`, `factions`, `missions`, `campaigns`, `users`, `technologies`, `maps`, `shipments`, `bank-accounts`, `bank-transactions`, `ledger-entries`. 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: false` for 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 given `structureId` and renders a scrollable feed.
|
|
- Accepts optional `refreshKey` prop — 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
|
|
1. Add event type constant to `src/utils/event-log/eventTypes.ts` if it doesn't exist yet.
|
|
2. If the target collection slug isn't in the `TARGET_COLLECTIONS` array in `GameEventLogs.ts`, add it.
|
|
3. Call `emitGameEvent(payload, { type: EventTypes.xxx, message: "...", ... })` after the successful mutation in the server action.
|
|
4. `EventLedger` will pick it up automatically if it's scoped to the same `structureId`.
|
|
|
|
### 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.
|
|
|
|
## Shipments & game tick
|
|
|
|
Shipping simulation (`src/collections/logistics/Shipments.ts`, `src/scripts/`, `src/lib/shipping.ts`, `src/lib/distance.ts`):
|
|
|
|
- **Shipment fields**: `origin`/`destination` → `game-structures`, `transportVehicle` → `game-vehicles`, `cargo[]` (relationship to resources/assets/vehicles + amount), `distance`, `fuelCost`/`fuelConsumed`, `status` (`pending`/`dispatched`/`in_transit`/`arrived`/`completed`/`cancelled`/`failed`/`stranded`), `autoReturn` checkbox, `failureReason`.
|
|
- **Game tick**: `bun run payload game-tick` — a `bin` registered on `payload.config.ts`, NOT an npm script. It processes active shipments + fuel consumption, then `process.exit(0)`.
|
|
- **Arrival handling** (`src/scripts/processShipmentTick.ts`): destination storage rules are re-checked on arrival; rejected cargo bounces back to origin, shipment goes `failed`, `ShipmentFail` event logged.
|
|
- **GameRules tuning**: `proximityThreshold` and `gameTickIntervalMinutes` live on the global `game-rules` doc.
|
|
- **UI**: `src/app/(frontend)/logistics/shipments/` (list + `[id]` detail with `ShipmentActions` controls), `src/app/(frontend)/logistics/game-vehicles/` (deployed vehicle views).
|
|
|
|
### Realtime SSE pipeline (in-memory, single-instance only)
|
|
|
|
gameTick → POST `/api/game-tick/notify` (guarded by `x-game-tick-secret` header = `GAME_TICK_NOTIFY_SECRET` env) → in-process bus (`src/lib/realtime/bus.ts`, module-level subscriber Set) → SSE `GET /api/realtime` → `GameTickRealtime` (`src/components/frontend/realtime/`) calls `router.refresh()` and dispatches `ptf:game-tick` → consumers via `src/hooks/useGameTick.ts` (`ShipmentToasts`, `EventLedger`). **Will not work across multiple server instances.**
|
|
|
|
- `GameTickRealtime` mounts only for logged-in users; `ShipmentToasts` only for logistics-qualified users.
|
|
- `hasLogisticsQualification(payload, user)` (`src/utils/access-control/hasLogisticsQualification.ts`) queries Profiles `progression.qualifications` for "logistics" (case-insensitive); admin/developer always pass. Used to gate logistics-only UI.
|
|
|
|
## Storage rules (logistics)
|
|
|
|
`Structure` collection has `allowedStorage` (per-item caps), `prohibitedStorage`, and `restrictToAllowed` (whitelist gate) under `storage`. Shared logic in `src/lib/storageRules.ts` (`checkStorageDeposit`, `isResourceProhibited`, `isResourceWhitelisted`, `getResourceStorageCap`, `storageViolationMessage`).
|
|
|
|
- Enforcement order: **prohibited → whitelist → per-item cap**, counting grid + void storage.
|
|
- Enforced in: `structures/actions.ts` (`addResource`, `transferResource`, `placeResourceOnGrid`), `shipments/actions.ts` (`createShipment` destination check), and `processShipmentTick.ts` on arrival.
|
|
- UI: `ManageStorageDialog` caps deposits to `min(mass, allowance)` and filters non-whitelisted items; structure page shows an amber "whitelist mode" banner when `restrictToAllowed` is set.
|
|
|
|
## Banking System
|
|
|
|
`src/collections/banking/` — `bank-accounts`, `bank-transactions`, `ledger-entries`. Per-person money for a future market feature plus unit/faction treasuries. Admin group: **Banking**.
|
|
|
|
- **BankAccounts**: `name`, `accountType` (`treasury`/`faction`/`personal`), `ownerFaction`/`ownerUser` (relationship, conditionally shown by type), `currency` (→ resources, defaults to the Game Rules main currency), `balance` (number, admin read-only — maintained by transactions), `status` (`open`/`frozen`/`closed`). Read: any logged-in user. Create/update: admin/developer. Delete: developer only.
|
|
- **BankTransactions**: `transactionNumber` (unique, auto-generated), `type` (`deposit`/`withdrawal`/`transfer`/`payment`/`fee`/`salary`/`adjustment`), `fromAccount`/`toAccount` (→ bank-accounts, optional per type), `amount`, `fee`, `memo`, `actor` (→ users), `status` (`completed`/`reversed`), `reference` (reversal), `timestamp`. Access: developer create/update/delete, any logged-in read.
|
|
- `transactionNumber` is auto-generated in a **beforeValidate hook** (`TXN-<base36 ts>-<rand>`) when empty. **Do not require callers to pass it.** Callers may pass `""` to satisfy TS on the required field — the hook treats falsy as missing.
|
|
- **LedgerEntries**: one per affected account per transaction. `account`, `transaction`, `type`, signed `amount` (positive = credit, negative = debit), `balanceAfter`, `memo`, `timestamp`. Read: any logged-in user. Create/update/delete: developer only.
|
|
- **Service lib**: `src/lib/banking/index.ts`.
|
|
- `applyTransaction(payload, { type, fromAccountId?, toAccountId?, amount, fee?, memo?, actorId? })` — single source of truth for balance math. Validates accounts exist/open/frozen + sufficient funds, creates the `bank-transactions` doc, appends ledger entries (signed), updates both balances, returns the transaction. Throws descriptive `Error`s.
|
|
- `createAccount(payload, { name, accountType, ownerFactionId?, ownerUserId? })` — creates a zeroed account in the main currency; throws if no main currency is set in Game Rules.
|
|
- `ensurePersonalAccount(payload, userId)` — finds-or-creates a personal account for a user (dedup).
|
|
- `getMainCurrencyId` / `getMainCurrencyName` — resolve the Game Rules `mainCurrency` → resource.
|
|
- `src/lib/banking/format.ts` — `formatAmount`, `currencyLabel`, `formatDate`. Currency labels prefer the resource's `name` (e.g. "Gold") over `codeName` (e.g. "res_gold").
|
|
- **Server actions**: `src/app/(frontend)/logistics/banking/actions.ts`. `createBankAccount` (regular users may only create their own personal account; treasury/faction require a manager), `ensureMyAccount`, `depositFunds`/`withdrawFunds`/`transferFunds`. Permission model: personal accounts are owner- or manager-only; **treasury/faction accounts are manager-only**. Manager = admin/developer or logistics-qualified (`hasLogisticsQualification`). Emits `finance:deposit` / `finance:withdraw` / `finance:transfer` / `bank:account-create` events.
|
|
- **UI**: `src/app/(frontend)/logistics/banking/` (overview + `[id]` detail), components in `src/components/frontend/banking/` (`BankingOverview`, `AccountCard`, `AccountDetail`, `CreateAccountDialog`, `BankTransactionDialog`, `LedgerTable`, `MyWalletCard`). Sidebar entry "Banking" under Logistics.
|
|
- **Event targets**: `bank-accounts`, `bank-transactions`, `ledger-entries` added to `GameEventLogs` `TARGET_COLLECTIONS` and `emit.ts` `targetCollection` union. Finance event types in `eventTypes.ts`.
|
|
- **Gotchas**: Payload's create TS overloads reject `undefined` on relationship/required fields — pass `null` for empty relationships and a concrete value (`""` for `transactionNumber`) or TS falls through to the draft-variant and errors `Property 'draft' is missing`. No migration has been added for these collections yet (project relies on dev `push: true`).
|
|
|
|
## 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 with `id`, `label`).
|
|
- **Outcome** (`OutcomeNode.tsx`) — emerald, terminal result. Target handle only. Fields: `title`, `text`, `effects[]` (each with `type`, `value`).
|
|
- **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 uses `JSON.stringify` diffing 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 `nodeTypes` object in `NarrativeFlowEditor.tsx`.
|
|
|
|
## Auth
|
|
|
|
Username-based login (no email login). Users log in via Payload admin with `username` only.
|
|
|
|
- `src/app/(frontend)/layout.tsx` is the gate: guests render `LandingPage` (no app shell); authed users get sidebar + `GameTickRealtime`.
|
|
- `/login` (`src/app/login/page.tsx` + `src/components/frontend/auth/LoginForm.tsx`) POSTs `{ username, password }` to `/api/users/login`, then `router.push("/")` + `router.refresh()`. The page redirects already-authed users to `/`.
|
|
- Logout lives in `NavUser` (sidebar) → `/api/users/logout`.
|
|
- Tip: a corrupted `payload-token` cookie causes an infinite login loop (`Unexpected end of JSON input` on `/api/users/me`) — clear cookies/use incognito. Dev user `dev` / `Test123`.
|
|
|
|
## 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`.
|
|
|
|
**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. `DATABASE_URI` line still shows MongoDB (outdated — trust `.env.test` for the real Postgres format), but `APP_URL` + `GAME_TICK_NOTIFY_SECRET` are current and required by the game tick.
|
|
- `.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.
|