Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup

## Summary

Updated Game Hub parent table and save behavior for the ALFA_017 stack item.

## Implementation

- Guest Add/Edit rows remain reachable for browsing, but Add and Edit Save buttons redirect to `account/sign-in.html`.
- Renamed the current role side control area to a `Game Crew` accordion.
- Removed Owner from displayed parent table headers, parent rows, add rows, edit rows, and expanded row colspan.
- Kept parent game rows with Source Idea and Readiness Output child rows/tables.
- Removed the instructional copy from the center panel.
- Matched parent table action buttons to compact game button sizing.

## Scope Control

- Preserved existing API/service contract.
- Did not add browser-owned product data.
- Did not add readiness math.
- Did not modify table-first governance content.

## ZIP

- `tmp/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_delta.zip`
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Branch validation: PASS

Branch:
pr/26174-ALFA-017-game-hub-guest-save-and-crew-cleanup

Base stack branch:
pr/26174-ALFA-016-game-hub-row-edit-add-selected-state

Checks:
- Current branch is the ALFA_017 branch: PASS
- Worktree was clean before ALFA_017 edits: PASS
- Scope limited to Game Hub page/script, targeted Playwright coverage, and required reports: PASS
- No protected Project Instructions changes: PASS
- No merge to main performed: PASS
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Manual validation notes: PASS

- Reviewed `docs_build/dev/ProjectInstructions/addendums/table_first_ui.md` and applied the game-row parent table pattern.
- Confirmed the Game Hub center panel no longer includes the removed instructional copy.
- Confirmed parent table headers display only Game, Purpose, Status, and Actions.
- Confirmed parent rows no longer display Owner while keeping owner fields available to existing repository data.
- Confirmed Source Idea and Readiness Output remain expanded child rows/tables under each game parent row.
- Confirmed guest Add/Edit Save controls redirect to `account/sign-in.html`.
- Confirmed Add, Edit, Save, and Cancel actions use compact button sizing consistent with game buttons.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Requirement checklist: PASS

- As a guest, clicking any Save button redirects to account/sign-in.html: PASS
- Move/rename current user role accordion to Game Crew: PASS
- Remove Owner from the displayed parent table fields: PASS
- Parent table columns are Game, Purpose, Status, Actions: PASS
- Owner remains implicit and is not displayed in the parent table: PASS
- Action buttons match the same scale/height as the game buttons: PASS
- Removed instructional copy: PASS
- Preserve Game row parent structure: PASS
- Preserve Source Idea child row/table: PASS
- Preserve Readiness Output child row/table: PASS
- Preserve API/service contract: PASS
- No browser-owned product data: PASS
- No silent fallbacks: PASS
- Follow table_first_ui.md: PASS
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Validation lane: PASS

Commands:
- `git diff --check -- toolbox/game-hub/index.html toolbox/game-hub/game-hub.js tests/playwright/tools/GameHubMockRepository.spec.mjs`
- PASS
- `node --check toolbox/game-hub/game-hub.js`
- PASS
- `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub"`
- PASS, 11 passed

Notes:
- A broader unscoped run of `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs` was attempted before the final targeted lane. It reported 12 passed and 2 failed. One failure was the ALFA_017 guest redirect assertion and was fixed. The remaining failure was outside this PR's surface: `Toolbox member-role filters focus tools without exposing admin-only controls` received existing `500 /api/game-journey/completion-metrics` responses.
9 changes: 8 additions & 1 deletion docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
assets/theme-v2/css/tables.css
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup.md
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_validation-lane.txt
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
tests/playwright/tools/GameHubMockRepository.spec.mjs
toolbox/game-hub/game-hub.js
toolbox/game-hub/index.html
Binary file modified docs_build/dev/reports/codex_review.diff
Binary file not shown.
38 changes: 28 additions & 10 deletions tests/playwright/tools/GameHubMockRepository.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,18 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
try {
await expect(page.locator(".tool-workspace")).toBeVisible();
await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Add Game" })).toHaveClass("btn");
await expect(page.getByRole("button", { name: "Add Game" })).toHaveClass(/\bbtn\b/);
await expect(page.getByRole("button", { name: "Add Game" })).toHaveClass(/\bbtn--compact\b/);
await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled();
await expect(page.getByLabel("Game Name")).toHaveCount(0);
await expect(page.getByLabel("Game Purpose")).toHaveCount(0);
await expect(page.getByLabel("Game Status")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveClass("btn");
await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeEnabled();
await expect(page.locator("summary").filter({ hasText: /^Game Setup$/ })).toHaveCount(0);
await expect(page.locator("summary").filter({ hasText: /^Game Crew$/ })).toHaveCount(1);
await expect(page.getByRole("link", { name: "Open Game Journey" })).toHaveCount(0);
await expect(page.locator(".tool-center-panel")).not.toContainText("Review games in the parent table");
await expect(page.locator("[data-project-record-status]")).toHaveText("Game table loaded.");
await expect(page.locator("[data-game-project-information]")).toHaveCount(0);
await expect(page.locator("[data-project-records-table]")).toHaveCount(0);
Expand All @@ -281,12 +284,12 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
"Game",
"Purpose",
"Status",
"Owner",
"Actions",
]);
await expect(page.locator("[data-game-rows-table='true'] thead")).not.toContainText(/Role|Next Tool/);
await expect(page.locator("[data-game-rows-table='true'] thead")).not.toContainText(/Owner|Role|Next Tool/);
const demoGameRow = page.locator("[data-game-row='demo-game']");
await expect(demoGameRow.locator("td")).toHaveText(["Game", "Under Construction", "User 1", "Edit"]);
await expect(demoGameRow.locator("td")).toHaveText(["Game", "Under Construction", "Edit"]);
await expect(demoGameRow).not.toContainText("User 1");
await expect(demoGameRow).toHaveAttribute("data-game-active", "true");
await expect(demoGameRow).toHaveAttribute("aria-current", "true");
await expect(demoGameRow.locator("th[data-game-active-cell='true']")).toContainText("Demo Game");
Expand All @@ -303,9 +306,11 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).not.toHaveClass(/primary/);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass(/\bbtn--compact\b/);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-current", "true");
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveText("Edit");
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/);
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveClass(/\bbtn--compact\b/);
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).not.toHaveAttribute("aria-current", "true");
await demoGameRow.locator("[data-game-toggle='demo-game']").click();
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
Expand Down Expand Up @@ -336,6 +341,8 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await page.getByRole("button", { name: "Add Game" }).click();
const addGameRow = page.locator("[data-game-add-row='input']");
await expect(addGameRow.locator("[data-game-action]")).toHaveText(["Save", "Cancel"]);
await expect(addGameRow.getByRole("button", { name: "Save" })).toHaveClass(/\bbtn--compact\b/);
await expect(addGameRow.locator("td")).toHaveCount(3);
await addGameRow.getByLabel("Game").fill("Launch Test Game");
await addGameRow.getByLabel("Purpose").selectOption("Learning Game");
await addGameRow.getByLabel("Status").selectOption("Ready for Testing");
Expand All @@ -351,6 +358,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await page.getByRole("button", { name: "Edit Launch Test Game" }).click();
const editGameRow = page.locator("[data-game-edit-row='launch-test-game-1']");
await expect(editGameRow.locator("[data-game-action]")).toHaveText(["Save", "Cancel"]);
await expect(editGameRow.getByRole("button", { name: "Save" })).toHaveClass(/\bbtn--compact\b/);
await expect(editGameRow.getByLabel("Game")).toHaveValue("Launch Test Game");
await expect(editGameRow.getByLabel("Game")).toHaveAttribute("readonly", "");
await editGameRow.getByLabel("Purpose").selectOption("Capability Demo");
Expand Down Expand Up @@ -525,11 +533,11 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }
try {
await expect(page.locator("[data-game-row='demo-game'] [data-game-toggle='demo-game']")).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).toBeDisabled();
await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).toBeEnabled();
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
await expect(page.locator("[data-project-record-status]")).toHaveText("Game table loaded. Sign in to save changes.");
await expect(page.locator("[data-project-records-table]")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Add Game" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeDisabled();
await expect(page.getByLabel("Game Name")).toHaveCount(0);
await expect(page.getByLabel("Game Purpose")).toHaveCount(0);
Expand All @@ -538,10 +546,21 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }

await page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']").click();
await expect(page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']")).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='gravity-demo']").getByRole("button", { name: "Edit Gravity Demo" })).toBeDisabled();
await expect(page.locator("[data-game-row='gravity-demo']").getByRole("button", { name: "Edit Gravity Demo" })).toBeEnabled();
await expect(page.locator("[data-game-hub-log]")).toHaveText("Sign in to create or update Game Hub projects.");

await expectNoPageFailures(failures);
await page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" }).click();
await page.locator("[data-game-edit-row='demo-game']").getByRole("button", { name: "Save" }).click();
await page.waitForURL(/\/account\/sign-in\.html$/);

await page.goto(`${failures.server.baseUrl}/toolbox/game-hub/index.html`, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "Add Game" }).click();
await page.locator("[data-game-add-row='input']").getByRole("button", { name: "Save" }).click();
await page.waitForURL(/\/account\/sign-in\.html$/);

expect(failures.pageErrors).toEqual([]);
expect(failures.consoleErrors).toEqual([]);
expect(failures.failedRequests.filter((request) => /^\d/.test(request) && !request.includes("/account/sign-in.html"))).toEqual([]);
} finally {
await failures.server.close();
}
Expand Down Expand Up @@ -599,7 +618,6 @@ test("Game Hub shows a creator-safe empty state when no projects exist", async (
"Game",
"Purpose",
"Status",
"Owner",
"Actions",
]);
await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
Expand Down Expand Up @@ -687,7 +705,7 @@ test("Game Hub reports malformed active-game payloads without throwing", async (
await expect(page.locator("[data-active-game-name]")).toHaveCount(0);
await expect(page.locator("[data-current-user-role]")).toHaveCount(0);
await expect(page.locator("[data-game-hub-log]")).toContainText("Active game is temporarily unavailable.");
await expect(page.getByRole("button", { name: "Add Game" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled();

await expectNoPageFailures(failures);
} finally {
Expand Down
40 changes: 20 additions & 20 deletions toolbox/game-hub/game-hub.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ function ensureProjectRecordsSaveAllowed(action) {
return false;
}

function redirectGuestToSignIn() {
window.location.href = "account/sign-in.html";
}

function ensureProjectRecordsSaveAllowedForSave() {
if (projectRecordsSaveAllowed()) {
return true;
}
redirectGuestToSignIn();
return false;
}

function populateSelect(select, options) {
if (!select) {
return;
Expand Down Expand Up @@ -188,7 +200,7 @@ function currentGameMember(activeGame) {

function createActionButton(label, action, options = {}) {
const button = document.createElement("button");
button.className = options.primary ? "btn primary" : "btn";
button.className = options.primary ? "btn btn--compact primary" : "btn btn--compact";
button.type = "button";
button.dataset.gameAction = action;
if (options.gameId) {
Expand All @@ -207,7 +219,6 @@ function createActionButton(label, action, options = {}) {
function createGameButton(game) {
const button = createActionButton("Edit", "edit-game", {
ariaLabel: `Edit ${game.name}`,
disabled: !projectRecordsSaveAllowed(),
gameId: game.id,
});
return button;
Expand Down Expand Up @@ -381,7 +392,7 @@ function renderExpandedGameRow(tbody, game, progress, active) {
row.dataset.gameChildRow = type;
row.id = id;
const content = document.createElement("td");
content.colSpan = 5;
content.colSpan = 4;
render(content);
row.append(content);
tbody.append(row);
Expand All @@ -394,10 +405,8 @@ function renderAddGameRow(tbody) {

if (!state.addingGame) {
const cell = document.createElement("td");
cell.colSpan = 5;
cell.append(createActionButton("Add Game", "start-add-game", {
disabled: !projectRecordsSaveAllowed(),
}));
cell.colSpan = 4;
cell.append(createActionButton("Add Game", "start-add-game"));
row.append(cell);
tbody.append(row);
return;
Expand All @@ -415,14 +424,13 @@ function renderAddGameRow(tbody) {
const statusCell = document.createElement("td");
statusCell.append(createSelect(GAME_HUB_GAME_STATUSES, "Planning", "gameStatusInput", "Status"));

const ownerCell = createCell("Current user");
const actions = document.createElement("td");
actions.append(
createActionButton("Save", "save-add-game", { primary: true }),
createActionButton("Cancel", "cancel-add-game"),
);

row.append(nameCell, purposeCell, statusCell, ownerCell, actions);
row.append(nameCell, purposeCell, statusCell, actions);
tbody.append(row);
}

Expand Down Expand Up @@ -457,7 +465,6 @@ function renderEditGameRow(tbody, game) {
nameCell,
purposeCell,
statusCell,
createCell(game.ownerDisplayName || "No owner"),
actions,
);
tbody.append(row);
Expand Down Expand Up @@ -490,7 +497,6 @@ function renderGameParentRow(tbody, game, activeGame, progress) {
nameCell,
createCell(game.purpose || "Game"),
createCell(game.status || "No status"),
createCell(game.ownerDisplayName || "No owner"),
);

const actions = document.createElement("td");
Expand Down Expand Up @@ -538,7 +544,7 @@ function renderGameList(progress) {
table.className = "data-table data-table--fixed";
table.dataset.gameRowsTable = "true";
table.setAttribute("aria-label", "Games");
table.innerHTML = "<thead><tr><th scope=\"col\">Game</th><th scope=\"col\">Purpose</th><th scope=\"col\">Status</th><th scope=\"col\">Owner</th><th scope=\"col\">Actions</th></tr></thead>";
table.innerHTML = "<thead><tr><th scope=\"col\">Game</th><th scope=\"col\">Purpose</th><th scope=\"col\">Status</th><th scope=\"col\">Actions</th></tr></thead>";
const body = document.createElement("tbody");
listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress));
renderAddGameRow(body);
Expand Down Expand Up @@ -652,7 +658,7 @@ function readGameRowFields(row) {
}

function saveAddedGame(row) {
if (!ensureProjectRecordsSaveAllowed("create")) {
if (!ensureProjectRecordsSaveAllowedForSave()) {
return;
}
const input = readGameRowFields(row);
Expand All @@ -677,7 +683,7 @@ function saveAddedGame(row) {
}

function saveEditedGame(row, gameId) {
if (!ensureProjectRecordsSaveAllowed("update")) {
if (!ensureProjectRecordsSaveAllowedForSave()) {
return;
}
const input = readGameRowFields(row);
Expand Down Expand Up @@ -740,9 +746,6 @@ elements.gameList?.addEventListener("click", (event) => {
}

if (action.dataset.gameAction === "start-add-game") {
if (!ensureProjectRecordsSaveAllowed("create")) {
return;
}
state.addingGame = true;
state.editingGameId = "";
renderWorkspace();
Expand All @@ -762,9 +765,6 @@ elements.gameList?.addEventListener("click", (event) => {
}

if (action.dataset.gameAction === "edit-game") {
if (!ensureProjectRecordsSaveAllowed("update")) {
return;
}
const game = repository.openGame(action.dataset.gameId);
if (reportRepositoryError(game, "Edit game") || !isRecord(game)) {
if (!isRepositoryErrorResult(game)) {
Expand Down
Loading
Loading