1
0
Fork 0
polaris-task-force/AGENTS.md

18 KiB

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

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:

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. 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 Errors.
    • 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.
  • 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.