docs: document the admin tech tree editor
Add a "Tech Tree (admin)" section covering the tech-categories collection, treePosition, the React Flow admin view, the propagating layout, the stability contract (connect/save-positions/auto-arrange), edge visuals, and the integration and e2e test commands. Note the tech tree e2e suite.
This commit is contained in:
parent
5f7fd508a5
commit
affc899da0
1 changed files with 13 additions and 1 deletions
14
AGENTS.md
14
AGENTS.md
|
|
@ -46,7 +46,7 @@ Known pre-existing errors (not yours, don't widen scope to fix them): `src/colle
|
||||||
|
|
||||||
- **Dedicated test database**: all tests run against `<db>_test` (derived from `DATABASE_URI`, or set `TEST_DATABASE_URI`), never the dev database. `tests/test-db.ts` bootstraps it idempotently: ensure database exists, push the schema (drizzle `push`, because the migration chain assumes a schema that push created), truncate every table (migration journals and PostGIS internals survive), seed baselines (`seedRoles` + a Game Rules global with a main currency resource, `tests/seed-baseline.ts`), and repair id sequences (migrations backfill explicit-id rows, which leaves sequences behind and causes `ValidationError: field is invalid: id` on creates). Truncate runs at both global setup (clears crashed-run leftovers) and teardown, so every run starts and ends empty. `TEST_DB_HARD_RESET=1` drops and recreates the test database from scratch instead of truncating. Serialized test files (`fileParallelism: false`) because parallel workers share the one database.
|
- **Dedicated test database**: all tests run against `<db>_test` (derived from `DATABASE_URI`, or set `TEST_DATABASE_URI`), never the dev database. `tests/test-db.ts` bootstraps it idempotently: ensure database exists, push the schema (drizzle `push`, because the migration chain assumes a schema that push created), truncate every table (migration journals and PostGIS internals survive), seed baselines (`seedRoles` + a Game Rules global with a main currency resource, `tests/seed-baseline.ts`), and repair id sequences (migrations backfill explicit-id rows, which leaves sequences behind and causes `ValidationError: field is invalid: id` on creates). Truncate runs at both global setup (clears crashed-run leftovers) and teardown, so every run starts and ends empty. `TEST_DB_HARD_RESET=1` drops and recreates the test database from scratch instead of truncating. Serialized test files (`fileParallelism: false`) because parallel workers share the one database.
|
||||||
- **Integration tests**: `tests/int/**/*.int.spec.ts` — Vitest with jsdom. Requires the live PostgreSQL test database (bootstrapped automatically via `vitest-global.ts`; `vitest.setup.ts` points the process at it). Uses `dotenv/config` via `vitest.setup.ts`.
|
- **Integration tests**: `tests/int/**/*.int.spec.ts` — Vitest with jsdom. Requires the live PostgreSQL test database (bootstrapped automatically via `vitest-global.ts`; `vitest.setup.ts` points the process at it). Uses `dotenv/config` via `vitest.setup.ts`.
|
||||||
- **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. Override the binary with `E2E_BROWSER_PATH` (e.g. Playwright's bundled chromium) when the installed Vivaldi crashes under automation; the default stays Vivaldi. The `webServer` config auto-starts `pnpm dev` with `DATABASE_URI` pointed at the test DB (bootstrapped by `tests/e2e/global-setup.ts`, with `reuseExistingServer: true`). `global-setup` also seeds the shared e2e login: `dev` / `Test123` (developer legacy role + a superuser Roles doc for permission-gated UI + the "J. Fraley" rank so the dashboard heading matches). E2E runs serially (`workers: 1`, one webpack dev server) with generous `timeout`/`expect` timeouts because first navigation to a route triggers a cold webpack compile. Currently: tours smoke (auto-start, dismissal, replay), frontend, operations, reservations, supply boxes, OPORD recall, session banner. Caveat: when your own dev server is already on port 3000, `reuseExistingServer` silently reuses it and its DEV database, bypassing the test-DB wiring; free the port first for a true test-DB run.
|
- **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. Override the binary with `E2E_BROWSER_PATH` (e.g. Playwright's bundled chromium) when the installed Vivaldi crashes under automation; the default stays Vivaldi. The `webServer` config auto-starts `pnpm dev` with `DATABASE_URI` pointed at the test DB (bootstrapped by `tests/e2e/global-setup.ts`, with `reuseExistingServer: true`). `global-setup` also seeds the shared e2e login: `dev` / `Test123` (developer legacy role + a superuser Roles doc for permission-gated UI + the "J. Fraley" rank so the dashboard heading matches). E2E runs serially (`workers: 1`, one webpack dev server) with generous `timeout`/`expect` timeouts because first navigation to a route triggers a cold webpack compile. Currently: tours smoke (auto-start, dismissal, replay), frontend, operations, reservations, supply boxes, OPORD recall, session banner, tech tree. Caveat: when your own dev server is already on port 3000, `reuseExistingServer` silently reuses it and its DEV database, bypassing the test-DB wiring; free the port first for a true test-DB run.
|
||||||
- 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`
|
||||||
|
|
||||||
|
|
@ -303,6 +303,18 @@ Leaflet world map at `/map` (top-level sidebar entry, whole unit views it). Full
|
||||||
- **Dependencies**: `@xyflow/react` (v12), `radix-ui` (context-menu primitives), `lucide-react` (icons).
|
- **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`.
|
- To add a new node type, create the component + register it in the `nodeTypes` object in `NarrativeFlowEditor.tsx`.
|
||||||
|
|
||||||
|
## Tech Tree (admin)
|
||||||
|
|
||||||
|
`/admin/tech-tree` — visual tech tree editor over `technologies`, built on `@xyflow/react` v12 following the narrative-flow patterns. Custom admin view (root view) + a "Tech Tree" nav entry injected under the Intelligence group; the Technologies list/edit views are untouched.
|
||||||
|
|
||||||
|
- **Schema**: `tech-categories` collection (`src/collections/intelligence/TechCategories.ts`: `name` unique, `position` number = timeline order, optional hex `color`) + `Technologies.category` relationship (FK `ON DELETE set null`, so deleting a category just unfiles its techs) + `Technologies.treePosition` group (`x`/`y` numbers; manual canvas position from dragging, empty = automatic). Permissions mirror technologies (`tech-categories:*` + `admin:tech-categories:manage`, registered in `src/permissions/index.ts`); `tech-categories` is in `DIVISION_ADMIN_QUALIFICATIONS`, so Intelligence division members get admin-panel access exactly like `technologies`. Migrations: `20260930_230747_add_tech_categories`, `20261001_000320_add_tech_tree_position`.
|
||||||
|
- **View wiring**: `payload.config.ts` `admin.components.views.techTree` (path `/tech-tree`, component `src/components/admin/tech-tree/TechTreeView.tsx`) and `admin.components.Nav` → `TechTreeNav.tsx`. Custom root views are public by default, so the view gates itself (logged-in + `technologies:read` OR Intelligence qualification). The nav replica injects the entry into the group containing `technologies` and reuses public exports (`NavWrapper`/`NavHamburger`/`DefaultNavClient` from `@payloadcms/next/client`, `groupNavItems` from `@payloadcms/ui/shared`); registered admin components must be **default exports** (importMap convention in this repo).
|
||||||
|
- **Layout** (`layout.ts`, pure): a **propagating left-to-right tree**. A technology WITH prerequisites is placed one column to the right of its parents, at their average row, so linking A -> B drops B immediately beside A and B's dependents continue further right; nothing is teleported to a distant column when its depth changes. Technologies in a category stay pinned to that category's column (the category contract); uncategorized roots fill a catalog grid whose slots come from each tech's rank among ALL uncategorized technologies (`MAX_ROWS_PER_COLUMN` = 10 rows, spilling into more columns) so promoting a tech into the tree leaves an empty slot rather than re-shuffling the grid; uncategorized dependents follow their parents inside the uncategorized zone. Collisions search rows outward from the desired row (dependents may also grow upward, and their columns are not height-capped, so a link from a full column still lands beside its parent rather than far right). Category bands are the bounding box of their members' final positions. Prerequisites placed to the RIGHT of their dependent surface as amber warnings (toolbar chip + node icon + preview panel section).
|
||||||
|
- **Edges**: AND prerequisites solid slate, OR prerequisites dashed cyan with an OR label. Connections are editable on the canvas: drag from a node's right handle onto another node (the target card OR its handle) to add it as an AND prerequisite; the drop-on-card case is resolved by an `onConnectEnd` fallback because React Flow only resolves drops onto a handle. Select an edge and press Backspace to remove the prerequisite. Duplicates, self-references, and cycles (direct or transitive) are rejected by the pure `prereqEdits.ts` helpers with an inline error in the toolbar.
|
||||||
|
- **Interactions**: nodes are draggable; dropping one PATCHes its `treePosition` (persisted, survives reload) and the toolbar shows an "Auto-arrange" button (technologies:update gated) that clears every manual position and restores the ordered layout. A **Save positions** button (technologies:update gated), next to Auto-arrange, commits the current on-screen position of every node to `treePosition` in one pass (matching what React Flow rendered, not just dragged/pinned cards) so the exact arrangement survives a reload; Auto-arrange then clears them. **Connecting does not move anything**: if the ordered layout would relocate the connected technology, the canvas pins it to its current spot (`treePosition` written in the same PATCH as the prerequisite) so the edge appears without the card jumping; the catalog grid also uses stable slots (each tech's rank among all uncategorized technologies, so a tech promoted into the tree leaves its slot empty instead of re-shuffling every other root). Pressing "Auto-arrange" clears those pins and moves everything, including the connected node, to its ordered position. Node click opens the side panel, which is a **lite editor** over the fields it shows (name, type, approval status, summary, category with inline create, prerequisites with AND/OR grouping and add/remove, research costs with resource rows and min/max durations): dirty tracking, Save/Reset, and an "Edit in full view" jump to the standard admin edit route. Final approval states stay superuser-only (`canSetFinalApproval` = `system:admin-access`), mirroring the field access. Quick-create dialog (name, type, summary, category, research durations) creates via REST; the category picker is a native input + suggestion list (existing values selectable, new values creatable inline) because Radix Popover inside the admin Dialog proved layer-unstable. New categories get `position = max + 1`. The canvas uses React Flow's `<Background variant="lines">` so the grid scales with zoom (a CSS-background grid does not, and looked broken at low zoom), and `fitView` is capped at `maxZoom: 0.85`. Panel and toolbar controls carry dark tactical chrome (the admin panel's light shadcn defaults would render as white slabs on the dark surface; bare `<button>` elements in the admin additionally inherit a global light background AND border, so list rows need explicit `bg-transparent` + `border-0`), and the panel's dropdowns use the in-house `PanelSelect` listbox rather than native `<select>`: a native popup is browser/OS chrome that ignores dark styling (only `color-scheme` influences it) and cannot be screenshotted, whereas `PanelSelect` renders its list through a portal as a fixed-position overlay above the panel (so it never pushes the panel's content down), flips upward when there is more room above, closes on scroll/resize so it cannot detach from its trigger, and offers a search field for long lists. Both it and the panel's category picker use a mono header strip, hairline row separators, a cyan left accent on the selected row, and the `.tech-tree-scroll` dark scrollbar rules from `custom.scss`.
|
||||||
|
- **Styling**: tactical dossier (dark `#05070a` canvas + 28px grid via inline styles, squared corners, classification strips, mono readouts); shadcn form controls in the dialog stay conventional. React Flow controls/attribution are darkened via `.tech-tree-canvas` rules in `src/app/(payload)/custom.scss`.
|
||||||
|
- **Gotchas**: React Flow in controlled mode needs `useNodesState` AND `useEdgesState` with change handlers that forward everything except `remove` (a no-op edge handler silently breaks edge selection, and a passed-through remove lets Backspace delete nodes that only exist in the data layer); category band nodes must be `pointerEvents: "none"` or they swallow edge/pane clicks; node handles must live OUTSIDE the card's `overflow-hidden` box or they become un-draggable; a second `next dev` for the same directory is impossible (Next 16 lock) so e2e verification either frees port 3000 or uses a copied repo (whose file watcher is unreliable: restart the copied server after every source change). technologies create requires `approvalStatus` + `researchCosts.minimumResearchDuration` (the field's `defaultValue: 0` violates its own `min: 1`, so creates must pass it explicitly); clearing `treePosition` means sending `{ x: null, y: null }` (a group cannot be nulled wholesale). The e2e seeds via cookie-authed REST (`page.request`) after frontend login, so it MUST run against the test DB: a dev server left on port 3000 is silently reused (`reuseExistingServer: true`) and its DEV database 403s every seed (the dev user has no create permission). Free the port first. Tests: `tests/int/tech-tree-layout.int.spec.ts` (pure layout: wrapping, overrides, band bboxes, warnings, cycles), `tests/int/tech-tree-prereq-edits.int.spec.ts` (pure add/remove/switch with cycle rejection), `tests/int/tech-categories.int.spec.ts` (CRUD, RBAC, division gating, FK set-null, position round-trip) + `tests/e2e/tech-tree.e2e.spec.ts` (nav, render, panel, edit nav, both quick-create paths, drag persistence + Auto-arrange, save positions + reload survival, connect/disconnect + lite-editor saves, wrapping at scale). Vivaldi crashes under automation in this suite; run with `E2E_BROWSER_PATH=<playwright chromium>`.
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
|
|
||||||
`src/collections/wiki/`: `wiki-pages`, `wiki-revisions`, `wiki-templates`. A user-maintained field guide for campaigns, characters, places, and the stories around them. Admin group: **Wiki**.
|
`src/collections/wiki/`: `wiki-pages`, `wiki-revisions`, `wiki-templates`. A user-maintained field guide for campaigns, characters, places, and the stories around them. Admin group: **Wiki**.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue