# Per-page & per-section access grants — build plan

*Planned 2026-08-12. Goal: Erin (or any admin) grants any user — regardless of role — edit
access to individual pages/posts, or full access to a whole post type (careers for HR,
courses for a department head), through a clear UI. Replaces the author-reassignment
workaround, which caps a page at one delegate and requires a role change.*

## What already exists (verified)

- `inc/bw-roles.php` re-registers the six Brentwood CPTs (`staff, landing, course,
  livestream, bw_hundred, bw_career`) under a **shared** `bw_content` capability type via
  `register_post_type_args` (line ~284), `map_meta_cap => true`. Versioned rollout via
  `BW_ROLES_VERSION` (currently 8) already grants the family to admin/editor/site-editor.
- Nothing outside `bw-roles.php` references `bw_content*` caps (grepped theme + bw-dev).
- `bw-staff-access.php` bounces bw_staff-only users out of wp-admin to `/my-profile` and
  hides the admin bar. `/my-profile` authorises by ownership, not caps (per its docblock)
  — so cap changes don't touch it, but the bounce must be lifted for grant-holders.
- 148 users are bw_staff (`read` only). Erin is administrator; Tracy editor.

## Phase 1 — split the shared family into per-type families

In the existing filter, replace the shared `bw_content` pair with per-type pairs:
`staff→(bw_staff, bw_staffs)`, `landing→(bw_landing, bw_landings)`, `course→(bw_course,
bw_courses)`, `livestream→(bw_livestream, bw_livestreams)`, `bw_hundred→(bw_hundred,
bw_hundreds)`, `bw_career→(bw_career, bw_careers)`.

(Cap/role slug note: only compound primitives are ever stored — `edit_bw_staffs` etc. —
so no collision with the `bw_staff` *role* slug.)

Same deploy, `BW_ROLES_VERSION → 9`:
- administrator + editor: full family × 6 (replaces `bw_contents` grants; remove old caps).
- bw_site_editor: own-content subset × 6 (mirror its current pattern: no `_others_`).
- bw_blog_manager: keep parity with whatever it holds today (check in build).
- **Must land as one atomic change** or the CPTs vanish from admin menus.
- Taxonomy edge: course taxonomies' `assign_terms` defaults to literal `edit_posts` —
  after the split a courses-only grantee can't assign terms. Set taxonomy caps (assign →
  `edit_bw_courses`) in the same filter pass.

## Phase 2 — grants engine (`inc/bw-access.php`, new)

Two grant kinds, two storage mechanisms, both standard WP:

1. **Per-post grants** — post meta `bw_editors` (array of user IDs).
   - `map_meta_cap`: `edit_post` / `read_post` / `publish_post` on a granted post →
     `['read']`. **Never** `delete_post` — a per-post grant is edit-only by design.
   - `user_has_cap`: dynamically add the type's list/edit primitive + `upload_files`
     while the user holds ≥1 grant of that type (makes the admin menu + list screen +
     Gutenberg/REST work at all for cap-less roles like bw_staff).
   - Gutenberg publish edge: updating a *published* post sends `status: publish`, which
     core checks against the type's `publish_posts` primitive (no post context). Handle
     in `user_has_cap` with REST-request context (route → post ID → granted?). Classic
     editor path is already covered by the `publish_post` meta cap above.
   - Block "Add New" for grant-only users: same literal cap gates list and create, so
     deny by context (`post-new.php` / REST collection POST). Graceful degradation if a
     path is missed: they could only ever save drafts — harmless.
   - `pre_get_posts` (admin list): users whose access comes only from grants see only
     their granted items. Hide the status view links for them (cosmetic).
2. **Per-type grants** — native `WP_User::add_cap()` of the full family (incl. create +
   delete — rian's spec: HR gets full careers lifecycle). Pages = core `*_pages` family;
   blog posts = core `*_posts` family (clean again after Phase 1). No custom storage:
   the grant IS the capability, visible to all standard tooling.

Integration: in `bw-staff-access.php`, exempt users with ≥1 grant (either kind) from the
wp-admin bounce and give them the admin bar back (its "Edit Page" link becomes their
discovery path: browse to the page → Edit Page). Login still lands on /my-profile.

## Phase 3 — UI

1. **Per-post**: ACF user field (multi, return id) "Who can edit this page" in the
   sidebar of all grantable types (page, post + the six CPTs). Location rule limits it
   to administrator + editor. Zero custom JS; matches how the site already does meta.
2. **Site Access screen** (Users → Site Access, admin-only, server-rendered):
   - Pick a user → checkboxes for whole-type access (Pages, Blog posts, Careers,
     Courses, Staff, Livestreams, Landing pages, Brentwood 100) → save = add/remove cap
     families.
   - The same screen lists the user's per-page grants (links to each page).
   - An audit table: every user holding any grant, what kind, linked. This is the view
     rian verifies against his list.
3. **Guide on the hub** afterwards (write-guides skill): "Giving someone edit access" —
   for Erin post-handover. Text goes to the hub, never the client copy (synced mirror).

## Explicitly NOT in v1

Category-scoped blog access ("all posts in category X"), front-end editing, per-post
grants that include delete/trash, approval workflows. Say so to Erin so expectations
are set.

## Release path (respects the content-on-dev policy)

1. Build + test on mosiah staging (`brentwooddev.demoing.info`) with a throwaway bw_staff
   user.
2. **Targeted rsync of `themes/kadence-child/` to the droplet** (same pattern as the
   bw-lead-ai 1.4.1 update) + chown 33:33. NO full push: dev DB/content untouched, GF
   stays 2.10.5 on dev, nothing overwritten. `BW_ROLES_VERSION` self-applies on next load.
3. Configure grants ON DEV from rian's list (UI or wp-cli over ssh) — users live there.
4. ⚠️ Operational consequence to flag: grants live in dev's DB (usermeta + postmeta), so
   a future full `push-to-dev` destroys them along with content. The push script needs a
   code-only mode soon (out of scope here, but now sharper).

## Supersedes / relates

- The planned **HR role is superseded**: HR = any user + Careers type grant.
- The **Admissions role** (BW Lead AI access) is separate and still blocked on the plugin
  swapping `manage_options` for its own caps (request pending with the BW Lead AI session).

## Test matrix (staging)

1. bw_staff user + one page grant: sees Pages menu with only that page; edits + updates
   the published page in Gutenberg; uploads an image; CANNOT Add New, trash, or open any
   other page; /my-profile still works; admin bar shows Edit Page on the granted page.
2. Same user + Careers type grant: full careers lifecycle (create, publish, trash);
   nothing else gained.
3. Editor regression: still edits everything across all types after the split.
4. bw_site_editor regression: own-content lifecycle across all types.
5. bw_staff (no grants) regression: still bounced to /my-profile, no admin bar.
6. Post Types Order drag-ordering still works for editors (bw_hundred, bw_career).
7. Guides remain read-only; Access screen CRUD + audit table correct.

## Estimate

A focused build day: Phase 1 ~1.5h · Phase 2 ~3h · Phase 3 ~2.5h · testing ~1.5h, then
same-day release + grant configuration from the list.
