diff --git a/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup.md b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup.md
new file mode 100644
index 000000000..e52b1f4ea
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup.md
@@ -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`
diff --git a/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_branch-validation.txt b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_branch-validation.txt
new file mode 100644
index 000000000..a26502903
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_branch-validation.txt
@@ -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
diff --git a/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_manual-validation-notes.txt b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_manual-validation-notes.txt
new file mode 100644
index 000000000..0c99d0f04
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_manual-validation-notes.txt
@@ -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.
diff --git a/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_requirement-checklist.txt b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_requirement-checklist.txt
new file mode 100644
index 000000000..10230f3b8
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_requirement-checklist.txt
@@ -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
diff --git a/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_validation-lane.txt b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_validation-lane.txt
new file mode 100644
index 000000000..8c37f1240
--- /dev/null
+++ b/docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_validation-lane.txt
@@ -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.
diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt
index 5e81caa5c..f7be6c46d 100644
--- a/docs_build/dev/reports/codex_changed_files.txt
+++ b/docs_build/dev/reports/codex_changed_files.txt
@@ -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
diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff
index f6a9821e0..882ec504b 100644
Binary files a/docs_build/dev/reports/codex_review.diff and b/docs_build/dev/reports/codex_review.diff differ
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index 2363ff88d..40f1bc19e 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -249,7 +249,8 @@ 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);
@@ -257,7 +258,9 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
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);
@@ -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");
@@ -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");
@@ -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");
@@ -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");
@@ -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);
@@ -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();
}
@@ -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);
@@ -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 {
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
index 888c75524..e4659f47b 100644
--- a/toolbox/game-hub/game-hub.js
+++ b/toolbox/game-hub/game-hub.js
@@ -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;
@@ -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) {
@@ -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;
@@ -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);
@@ -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;
@@ -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);
}
@@ -457,7 +465,6 @@ function renderEditGameRow(tbody, game) {
nameCell,
purposeCell,
statusCell,
- createCell(game.ownerDisplayName || "No owner"),
actions,
);
tbody.append(row);
@@ -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");
@@ -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 = " ";
+ table.innerHTML = "Game Purpose Status Owner Actions ";
const body = document.createElement("tbody");
listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress));
renderAddGameRow(body);
@@ -652,7 +658,7 @@ function readGameRowFields(row) {
}
function saveAddedGame(row) {
- if (!ensureProjectRecordsSaveAllowed("create")) {
+ if (!ensureProjectRecordsSaveAllowedForSave()) {
return;
}
const input = readGameRowFields(row);
@@ -677,7 +683,7 @@ function saveAddedGame(row) {
}
function saveEditedGame(row, gameId) {
- if (!ensureProjectRecordsSaveAllowed("update")) {
+ if (!ensureProjectRecordsSaveAllowedForSave()) {
return;
}
const input = readGameRowFields(row);
@@ -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();
@@ -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)) {
diff --git a/toolbox/game-hub/index.html b/toolbox/game-hub/index.html
index 8306e6366..c313eeb7a 100644
--- a/toolbox/game-hub/index.html
+++ b/toolbox/game-hub/index.html
@@ -26,16 +26,20 @@ Game Purpose Status Actions