1
0
Fork 0

docs: refresh AGENTS.md guides

Update root and per-directory AGENTS.md files to reflect the current
state of the codebase: collection inventory, RBAC registry, base
management, personnel pages, training minigames, Discord bot transfer
and evaluation-reminder flows, banking currency config, migration
wrapper, and Playwright/Vivaldi e2e details.
This commit is contained in:
Jason Fraley 2026-09-06 05:32:53 -04:00
parent 55a03e8094
commit 33174572cc
6 changed files with 139 additions and 47 deletions

View file

@ -14,7 +14,7 @@ Next.js 16 + Payload CMS 3.88.0 app for an Arma 3 unit. PostgreSQL database via
## Package manager ## Package manager
**Bun** is the primary package manager (`bun.lock`, `bunfig.toml`). `pnpm-lock.yaml` also exists; use Bun for installs and running scripts. **Bun** is the primary package manager (`bun.lock`; `bunfig.toml` is empty). `package.json` declares `packageManager: pnpm@9.15.4` and several scripts internally invoke pnpm (`db`, `test`, `test:e2e`) — always invoke them via `bun run <script>`, which still works; just don't edit those scripts to use bun directly without testing. `pnpm-lock.yaml` also exists.
## Essential commands ## Essential commands
@ -45,7 +45,7 @@ Known pre-existing errors (not yours, don't widen scope to fix them): `src/colle
## Test details ## 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`. - **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). - **E2E tests**: `tests/e2e/*.e2e.spec.ts` — Playwright with a project named "vivaldi" that launches the **Vivaldi binary** (`/usr/bin/vivaldi`, headless args in `playwright.config.ts`) — not plain Chromium. The `webServer` config auto-starts `pnpm dev` (with `reuseExistingServer: true`). Currently minimal (homepage smoke test).
- Run a single integration test: `bun run vitest run tests/int/api.int.spec.ts` - 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` - Run a single e2e test: `bun run playwright test tests/e2e/frontend.e2e.spec.ts`
@ -101,15 +101,21 @@ Break these and you get runtime errors or silently dead code:
Organized by domain under `src/collections/`: Organized by domain under `src/collections/`:
- **users/** — Users (auth, username login), Ranks, Profiles, Awards, Qualifications, Assignments, Experience - **users/** — Users (auth, username login), Ranks, Profiles (incl. minigame stats), Awards, Qualifications, Assignments, AssignmentTransfers, Experience, Evaluations, Roles (dynamic RBAC), UserNotifications
- **intelligence/** — Missions, Campaigns, Factions, Technologies - **intelligence/** — Missions, MissionAttendances, Campaigns, Factions, Technologies
- **logistics/** — Assets, Resources, Vehicles, Structures, Shipments - **logistics/** — Assets, Resources, Vehicles, Structures (with staffing defaults), Shipments
- **banking/** — BankAccounts, BankTransactions, LedgerEntries - **banking/** — BankAccounts, BankTransactions, LedgerEntries
- **market/** — MarketListings, MarketNegotiations
- **locker/** — LockerStorages, Loadouts
- **world/** — Maps, NarrativeEvents - **world/** — Maps, NarrativeEvents
- **server/** — MissionFiles, ModLists - **server/** — MissionFiles, ModLists, GameServers, ArmaCommands, ArmaSyncEvents
- **game/** — GameRules (global), GameStructures, GameHardResources, GameEventLogs, GameVehicles, GameNpcs - **game/** — GameRules (global, incl. currency display names + staff hiring flags), GameStructures, GameHardResources, GameEventLogs, GameVehicles, GameNpcs, NpcStaffing, LaborClassifications (+ shared `staffingFields.ts` factory)
- **projects/** — Projects, Labels, Releases, Sprints
- **tickets/** — Tickets, TicketVotes
- **wiki/** — WikiPages, WikiRevisions, WikiTemplates
- top-level — `Media.ts` (image uploads, used by wiki), `Shims.ts` (audience-targeted content)
Access control helpers live in `src/utils/access-control/` (isRole, hasRoles). Roles: `guest`, `user`, `admin`, `developer`. Access control helpers live in `src/utils/access-control/` (isRole, hasRoles). Full RBAC lives in `src/permissions/index.ts` + the `Roles` collection. Roles: `guest`, `user`, `admin`, `developer`.
## Event Log System ## Event Log System
@ -120,7 +126,7 @@ Access control helpers live in `src/utils/access-control/` (isRole, hasRoles). R
- **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`. - **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. - **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`. - **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`, `game-npcs`, `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. - **targetCollection options**: `game-structures`, `structures`, `resources`, `assets`, `vehicles`, `game-vehicles`, `game-npcs`, `npc-staffing`, `factions`, `missions`, `campaigns`, `users`, `technologies`, `maps`, `shipments`, `bank-accounts`, `bank-transactions`, `ledger-entries`, `market-listings`, `market-negotiations`, `locker-storages`, `loadouts`, `evaluations`, `tickets`, `wiki-pages`, `wiki-revisions`, `wiki-templates`, `game-servers`, `assignment-transfers`. 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). - **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 ### UI
@ -152,7 +158,8 @@ Shipping simulation (`src/collections/logistics/Shipments.ts`, `src/scripts/`, `
- **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`. - **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)`. - **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)`.
- **Bin file logging**: all five bins (`game-tick`, `market-tick`, `mission-tick`, `server-tick`, `generate-mission`) tee their logs to daily files `logs/<bin-key>/<YYYY-MM-DD>.log` (UTC) via `createBinLogger` in `src/scripts/lib/binFileLogger.ts` — in addition to the console. Writes are `appendFileSync` (bin scripts `process.exit` immediately, so async streams would truncate). A fatal error in a bin is caught, logged to the file, and exits code 1. Directory override: `BIN_LOG_DIR` env (defaults to `<cwd>/logs` — inside Docker point it at a persistent volume). `logs/` is gitignored. - **Bin file logging**: all six bins (`game-tick`, `market-tick`, `mission-tick`, `server-tick`, `base-tick`, `generate-mission`) tee their logs to daily files `logs/<bin-key>/<YYYY-MM-DD>.log` (UTC) via `createBinLogger` in `src/scripts/lib/binFileLogger.ts` — in addition to the console. Writes are `appendFileSync` (bin scripts `process.exit` immediately, so async streams would truncate). A fatal error in a bin is caught, logged to the file, and exits code 1. Directory override: `BIN_LOG_DIR` env (defaults to `<cwd>/logs` — inside Docker point it at a persistent volume). `logs/` is gitignored.
- **Base tick**: `bun run payload base-tick` — bin registered like the others (needs external cron). Runs `processBaseTick` (`src/lib/base/tick.ts`): pays staff salaries from the structure treasury (banking `applyTransaction` type `salary`), charges structure maintenance, flags upkeep shortages (see Base Management below). Emits `staff:salary-paid`/`staff:salary-unpaid`/`structure:maintenance-paid`/`structure:maintenance-unpaid` events. Notifies clients via the same SSE path as the other ticks (`source: "base-tick"`).
- **Mission auto-completion**: `bun run payload mission-tick` — bin registered like the others (needs external cron). Sweeps missions whose status is `Scheduled`/`Active` and whose scheduled date (`classification.startDateTime`) has fully passed — from the start of the following **server-local** day — and sets them to `Completed`, emitting a `mission:auto-complete` event per mission (logic: `src/lib/intelligence/missionLifecycle.ts`, test: `tests/int/mission-lifecycle.int.spec.ts`). Draft statuses (Concept/Planning/Ready) and terminal statuses (Completed/Cancelled) are never touched; idempotent. Notifies clients via the same SSE path as the other ticks. - **Mission auto-completion**: `bun run payload mission-tick` — bin registered like the others (needs external cron). Sweeps missions whose status is `Scheduled`/`Active` and whose scheduled date (`classification.startDateTime`) has fully passed — from the start of the following **server-local** day — and sets them to `Completed`, emitting a `mission:auto-complete` event per mission (logic: `src/lib/intelligence/missionLifecycle.ts`, test: `tests/int/mission-lifecycle.int.spec.ts`). Draft statuses (Concept/Planning/Ready) and terminal statuses (Completed/Cancelled) are never touched; idempotent. Notifies clients via the same SSE path as the other ticks.
- **Server presence**: `bun run payload server-tick` — bin registered like the others (needs external cron, every minute). Flips `game-servers` docs that claim `status: "online"` but whose last heartbeat (`lastSeenAt`) is older than 180s to `offline`, including online docs with no `lastSeenAt` at all; emits a `server:offline` event per flipped server (logic: `src/lib/arma-bridge/presence.ts`, test: `tests/int/server-presence.int.spec.ts`). Idempotent. Notifies clients via the same SSE path as the other ticks. - **Server presence**: `bun run payload server-tick` — bin registered like the others (needs external cron, every minute). Flips `game-servers` docs that claim `status: "online"` but whose last heartbeat (`lastSeenAt`) is older than 180s to `offline`, including online docs with no `lastSeenAt` at all; emits a `server:offline` event per flipped server (logic: `src/lib/arma-bridge/presence.ts`, test: `tests/int/server-presence.int.spec.ts`). Idempotent. Notifies clients via the same SSE path as the other ticks.
- **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. - **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.
@ -174,6 +181,23 @@ gameTick → POST `/api/game-tick/notify` (guarded by `x-game-tick-secret` heade
- Enforced in: `structures/actions.ts` (`addResource`, `transferResource`, `placeResourceOnGrid`), `shipments/actions.ts` (`createShipment` destination check), and `processShipmentTick.ts` on arrival. - 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. - 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.
## Base Management (staffing & upgrades)
Labor/staffing simulation for structures. Collections in `src/collections/game/`: `npc-staffing` (named NPC staff + labor headcount blocks on game-structures), `labor-classifications` (labor trades: name, key, `defaultUnitSalary`, `availableHeadcount`), and a shared `staffingFields.ts` factory (`createStaffingFields`) that adds `maintenanceCost` / `laborRequirements` / `staffPositions` to both GameStructures instances and Structures blueprints ("Staffing Defaults" group). A `GameStructures` beforeChange hook copies blueprint staffing defaults on create.
- **Service lib**: `src/lib/base/` — `staffing.ts` (`hireStaff`, `fireStaff`, `setLaborHeadcount`, `assertStaffHiringEnabled` — enforces position caps, labor-vs-named-staff rules), `upgrade.ts` (`upgradeStructure` — validates + consumes stored materials, swaps structure type via `upgradesInto`), `storage.ts` (`consumeStorage`, `storedAmount`, ...), `staffingDefaults.ts`, `tick.ts` (`processBaseTick`), `types.ts`.
- **Server actions**: `src/app/(frontend)/logistics/base/actions.ts` — `upgradeStructure`, `hireStaff`, `fireStaff`, `setLaborHeadcount` (logistics-qualification gated, canonical ActionResult pattern).
- **Gate**: Game Rules globals `staffHiringEnabled` + `staffHiringDisabledMessage` disable all hiring when off.
- **Base tick**: see `base-tick` bin under Shipments & game tick.
- **UI**: `src/components/frontend/baseManagement/` (`StaffRoster`, `HireStaffDialog`, `EmploymentCard`, `UpgradeCard`) mounted on the structure detail page (`logistics/structures/[id]`).
- **Events**: `staff:hired` / `staff:terminated` / `staff:headcount-set` / `staff:salary-paid` / `staff:salary-unpaid` / `structure:maintenance-paid` / `structure:maintenance-unpaid` / `structure:upgraded` / `structure:upkeep-short`. Event target: `npc-staffing`.
## Personnel (NPC directory)
- `/personnel/contacts` — NPC directory over `game-npcs` (`src/components/frontend/personnel/`: `NpcDirectory`, `NpcCard`); `/personnel/contacts/[id]` — NPC profile (faction, in-game vs generated badges, vendor listings, `NpcProfile`).
- `/personnel/statistics` — theater-wide NPC stats: faction distribution, in-game vs generated counts, vendor stall count, top 5 vendors by active auto listings.
- Sidebar entries live in `src/components/frontend/blocks/sidebarData.ts` (nav data extracted from `AppSidebar`).
## Banking System ## 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**. `src/collections/banking/` — `bank-accounts`, `bank-transactions`, `ledger-entries`. Per-person money for a future market feature plus unit/faction treasuries. Admin group: **Banking**.
@ -187,7 +211,7 @@ gameTick → POST `/api/game-tick/notify` (guarded by `x-game-tick-secret` heade
- `createAccount(payload, { name, accountType, ownerFactionId?, ownerUserId? })` — creates a zeroed account in the main currency; throws if no main currency is set in Game Rules. - `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). - `ensurePersonalAccount(payload, userId)` — finds-or-creates a personal account for a user (dedup).
- `getMainCurrencyId` / `getMainCurrencyName` — resolve the Game Rules `mainCurrency` → resource. - `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"). - `src/lib/banking/format.ts` — `formatAmount`, `currencyLabel`, `formatDate`. Takes a `CurrencyConfig` (threaded from Game Rules) — labels prefer the configured display names over the resource's `name` (e.g. "Gold") over `codeName` (e.g. "res_gold"), with pluralization.
- **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. - **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. - **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`. - **Event targets**: `bank-accounts`, `bank-transactions`, `ledger-entries` added to `GameEventLogs` `TARGET_COLLECTIONS` and `emit.ts` `targetCollection` union. Finance event types in `eventTypes.ts`.
@ -270,6 +294,15 @@ gameTick → POST `/api/game-tick/notify` (guarded by `x-game-tick-secret` heade
- **Database**: migration batch 28 `src/migrations/20260904_230838_add_wiki_collections.ts`: tables `wiki_pages` (+ `_texts`), `wiki_revisions`, `wiki_templates`, plus the three wiki values added to the `game_event_logs` `target_collection` enum. Dev uses `bun run payload migrate` for schema changes (`push: false`). - **Database**: migration batch 28 `src/migrations/20260904_230838_add_wiki_collections.ts`: tables `wiki_pages` (+ `_texts`), `wiki_revisions`, `wiki_templates`, plus the three wiki values added to the `game_event_logs` `target_collection` enum. Dev uses `bun run payload migrate` for schema changes (`push: false`).
- **Gotchas**: image uploads go through the `uploadWikiImage` server action in `src/app/(frontend)/wiki/actions.ts` (any logged-in user, image MIME only, 8 MB cap, stored in `media` with `read: () => true` so images are public; the editor toolbar's "Upload image" button inserts `![alt](/api/media/file/<filename>)` — requires `experimental.serverActions.bodySizeLimit` (10mb) in `next.config.mjs`); lockdown blocks edit/restore server-side (actions throw `"This page is locked and cannot be edited."`); restore/delete/lock are intelligence-qualification gated; `WikiRevisions` denies create/update/delete outright, so all revision writes go through the service layer with `overrideAccess`. - **Gotchas**: image uploads go through the `uploadWikiImage` server action in `src/app/(frontend)/wiki/actions.ts` (any logged-in user, image MIME only, 8 MB cap, stored in `media` with `read: () => true` so images are public; the editor toolbar's "Upload image" button inserts `![alt](/api/media/file/<filename>)` — requires `experimental.serverActions.bodySizeLimit` (10mb) in `next.config.mjs`); lockdown blocks edit/restore server-side (actions throw `"This page is locked and cannot be edited."`); restore/delete/lock are intelligence-qualification gated; `WikiRevisions` denies create/update/delete outright, so all revision writes go through the service layer with `overrideAccess`.
## Training Minigames
Four XP-earning minigames, each with a page + server actions + a pure client-safe game-rules lib, recording stats under Profiles `progression.minigames.*`:
- **Qualification Course (aim trainer)** — `/qualification` (`src/app/(frontend)/qualification/`), game rules in `src/lib/aim-trainer.ts` (recruit/veteran/elite difficulties, civilian targets, scoring/accuracy/XP, injected rng). Components: `src/components/frontend/qualification/` (`AimTrainerArena`, `AimTrainerGame`, `AimTrainerCanvas`, `AimTrainerLeaderboard`). Action `submitAimTrainerResult` validates result bounds, updates profile stats, awards XP, emits `minigame:aim-trainer`.
- **Radio Traffic** — `/radio`, sprint + watch modes, procedural message generation (`src/components/frontend/radio/messageGenerator.ts`). Action `submitRadioResult` emits `minigame:radio-traffic`; stats under `progression.minigames.radioTraffic` (incl. `bestSprintTimeMs`).
- **Flight Simulator** — `/flappy` (canvas game + leaderboard). **Minefield Clearance (EOD)** — `/eod`.
- Leaderboards match active players by user id (not display name) — see commit 72715c4 if leaderboard queries look wrong.
## Discord Bot ## Discord Bot
A Discord bot living in `src/bot/`, run as a standalone long-running process via `bun run bot` — it imports `@payload-config` directly (same pattern as `src/scripts/` and `src/tools/seed/`) and shares the Postgres DB with the web app. **Under active development and testing.** **Full product requirements live in `docs/bot/context.md`; the approved implementation design is in `docs/bot/design.md` — read both before touching bot code.** A Discord bot living in `src/bot/`, run as a standalone long-running process via `bun run bot` — it imports `@payload-config` directly (same pattern as `src/scripts/` and `src/tools/seed/`) and shares the Postgres DB with the web app. **Under active development and testing.** **Full product requirements live in `docs/bot/context.md`; the approved implementation design is in `docs/bot/design.md` — read both before touching bot code.**
@ -297,7 +330,7 @@ Username-based login (no email login). Users log in via Payload admin with `user
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. 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: false` — run `bun run payload migrate` after schema changes to apply them locally. In development, `postgresAdapter` uses `push: false` — run `bun run payload migrate` after schema changes to apply them locally. The repo also ships a wrapper, `bun run migrate` (`src/scripts/run-migrations.ts`), which adds env selection (dev/stg/prd), `--status`, and a dev-only `--fresh`.
**Always name your migrations**: create them with `bun run payload migrate:create <name>` (snake_case, e.g. `migrate:create add_evaluation_reminders_field`), never bare `migrate:create`. Unnamed migrations get timestamp-only filenames (`20260905_213824.ts`) that say nothing about what they do — several older migrations suffer from this and it makes the migration history and rollback auditing (data-loss review of `DROP COLUMN`/`DROP TABLE` steps) much harder than it needs to be. Named example: `20260906_050900_add_evaluation_reminders_field.ts`. **Always name your migrations**: create them with `bun run payload migrate:create <name>` (snake_case, e.g. `migrate:create add_evaluation_reminders_field`), never bare `migrate:create`. Unnamed migrations get timestamp-only filenames (`20260905_213824.ts`) that say nothing about what they do — several older migrations suffer from this and it makes the migration history and rollback auditing (data-loss review of `DROP COLUMN`/`DROP TABLE` steps) much harder than it needs to be. Named example: `20260906_050900_add_evaluation_reminders_field.ts`.

View file

@ -23,16 +23,21 @@ bot/
events/ events/
interactionCreate.ts # Routes ptf-att: RSVP button interactions interactionCreate.ts # Routes ptf-att: RSVP button interactions
transferInteractions.ts # Handles transfer decision buttons (approve/deny/appeal)
services/ services/
index.ts # Service registry index.ts # Service registry
missionEmbeds.ts # Attendance embed lifecycle + reconcile loop (poll tick) missionEmbeds.ts # Attendance embed lifecycle + reconcile loop (poll tick)
notificationBridge.ts # Poll user-notifications → Discord DMs notificationBridge.ts # Poll user-notifications → Discord DMs
evaluationReminders.ts # Post-mission evaluation reminder DMs (one-shot marker on Missions)
signup.ts # Signup service logic signup.ts # Signup service logic
transferRequests.ts # Polls assignment transfer requests awaiting leader decision
transferDelivery.ts # Transfer leader-request / rejection / info delivery DMs
lib/ lib/
roles.ts # isStaff check roles.ts # isStaff check
resolve.ts # discordId ↔ Payload user lookups resolve.ts # discordId ↔ Payload user lookups
transferMessaging.ts # Transfer embed builders + delivery keys
``` ```
## Command registration scope ## Command registration scope
@ -52,14 +57,24 @@ Bot posts RSVP embeds (Yes/Tentative/No) for future, Ready/Scheduled, visibility
`notificationBridge` polls `user-notifications` (cursor = last seen id; cap 5 DMs/tick). DMs opted-in users (`preferences.discord.enabled`, not in `mutedTypes`). `notificationBridge` polls `user-notifications` (cursor = last seen id; cap 5 DMs/tick). DMs opted-in users (`preferences.discord.enabled`, not in `mutedTypes`).
## Feature flow: evaluation reminders
Once a mission is evaluable (`Completed`, or `Scheduled`/`Active` past its start), sends a one-shot DM per linked user asking them to rate their leadership (leaders also get a "rate your subordinates" section). Recipient plan: `computeEvaluationReminderPlan` in `src/lib/evaluations/reminders.ts`; state marker `evaluationRemindersSentAt` on Missions (withheld if the 40-DM/tick cap is hit — retries next tick). DMs ignore `preferences.discord.enabled` (defaults false, not yet exposed in web UI). Staff can re-send manually via `/remind-evaluations`.
## Feature flow: assignment transfers
`transferRequests` polls `assignment-transfers` for requests awaiting leader decision; leaders approve/deny via buttons handled in `transferInteractions.ts`; DMs are built by `lib/transferMessaging.ts` and sent through `transferDelivery.ts` (leader request, requester rejection notice, decision info). Web side: `src/lib/transfers/` + `/transfers` page.
## Where to look ## Where to look
| Task | Path | | Task | Path |
|------|------| |------|------|
| Add new slash command | `bot/commands/<name>.ts` + register in `bot/commands/index.ts` | | Add new slash command | `bot/commands/<name>.ts` + register in `bot/commands/index.ts` |
| Add button interaction | `bot/events/interactionCreate.ts` | | Add button interaction | `bot/events/interactionCreate.ts` (RSVP) or `transferInteractions.ts` (transfers) |
| Modify embed lifecycle | `bot/services/missionEmbeds.ts` | | Modify embed lifecycle | `bot/services/missionEmbeds.ts` |
| Change DM bridging | `bot/services/notificationBridge.ts` | | Change DM bridging | `bot/services/notificationBridge.ts` |
| Evaluation reminder logic | `bot/services/evaluationReminders.ts` + `src/lib/evaluations/reminders.ts` |
| Transfer workflow | `bot/services/transferRequests.ts` + `lib/transferMessaging.ts` |
| User lookup patterns | `bot/lib/resolve.ts` | | User lookup patterns | `bot/lib/resolve.ts` |
## Anti-patterns ## Anti-patterns

View file

@ -4,29 +4,31 @@
## Overview ## Overview
36 collections across 12 domain groups. Access control defined in `src/permissions/index.ts` (616 lines, 100+ permissions across 25 groups). RBAC check: `hasPermission(payload, user, "collection:action")`. 54 files (~53 collections) across 15 domain groups. Access control defined in `src/permissions/index.ts` (~870 lines, permission registry across groups). RBAC check: `hasPermission(payload, user, "collection:action")`.
## Structure ## Structure
``` ```
collections/ collections/
Media.ts # Generic media upload Media.ts # Image uploads (public read; used by wiki image upload)
Shims.ts # Global: CSS/JS shims (admin-only) Shims.ts # Global: audience-targeted content (admin-only)
users/ 9 files # Users (auth), Ranks, Profiles, Awards, users/ 11 files # Users (auth), Ranks, Profiles (incl. minigame stats), Awards,
# Qualifications, Assignments, Experience, # Qualifications, Assignments, AssignmentTransfers, Experience,
# Roles (dynamic RBAC), UserNotifications # Evaluations, Roles (dynamic RBAC), UserNotifications
intelligence/ 5 files # Missions, MissionAttendances, Campaigns, intelligence/ 5 files # Missions, MissionAttendances, Campaigns, Factions, Technologies
# Factions, Technologies logistics/ 5 files # Structures (staffing defaults), Resources, Assets, Vehicles, Shipments
logistics/ 5 files # Structures, Resources, Assets, Vehicles, Shipments
banking/ 3 files # BankAccounts, BankTransactions, LedgerEntries banking/ 3 files # BankAccounts, BankTransactions, LedgerEntries
market/ 2 files # MarketListings, MarketNegotiations market/ 2 files # MarketListings, MarketNegotiations
locker/ 2 files # LockerStorages, Loadouts locker/ 2 files # LockerStorages, Loadouts
game/ 6 files # GameRules (global), GameStructures, GameVehicles, game/ 9 files # GameRules (global, currency + staffing flags), GameStructures,
# GameNpcs, GameHardResources, GameEventLogs # GameVehicles, GameNpcs, GameHardResources, GameEventLogs,
server/ 2 files # MissionFiles, ModLists # NpcStaffing, LaborClassifications (+ shared staffingFields.ts factory)
server/ 5 files # MissionFiles, ModLists, GameServers, ArmaCommands, ArmaSyncEvents
world/ 2 files # Maps, NarrativeEvents (React Flow editor) world/ 2 files # Maps, NarrativeEvents (React Flow editor)
tickets/ 1 file # Tickets (Lexical rich text) projects/ 4 files # Projects, Labels, Releases, Sprints (Jira-like)
tickets/ 2 files # Tickets (Lexical rich text), TicketVotes
wiki/ 3 files # WikiPages, WikiRevisions, WikiTemplates
``` ```
## Key relationships ## Key relationships
@ -35,11 +37,14 @@ collections/
Users ──┬── Profiles ──┬── Qualifications Users ──┬── Profiles ──┬── Qualifications
│ ├── Awards │ ├── Awards
│ ├── Assignments ── Ranks │ ├── Assignments ── Ranks
│ ├── AssignmentTransfers
│ └── Experience │ └── Experience
├── UserNotifications ├── UserNotifications
├── Evaluations
└── BankAccounts ── BankTransactions ── LedgerEntries └── BankAccounts ── BankTransactions ── LedgerEntries
GameStructures ── Structures (template) ── Resources/Assets/Vehicles GameStructures ── Structures (template) ── Resources/Assets/Vehicles
GameStructures ── NpcStaffing ── LaborClassifications
GameVehicles ── Vehicles (template) GameVehicles ── Vehicles (template)
GameNpcs ── MarketListings ── MarketNegotiations GameNpcs ── MarketListings ── MarketNegotiations
MissionAttendances ── Missions ── Campaigns MissionAttendances ── Missions ── Campaigns
@ -57,8 +62,8 @@ Admin group access: `developer` only for destructive operations, `admin` for rea
## Hooks with side effects ## Hooks with side effects
- `Structures` `beforeChange`: emits `structure:resize` - `Structures` `beforeChange`: emits `structure:resize`; blueprint "Staffing Defaults" group via `staffingFields.ts`
- `GameStructures` `afterChange`: emits storage edit events - `GameStructures` `afterChange`: emits storage edit events; `beforeChange` copies blueprint staffing defaults on create
- `Users` `afterChange`: maintains profile sync - `Users` `afterChange`: maintains profile sync
- `BankTransactions` `beforeValidate`: auto-generates `transactionNumber` - `BankTransactions` `beforeValidate`: auto-generates `transactionNumber`
- `MarketNegotiations`: patience meter enforcement - `MarketNegotiations`: patience meter enforcement

View file

@ -11,20 +11,35 @@
``` ```
components/frontend/ components/frontend/
auth/ 2 files LoginForm, LoginLink (returnTo via usePathname) auth/ 2 files LoginForm, LoginLink (returnTo via usePathname)
account/ 4 files Profile, password, preferences, Discord link account/ 16 files Profile, password, preferences, Discord link
awards/ 4 files Medal/ribbon display, award dialogs
banking/ 9 files Account cards, ledger, deposit/withdraw/transfer dialogs banking/ 9 files Account cards, ledger, deposit/withdraw/transfer dialogs
blocks/ 6 files AppSidebar, NavCore, NavUser, XPDisplay baseManagement/ 4 files StaffRoster, HireStaffDialog, EmploymentCard, UpgradeCard (structure page)
blocks/ 7 files AppSidebar (data in sidebarData.ts), NavCore, NavUser, XPDisplay
dashboard/ 6 files ProfileSummary, QuickStats, MissionBriefing, RecentEvents dashboard/ 6 files ProfileSummary, QuickStats, MissionBriefing, RecentEvents
flappy/ 4 files Canvas game, leaderboard, sounds eod/ 4 files Minefield Clearance minigame
helpdesk/ 6 files Ticket list/detail, create dialog, timeline evaluations/ 0 files EMPTY directory (placeholder — no components yet)
intelligence/ 13 files Mission cards/attendance/comms, campaign/faction cards flappy/ 4 files Flight Simulator canvas game, leaderboard, sounds
locker/ 12 files Grid, equipment editor, loadouts, wardrobe helpdesk/ 8 files Ticket list/detail, create dialog, timeline
impersonation/ 2 files Admin impersonation banner/controls
intelligence/ 17 files Mission cards/attendance/comms, campaign/faction cards
locker/ 13 files Grid, equipment editor, loadouts, wardrobe
logistics/ 8 files Shipment cards/actions, toast notifications logistics/ 8 files Shipment cards/actions, toast notifications
market/ 10 files Listing cards, negotiation flow, NPC chat, patience meter market/ 10 files Listing cards, negotiation flow, NPC chat, patience meter
notifications/ 2 files Bell (polling), inbox page notifications/ 2 files Bell (polling), inbox page
personnel/ 3 files NpcDirectory, NpcCard, NpcProfile (/personnel/contacts)
profile/ 3 files Profile page components (incl. rating dialog)
projects/ 12 files Jira-like boards, releases, sprints
qualification/ 6 files Aim trainer arena/game/canvas/leaderboard, sounds
radio/ 5 files Radio Traffic game, message generator, leaderboard
realtime/ 1 file GameTickRealtime (SSE -> router.refresh) realtime/ 1 file GameTickRealtime (SSE -> router.refresh)
roster/ 1 file Org chart view roster/ 1 file Org chart view
session/ 1 file Session expiry UI (pairs with useSessionExpired hook)
shims/ 1 file Frontend shim renderer
storage/ 14 files Structure grid, storage dialogs, event ledger storage/ 14 files Structure grid, storage dialogs, event ledger
transfers/ 7 files Assignment transfer request/decision UI
wiki/ 11 files Index, editor + toolbar, content renderer, revisions, standards dialog
+ root files: LandingPage.tsx, SiteHeader.tsx, ClientDate.tsx, PayloadButtons.tsx
``` ```
## Conventions ## Conventions
@ -51,11 +66,13 @@ Market negotiation uses `MakeOfferDialog` nested inside `ListingCard` dialog. Lo
| Task | Path | | Task | Path |
|------|------| |------|------|
| Add a new page | `src/app/(frontend)/<domain>/page.tsx` + create client component here | | Add a new page | `src/app/(frontend)/<domain>/page.tsx` + create client component here |
| Add nav entry | `src/components/frontend/blocks/NavCore.tsx` | | Add nav entry | `src/components/frontend/blocks/sidebarData.ts` (nav data extracted from `AppSidebar`) |
| Modify auth gate | `src/app/(frontend)/layout.tsx` (conditional renders `LandingPage` or shell) | | Modify auth gate | `src/app/(frontend)/layout.tsx` (conditional renders `LandingPage` or shell) |
| Add SSE consumer | `src/hooks/useGameTick.ts` + mount in layout | | Add SSE consumer | `src/hooks/useGameTick.ts` + mount in layout |
| NPC dialogue/chatter | `src/lib/market/npcDialogue.ts` (pure, client-safe import) | | NPC dialogue/chatter | `src/lib/market/npcDialogue.ts` (pure, client-safe import) |
| Minigame game rules | `src/lib/aim-trainer.ts` etc. — pure libs, components consume them |
| Keyboard shortcuts | `src/components/command-palette/` | | Keyboard shortcuts | `src/components/command-palette/` |
| Admin custom fields | `src/components/admin/` (awards designers, CollapsibleGroupField, narrative-flow, shims) |
## Anti-patterns ## Anti-patterns

View file

@ -4,7 +4,7 @@
## Overview ## Overview
23 files across 8 domain subdirectories. Contains pure business logic and Payload-dependent service modules. Server actions in route directories delegate here; these modules hold the actual domain rules. 63 files across 17 domain subdirectories plus root modules. Contains pure business logic and Payload-dependent service modules. Server actions in route directories delegate here; these modules hold the actual domain rules.
## Structure ## Structure
@ -15,39 +15,56 @@ lib/
shipping.ts # Fuel cost, transit time, vehicle effective speed shipping.ts # Fuel cost, transit time, vehicle effective speed
distance.ts # Haversine distance calculation distance.ts # Haversine distance calculation
logistics.ts # Shared logistics helpers logistics.ts # Shared logistics helpers
aim-trainer.ts # Pure aim-trainer game rules (difficulties, scoring, XP)
impersonation.ts # Admin impersonation cookie helpers
redaction.ts # Text redaction helper
versionInfo.ts # Build version display versionInfo.ts # Build version display
arma-bridge/ # Arma server sync: auth, heartbeat/presence, commands, sync events
attendance/ # Mission attendance service (single write path) attendance/ # Mission attendance service (single write path)
banking/ # Transaction engine, account creation, formatting awards/ # Award grant eligibility + diff/notify/cleanup
locker/ # Grid logic, placement validation, loadouts banking/ # Transaction engine, account creation, currency config, formatting
base/ # Staffing (hire/fire/headcount), structure upgrades, storage, base tick
evaluations/ # Evaluation levels + bot reminder plan (reminders.ts)
locker/ # Grid logic, placement validation, loadouts, attachments/skins
logistics/ # Mission lifecycle + mission reminder logic
market/ # Buy/sell, NPC negotiation, dialogue, vendor resolution market/ # Buy/sell, NPC negotiation, dialogue, vendor resolution
notifications/ # User notification helper + muteable types notifications/ # User notification helper + muteable types
realtime/ # In-process SSE bus (single-instance only) projects/ # Project ticket-type metadata (projectMeta.ts)
realtime/ # In-process SSE bus + presence tracking (single-instance only)
session/ # Session token expiry helpers
shims/ # Shim audience/path evaluation (djb2 hash matching)
tickets/ # Ticket vocabulary, Lexical helpers, staff resolution tickets/ # Ticket vocabulary, Lexical helpers, staff resolution
transfers/ # Assignment transfer workflow (request/decide/appeal) + notification types
wiki/ # Slugify, templates, wikilinks, markdown pipeline, page service
``` ```
## Dependency graph ## Dependency graph
``` ```
Server actions ──┬── storageRules.ts Server actions ──┬── storageRules.ts
├── lib/banking/index.ts ──┬── format.ts ├── lib/banking/index.ts ──┬── format.ts (CurrencyConfig)
│ └── ui.ts │ └── ui.ts
├── lib/market/index.ts ──┬── negotiations.ts ├── lib/market/index.ts ──┬── negotiations.ts
│ ├── npcs.ts │ ├── npcs.ts
│ ├── npcDialogue.ts (pure, client-safe) │ ├── npcDialogue.ts (pure, client-safe)
│ └── chatBubbles.ts (pure, client-safe) │ └── chatBubbles.ts (pure, client-safe)
├── lib/base/ (staffing.ts, upgrade.ts, tick.ts, storage.ts)
├── lib/wiki/service.ts + prepare.ts (markdown.tsx is pure, client-safe)
├── lib/locker/index.ts ── search.ts ├── lib/locker/index.ts ── search.ts
├── lib/tickets/, lib/transfers/, lib/awards/
├── lib/attendance/index.ts ├── lib/attendance/index.ts
├── lib/notifications/index.ts ├── lib/evaluations/ (reminders.ts drives the bot's evaluation DMs)
└── lib/tickets/ └── lib/notifications/index.ts
game-tick script ── shipping.ts, distance.ts, storageRules.ts game-tick script ── shipping.ts, distance.ts, storageRules.ts
market-tick script ── lib/market/index.ts (autoPrice, autoQuantity, npc vendor logic) market-tick script ── lib/market/index.ts (autoPrice, autoQuantity, npc vendor logic)
base-tick script ── lib/base/tick.ts (processBaseTick: salaries, maintenance, upkeep)
``` ```
## Pure vs Payload-dependent ## Pure vs Payload-dependent
- **Pure modules** (no Payload import, safe for client + server): `npcDialogue.ts`, `chatBubbles.ts`, `ticketMeta.ts`, `distance.ts`, `storageRules.ts` (logic only) - **Pure modules** (no Payload import, safe for client + server): `npcDialogue.ts`, `chatBubbles.ts`, `ticketMeta.ts`, `distance.ts`, `aim-trainer.ts`, `wiki/markdown.tsx`, `wiki/prepare.ts`, `wiki/templates.ts`, `wiki/wikilinks.ts`, `shims/evaluate.ts`, `storageRules.ts` (logic only)
- **Payload-dependent** (import `@payload-config`, server-only): everything else - **Payload-dependent** (import `@payload-config`, server-only): everything else
## Key modules ## Key modules
@ -56,7 +73,10 @@ market-tick script ── lib/market/index.ts (autoPrice, autoQuantity, npc vend
Enforcement order: **prohibited → whitelist → per-item cap**. Functions: `checkStorageDeposit`, `isResourceProhibited`, `isResourceWhitelisted`, `getResourceStorageCap`, `storageViolationMessage`. Used in structure actions, shipment creation, and arrival processing. Enforcement order: **prohibited → whitelist → per-item cap**. Functions: `checkStorageDeposit`, `isResourceProhibited`, `isResourceWhitelisted`, `getResourceStorageCap`, `storageViolationMessage`. Used in structure actions, shipment creation, and arrival processing.
### `banking/index.ts` ### `banking/index.ts`
`applyTransaction()` — single source of truth for balance math. Validates accounts, creates transaction + ledger entries, updates balances. `ensurePersonalAccount()` — dedup find-or-create. `getMainCurrencyId()` — resolves Game Rules main currency. `applyTransaction()` — single source of truth for balance math. Validates accounts, creates transaction + ledger entries, updates balances. `ensurePersonalAccount()` — dedup find-or-create. `getMainCurrencyId()` — resolves Game Rules main currency. `currencyConfigFromGameRules()` builds the `CurrencyConfig` threaded through banking/market UI.
### `base/`
Staffing/upkeep engine for structures: `staffing.ts` (`hireStaff`/`fireStaff`/`setLaborHeadcount`/`assertStaffHiringEnabled`), `upgrade.ts` (`upgradeStructure` — consumes stored materials, swaps type via `upgradesInto`), `storage.ts` (`consumeStorage`/`storedAmount`), `tick.ts` (`processBaseTick` — salaries, maintenance, upkeep flags).
### `market/index.ts` ### `market/index.ts`
`buyListing()` — validates, debits buyer, credits locker, marks sold. Supports partial buys via `quantity` parameter. `creditLockerQuantity()` — merges stackables or fills empty grid spots; throws if no space. `buyListing()` — validates, debits buyer, credits locker, marks sold. Supports partial buys via `quantity` parameter. `creditLockerQuantity()` — merges stackables or fills empty grid spots; throws if no space.
@ -74,6 +94,8 @@ Module-level subscriber Set. SSE endpoint `GET /api/realtime` subscribes; game t
| Add storage rule logic | `storageRules.ts` (pure functions) | | Add storage rule logic | `storageRules.ts` (pure functions) |
| Modify transaction flow | `lib/banking/index.ts` — `applyTransaction()` | | Modify transaction flow | `lib/banking/index.ts` — `applyTransaction()` |
| Change NPC pricing | `lib/market/negotiations.ts` — NPC_ACCEPT constants | | Change NPC pricing | `lib/market/negotiations.ts` — NPC_ACCEPT constants |
| Add staffing/upkeep logic | `lib/base/` (hire/fire/upgrade/tick) |
| Modify minigame rules | `aim-trainer.ts` (pure) or the game's components dir |
| Add notification type | `lib/notifications/notificationTypes.ts` | | Add notification type | `lib/notifications/notificationTypes.ts` |
| Add pure client+server logic | Verify no Payload import; place in appropriate domain dir | | Add pure client+server logic | Verify no Payload import; place in appropriate domain dir |

View file

@ -10,7 +10,7 @@
``` ```
utils/ utils/
access-control/ 6 files Permission checking, role gates, qualification queries access-control/ 8 files Permission checking, role gates, qualification queries
event-log/ 3 files Event emitter, type constants, formatting event-log/ 3 files Event emitter, type constants, formatting
xp/ 1 file Level resolver xp/ 1 file Level resolver
``` ```
@ -33,8 +33,8 @@ Queries `Profiles.progression.qualifications` for specific qualification strings
### Emitter: `emitGameEvent(payload, { type, message, ... })` ### Emitter: `emitGameEvent(payload, { type, message, ... })`
Fire-and-forget create on `game-event-logs`. Always sets `system: true`. Silent error catch (no throw). Always called AFTER successful mutation in server actions. Fire-and-forget create on `game-event-logs`. Always sets `system: true`. Silent error catch (no throw). Always called AFTER successful mutation in server actions.
### Event types: `EventTypes` constants (95 types across 12 categories) ### Event types: `EventTypes` constants (~105 types across 15+ categories)
Defined in `eventTypes.ts`. Use these constants for TypeScript narrowing on the `type` field. Categories: `mission:*`, `finance:*`, `market:*`, `structure:*`, `logistics:*`, `bank:*`, `notification:*`, `locker:*`, `xp:*`, `ticket:*`, `attendance:*`, `system:*`. Defined in `eventTypes.ts`. Use these constants for TypeScript narrowing on the `type` field. Categories include: `mission:*`, `finance:*`, `market:*`, `structure:*`, `staff:*`, `logistics:*`, `bank:*`, `notification:*`, `locker:*`, `xp:*`, `ticket:*`, `wiki:*`, `minigame:*`, `attendance:*`, `system:*`.
### Display: `formatType(type)` — human-readable label for event types. ### Display: `formatType(type)` — human-readable label for event types.