From c580eab7ee01ca16b65dec7f7e13c98ba276652c Mon Sep 17 00:00:00 2001 From: wahyuwidharto Date: Tue, 14 Jul 2026 17:00:23 +0700 Subject: [PATCH] docs(readme): restructure for public-repo readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure restructuring pass — every sentence survives; only headings, positions, and the intro bullets change: - Add a table of contents after the intro. - Move the M4/M5.A/M5.B/M5.C milestone paragraphs out of Quick start into a dedicated 'Feature deep-dives' section (bold leads become headings; prose verbatim). - Promote the golden flow to its own 'Try the full flow' H2 so Quick start ends at the smoke check. - Promote the admin-authorization blockquote to an 'Admin authorization' subsection under Architecture. - Rename 'Status' to 'Documentation' and move the sources-of-truth table below the visitor-facing sections (Quick start, Deploy, Game integration). - Update 'What's in the box' to match shipped scope: distribution models two -> three (ADT), plus bullets for window enforcement, auto-approve, and the admin detail-page shell + announcements, each linking to its deep-dive. Co-Authored-By: Claude Fable 5 --- README.md | 117 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 6a63805..e75910c 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,29 @@ Built for indie and mid-size studios that already use AGS and need tenant-isolat ![playtesthub end-to-end walkthrough — admin creates a playtest, player signs up, admin approves, code is granted, player submits a survey response, admin reviews the audit log](docs/images/walkthrough.gif) +- [What's in the box](#whats-in-the-box) +- [Quick start](#quick-start) +- [Try the full flow](#try-the-full-flow) +- [Feature deep-dives](#feature-deep-dives) +- [Architecture at a glance](#architecture-at-a-glance) +- [Deploy](#deploy) +- [Integrating with your game](#integrating-with-your-game) +- [Documentation](#documentation) +- [Development workflow](#development-workflow) +- [Contributing](#contributing) +- [License](#license) + ## What's in the box -- **Two distribution models** per playtest — `STEAM_KEYS` (CSV passthrough, manual Steam redemption) and `AGS_CAMPAIGN` (in-game redemption via the AGS Platform Campaign API). One internal code pool and state machine for both. +- **Three distribution models** per playtest — `STEAM_KEYS` (CSV passthrough, manual Steam redemption), `AGS_CAMPAIGN` (in-game redemption via the AGS Platform Campaign API), and `ADT` (AccelByte Development Toolkit build delivery via download URL — see [ADT distribution](#adt-distribution-m5b)). One internal code pool and state machine for the two code-based models. - **Discord-federated player identity** via AGS IAM's platform-token grant. Players sign in with Discord; the backend receives a real AGS user. - **NDA versioning with forced re-acceptance** — edit the NDA mid-playtest and approved players must re-accept before submitting a survey response. - **Discord DM delivery** of granted codes — FIFO worker queue with circuit breaker, manual retry, and restart-sweep semantics. Approval succeeds even if the DM fails; the code is also visible in the player UI. Requires a Discord server the bot and applicants both join (Discord blocks bot DMs without a mutual guild) — see [`docs/runbooks/setup-ags-discord.md` § 7 "Discord bot + server"](docs/runbooks/setup-ags-discord.md#7-discord-bot--server-required-for-dm-delivery). +- **Playtest window enforcement** — a background worker auto-transitions `DRAFT → OPEN` at `startsAt` and `OPEN → CLOSED` at `endsAt`; see [Window enforcement](#window-enforcement-m4). +- **Auto-approve** — cap-bounded instant approval of the first N signups straight from the signup handler, skipping manual triage; see [Auto-approve](#auto-approve-m5a). - **Versioned typed surveys** (text, 1–5 rating, multi-choice) with per-version response splits. - **Per-action audit log** for every admin mutation, stable JSONB shapes. +- **Admin detail-page shell + Discord bulk announcements** — list + detail-page-with-tabs admin UI and admin-authored broadcast DMs; see [UX revamp](#ux-revamp-m5c). - **TDD-first** — unit, integration (testcontainers Postgres), e2e golden flow, and smoke harness (`pth` CLI). CI enforces every gate on every PR. ```mermaid @@ -36,24 +51,6 @@ flowchart LR SV <--> AGS ``` -> **Admin authorization.** Every admin RPC is gated on the built-in AGS IAM permission `ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI` (the AppUI-admin perm) at a per-RPC action bit (CREATE / READ / UPDATE / DELETE) — held by namespace-admin roles like **Game Admin** and **Studio Admin** that studios already assign to admin staff. Studios authorize playtest admins by assigning one of those roles in the AGS Admin Portal; **no custom role creation required**, which is what makes this work on Shared Cloud (game admins cannot assign `CUSTOM:*` perms there). The `AuditLog` provides per-action attribution. See [PRD §6 AuthZ](docs/PRD.md#security) and [PRD §9 R8](docs/PRD.md). - -## Status - -**v1.0.0 shipped (MIT).** Track progress in [`docs/STATUS.md`](docs/STATUS.md). Sources of truth, in order: - -| Doc | What it owns | -| --- | --- | -| [`docs/PRD.md`](docs/PRD.md) | Behavior. Authoritative if anything else disagrees. | -| [`docs/schema.md`](docs/schema.md) | DB schemas, audit-log enum + JSONB shapes, fenced-finalize SQL. | -| [`docs/errors.md`](docs/errors.md) | Byte-exact gRPC error codes / messages. | -| [`docs/architecture.md`](docs/architecture.md) | Stack + external dependency detail. | -| [`docs/engineering.md`](docs/engineering.md) | Repo layout, test strategy, TDD workflow, CI gates. | -| [`docs/cli.md`](docs/cli.md) | `pth` CLI spec — surface for humans + AI to drive the app end-to-end. | -| [`docs/dm-queue.md`](docs/dm-queue.md) | DM worker FIFO, circuit breaker, restart sweep. | -| [`docs/ags-failure-modes.md`](docs/ags-failure-modes.md) | AGS retry policy, cleanup matrix, M2 sub-cap rules. | -| [`docs/game-integration.md`](docs/game-integration.md) | Bridging playtesthub's Discord-headless AGS user to the game's Steam-headless AGS user. | - ## Quick start ### Prerequisites @@ -80,7 +77,7 @@ Smoke check: ./scripts/smoke/boot.sh # ephemeral PG + backend boot + reflection probe ``` -### Reproduce the golden flow +## Try the full flow The `pth` CLI is the canonical end-to-end harness — same surface a human or AI uses to drive the system, and the same path the e2e test exercises. The composite command **`pth flow golden-m3`** runs the full M3 golden flow (admin creates an NDA-required STEAM_KEYS playtest → publishes → player signs up → accepts the NDA → admin uploads keys → admin approves → player retrieves the granted code → admin authors a survey → player submits a response → admin lists responses) and emits one NDJSON line per step. @@ -134,13 +131,23 @@ Tear down: The tests under [`e2e/golden_m1_test.go`](e2e/golden_m1_test.go), [`e2e/golden_m2_test.go`](e2e/golden_m2_test.go), [`e2e/golden_m3_test.go`](e2e/golden_m3_test.go), and [`e2e/golden_m4_test.go`](e2e/golden_m4_test.go) wrap the same sequences behind `go test ./e2e/...` for CI / operator verification — see [`docs/cli.md` §7.4](docs/cli.md). -**Window enforcement (M4)** — `Playtest.startsAt` / `Playtest.endsAt` are no longer display-only. A background `internal/window/` worker auto-transitions `DRAFT → OPEN` at `startsAt` and `OPEN → CLOSED` at `endsAt` (PRD §5.1, [`docs/STATUS_M4.md`](docs/STATUS_M4.md)). `pth flow golden-m4 --slug e2e-m4 --admin-profile admin` exercises the path: create with a window, await the auto-open + auto-close, assert two system-attributed `playtest.status_transition` audit rows. +## Feature deep-dives + +### Window enforcement (M4) + +`Playtest.startsAt` / `Playtest.endsAt` are no longer display-only. A background `internal/window/` worker auto-transitions `DRAFT → OPEN` at `startsAt` and `OPEN → CLOSED` at `endsAt` (PRD §5.1, [`docs/STATUS_M4.md`](docs/STATUS_M4.md)). `pth flow golden-m4 --slug e2e-m4 --admin-profile admin` exercises the path: create with a window, await the auto-open + auto-close, assert two system-attributed `playtest.status_transition` audit rows. + +### Auto-approve (M5.A) + +Set `autoApprove=true` + `autoApproveLimit=N` (1..100,000) on a playtest and the first `N` signups land `APPROVED` straight from the signup handler, skipping the manual triage queue (PRD §5.4, [`docs/STATUS_M5.md`](docs/STATUS_M5.md)). Distribution-model-agnostic — works for STEAM_KEYS, AGS_CAMPAIGN, and ADT (Track B). The cap bounds **auto-approvals only**; manual `ApproveApplicant` against PENDING applicants stays uncapped. Pool-empty during a burst is a silent PENDING fallback — the signup still succeeds and the operator restocks (or manually approves) at leisure; no applicant-visible error. `pth flow golden-m2 --slug "auto-$(date +%s)" --admin-profile admin --player-profile "player-$USER_ID" --auto-approve --auto-approve-limit 5` runs the variant: same seven NDJSON lines, but `upload-codes` is hoisted before `signup` (auto-approve consumes from the pool inside the signup tx) and the manual `approve` step is replaced by `assert-applicant-auto-approved`, which calls `ListApplicants` and pins `status=APPROVED` + `auto_approved=true` on the just-signed-up row. -**Auto-approve (M5.A)** — set `autoApprove=true` + `autoApproveLimit=N` (1..100,000) on a playtest and the first `N` signups land `APPROVED` straight from the signup handler, skipping the manual triage queue (PRD §5.4, [`docs/STATUS_M5.md`](docs/STATUS_M5.md)). Distribution-model-agnostic — works for STEAM_KEYS, AGS_CAMPAIGN, and ADT (Track B). The cap bounds **auto-approvals only**; manual `ApproveApplicant` against PENDING applicants stays uncapped. Pool-empty during a burst is a silent PENDING fallback — the signup still succeeds and the operator restocks (or manually approves) at leisure; no applicant-visible error. `pth flow golden-m2 --slug "auto-$(date +%s)" --admin-profile admin --player-profile "player-$USER_ID" --auto-approve --auto-approve-limit 5` runs the variant: same seven NDJSON lines, but `upload-codes` is hoisted before `signup` (auto-approve consumes from the pool inside the signup tx) and the manual `approve` step is replaced by `assert-applicant-auto-approved`, which calls `ListApplicants` and pins `status=APPROVED` + `auto_approved=true` on the just-signed-up row. +### ADT distribution (M5.B) -**ADT distribution (M5.B)** — a third `distributionModel` (`ADT`) that ships an AccelByte Development Toolkit build via a download URL instead of a redemption code (PRD §4.8, [`docs/STATUS_M5.md`](docs/STATUS_M5.md), runbook in [`docs/runbooks/adt-linking.md`](docs/runbooks/adt-linking.md)). One-time per studio: an admin links the studio's ADT namespace via the **Link new ADT Namespace** button on the Playtests list page (state-bearing redirect to ADT — no credential is exchanged; subsequent ADT API calls authenticate via playtesthub's AGS service IAM JWT). Each playtest then picks an ADT namespace + game + build at create time. Approve resolves a download URL via `adt.Client.IssueDownloadURL` (per-build per the 2026-05-20 ADT spec — fixed 24h CDN TTL) and falls back to the playtest's static `adtFallbackDownloadUrl` when ADT is unavailable; the DM body embeds the URL. The player UI's Pending page renders a download card instead of a code panel for ADT playtests, backed by the new `GetADTDownloadInfo` RPC. CLI surface: `pth adt linkage {list,start,complete,unlink}` + `pth adt build list` + `pth flow golden-m5 --slug … --dry-run` for the 11-step request-shape catalogue. +A third `distributionModel` (`ADT`) that ships an AccelByte Development Toolkit build via a download URL instead of a redemption code (PRD §4.8, [`docs/STATUS_M5.md`](docs/STATUS_M5.md), runbook in [`docs/runbooks/adt-linking.md`](docs/runbooks/adt-linking.md)). One-time per studio: an admin links the studio's ADT namespace via the **Link new ADT Namespace** button on the Playtests list page (state-bearing redirect to ADT — no credential is exchanged; subsequent ADT API calls authenticate via playtesthub's AGS service IAM JWT). Each playtest then picks an ADT namespace + game + build at create time. Approve resolves a download URL via `adt.Client.IssueDownloadURL` (per-build per the 2026-05-20 ADT spec — fixed 24h CDN TTL) and falls back to the playtest's static `adtFallbackDownloadUrl` when ADT is unavailable; the DM body embeds the URL. The player UI's Pending page renders a download card instead of a code panel for ADT playtests, backed by the new `GetADTDownloadInfo` RPC. CLI surface: `pth adt linkage {list,start,complete,unlink}` + `pth adt build list` + `pth flow golden-m5 --slug … --dry-run` for the 11-step request-shape catalogue. -**UX revamp (M5.C)** — the admin shell shifts from list+modal to a list + detail-page-with-tabs layout (PRD §5.7 M5.C restructure + [`docs/STATUS_M5.md`](docs/STATUS_M5.md), tour in [`docs/runbooks/admin-shell-tour.md`](docs/runbooks/admin-shell-tour.md)). Clicking a row's **View** button navigates to `/playtest/` whose header carries the breadcrumb + title + date range + status pill + **Publish** / **Stop Playtest** verbs (pure copy renames over M4's existing state machine — no PRD §5.1 change). Below sit four tabs: *Playtest Info* (read-only summary + Edit), *Distribution* (per-model rendering with shared empty-state scaffold), *Participants* (6-column table with Code Sent Date derived from `applicant.last_dm_attempt_at`; the four ADT telemetry cache columns ship dormant for M6), and *Discord Bot Tools* (admin-authored bulk DM broadcast — subject + message are PII-sensitive and never logged; runbook in [`docs/runbooks/announcement-broadcast.md`](docs/runbooks/announcement-broadcast.md)). CLI surface: `pth announcement {create,list}` ships the broadcast tooling outside the admin UI. +### UX revamp (M5.C) + +The admin shell shifts from list+modal to a list + detail-page-with-tabs layout (PRD §5.7 M5.C restructure + [`docs/STATUS_M5.md`](docs/STATUS_M5.md), tour in [`docs/runbooks/admin-shell-tour.md`](docs/runbooks/admin-shell-tour.md)). Clicking a row's **View** button navigates to `/playtest/` whose header carries the breadcrumb + title + date range + status pill + **Publish** / **Stop Playtest** verbs (pure copy renames over M4's existing state machine — no PRD §5.1 change). Below sit four tabs: *Playtest Info* (read-only summary + Edit), *Distribution* (per-model rendering with shared empty-state scaffold), *Participants* (6-column table with Code Sent Date derived from `applicant.last_dm_attempt_at`; the four ADT telemetry cache columns ship dormant for M6), and *Discord Bot Tools* (admin-authored bulk DM broadcast — subject + message are PII-sensitive and never logged; runbook in [`docs/runbooks/announcement-broadcast.md`](docs/runbooks/announcement-broadcast.md)). CLI surface: `pth announcement {create,list}` ships the broadcast tooling outside the admin UI. ## Architecture at a glance @@ -151,6 +158,47 @@ The tests under [`e2e/golden_m1_test.go`](e2e/golden_m1_test.go), [`e2e/golden_m Repo layout is documented in [`docs/engineering.md` §2](docs/engineering.md#2-repo-layout). +### Admin authorization + +Every admin RPC is gated on the built-in AGS IAM permission `ADMIN:NAMESPACE:{namespace}:EXTEND:APPUI` (the AppUI-admin perm) at a per-RPC action bit (CREATE / READ / UPDATE / DELETE) — held by namespace-admin roles like **Game Admin** and **Studio Admin** that studios already assign to admin staff. Studios authorize playtest admins by assigning one of those roles in the AGS Admin Portal; **no custom role creation required**, which is what makes this work on Shared Cloud (game admins cannot assign `CUSTOM:*` perms there). The `AuditLog` provides per-action attribution. See [PRD §6 AuthZ](docs/PRD.md#security) and [PRD §9 R8](docs/PRD.md). + +## Deploy + +Three deployable surfaces. Each has its own host and its own runbook. + +1. **Backend (Extend Service Extension)** — Go binary + Postgres on AGS Extend. + 1. Create the Extend Service Extension app in the AGS Admin Portal. Set the env vars and secrets per [PRD §5.9](docs/PRD.md#59-runtime-configuration-go-backend) — including `CORS_ALLOWED_ORIGINS` if the player will be hosted off-origin. + 2. Build + push with [`extend-helper-cli`](https://github.com/AccelByte/extend-helper-cli): + ```bash + extend-helper-cli image-upload --login \ + --namespace --app --image-tag v0.0.1 + ``` + 3. Deploy the pushed image from **App Detail → Image Version History → Deploy**, or via `extend-helper-cli deploy-app --wait`. +2. **Player frontend (Svelte → GitHub Pages)** — static bundle, hash-routed, Discord-federated. Auto-deploys on push to `main` via [`.github/workflows/pages.yml`](.github/workflows/pages.yml). Setup is one-time per fork — enable Pages with the workflow build source, set three repo Variables, allowlist the Pages origin in the backend's `CORS_ALLOWED_ORIGINS`, register the Pages callback URL with Discord + AGS. Walk-through in [`docs/runbooks/deploy-player-pages.md`](docs/runbooks/deploy-player-pages.md). Vercel + custom-domain variants are noted in that runbook's § Out of scope. +3. **Admin UI (Extend App UI)** — React Module Federation remote hosted by AccelByte. `extend-helper-cli appui create` + `appui upload` (Internal Shared Cloud only today — see [`docs/engineering.md` §8](docs/engineering.md#8-temporary-ags-platform-workarounds)). + +For first-time AGS + Discord setup (IAM client, platform credential, redirect URIs), follow [`docs/runbooks/setup-ags-discord.md`](docs/runbooks/setup-ags-discord.md) before any of the above. + +## Integrating with your game + +playtesthub identifies players by their **Discord-federated** AGS user; the game probably identifies them by their **Steam-federated** AGS user. AGS IAM treats those as two separate headless accounts, so the same human ends up with two different AGS userIds unless the integration explicitly bridges them. [`docs/game-integration.md`](docs/game-integration.md) covers the four patterns a game team can pick from — recommended path is a one-time Discord-OAuth gate on first launch that links Steam onto the playtesthub-side account. + +## Documentation + +**v1.0.0 shipped (MIT).** Track progress in [`docs/STATUS.md`](docs/STATUS.md). Sources of truth, in order: + +| Doc | What it owns | +| --- | --- | +| [`docs/PRD.md`](docs/PRD.md) | Behavior. Authoritative if anything else disagrees. | +| [`docs/schema.md`](docs/schema.md) | DB schemas, audit-log enum + JSONB shapes, fenced-finalize SQL. | +| [`docs/errors.md`](docs/errors.md) | Byte-exact gRPC error codes / messages. | +| [`docs/architecture.md`](docs/architecture.md) | Stack + external dependency detail. | +| [`docs/engineering.md`](docs/engineering.md) | Repo layout, test strategy, TDD workflow, CI gates. | +| [`docs/cli.md`](docs/cli.md) | `pth` CLI spec — surface for humans + AI to drive the app end-to-end. | +| [`docs/dm-queue.md`](docs/dm-queue.md) | DM worker FIFO, circuit breaker, restart sweep. | +| [`docs/ags-failure-modes.md`](docs/ags-failure-modes.md) | AGS retry policy, cleanup matrix, M2 sub-cap rules. | +| [`docs/game-integration.md`](docs/game-integration.md) | Bridging playtesthub's Discord-headless AGS user to the game's Steam-headless AGS user. | + ## Development workflow This repo is **TDD-first**. Every production change follows red → green → refactor: @@ -178,27 +226,6 @@ buf lint CI runs the same gates on every PR — see [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Browser-based a11y (`@axe-core/playwright` per [`docs/engineering.md` §5](docs/engineering.md#5-ci-gates)) is tracked under STATUS phase 12.1. -## Deploy - -Three deployable surfaces. Each has its own host and its own runbook. - -1. **Backend (Extend Service Extension)** — Go binary + Postgres on AGS Extend. - 1. Create the Extend Service Extension app in the AGS Admin Portal. Set the env vars and secrets per [PRD §5.9](docs/PRD.md#59-runtime-configuration-go-backend) — including `CORS_ALLOWED_ORIGINS` if the player will be hosted off-origin. - 2. Build + push with [`extend-helper-cli`](https://github.com/AccelByte/extend-helper-cli): - ```bash - extend-helper-cli image-upload --login \ - --namespace --app --image-tag v0.0.1 - ``` - 3. Deploy the pushed image from **App Detail → Image Version History → Deploy**, or via `extend-helper-cli deploy-app --wait`. -2. **Player frontend (Svelte → GitHub Pages)** — static bundle, hash-routed, Discord-federated. Auto-deploys on push to `main` via [`.github/workflows/pages.yml`](.github/workflows/pages.yml). Setup is one-time per fork — enable Pages with the workflow build source, set three repo Variables, allowlist the Pages origin in the backend's `CORS_ALLOWED_ORIGINS`, register the Pages callback URL with Discord + AGS. Walk-through in [`docs/runbooks/deploy-player-pages.md`](docs/runbooks/deploy-player-pages.md). Vercel + custom-domain variants are noted in that runbook's § Out of scope. -3. **Admin UI (Extend App UI)** — React Module Federation remote hosted by AccelByte. `extend-helper-cli appui create` + `appui upload` (Internal Shared Cloud only today — see [`docs/engineering.md` §8](docs/engineering.md#8-temporary-ags-platform-workarounds)). - -For first-time AGS + Discord setup (IAM client, platform credential, redirect URIs), follow [`docs/runbooks/setup-ags-discord.md`](docs/runbooks/setup-ags-discord.md) before any of the above. - -## Integrating with your game - -playtesthub identifies players by their **Discord-federated** AGS user; the game probably identifies them by their **Steam-federated** AGS user. AGS IAM treats those as two separate headless accounts, so the same human ends up with two different AGS userIds unless the integration explicitly bridges them. [`docs/game-integration.md`](docs/game-integration.md) covers the four patterns a game team can pick from — recommended path is a one-time Discord-OAuth gate on first launch that links Steam onto the playtesthub-side account. - ## Contributing Issues and PRs welcome. Before opening one: