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,6 @@
Branch Validation: PASS

PASS - Current branch: pr/26174-ALFA-006-game-hub-empty-and-error-states.
PASS - Stack base: pr/26174-ALFA-005-idea-project-validation-polish.
PASS - Changes are scoped to Game Hub empty/error UI, targeted tests, and required reports.
PASS - No merge to main performed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Manual Validation Notes: PASS

PASS - Confirmed no-project list renders a creator-safe empty state and no game rows.
PASS - Confirmed listGames failure renders a creator-safe unavailable state and no game rows.
PASS - Confirmed intentionally sensitive upstream error text is not visible in main Game Hub UI.
PASS - Confirmed unavailable state is explicit, not a silent empty-list fallback.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Requirement Checklist: PASS

PASS - Added creator-safe empty state for no Game Hub projects.
PASS - Added creator-safe API unavailable message.
PASS - No server/internal technical error details are shown to creators.
PASS - No silent fallbacks were added; unavailable project lists render an explicit unavailable state.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Validation Lane: PASS

Targeted Playwright impacted lane:
PASS - npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"

Notes:
- Full workspace smoke was not run; targeted impacted Playwright validation was used per request.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# PR_26174_ALFA_006-game-hub-empty-and-error-states

## Purpose

Add creator-safe empty and API-unavailable states for Game Hub projects.

## Summary

- Added a distinct Game Hub project-list empty state for no available projects.
- Added a distinct project-list unavailable state when the Local API/service contract cannot return projects.
- Kept creator-facing messages free of server, database, repository, stack, or secret details.
- Added targeted Playwright coverage for empty and unavailable project-list states.

## Validation

PASS - `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub shows a creator-safe empty state|Game Hub shows a creator-safe unavailable state"`
13 changes: 7 additions & 6 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
toolbox/game-hub/game-hub.js
tests/playwright/tools/GameHubMockRepository.spec.mjs
docs_build/dev/reports/codex_review.diff
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish.md
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_005-idea-project-validation-polish-manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states.md
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_006-game-hub-empty-and-error-states-manual-validation-notes.txt
247 changes: 133 additions & 114 deletions docs_build/dev/reports/codex_review.diff
Original file line number Diff line number Diff line change
@@ -1,128 +1,147 @@
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
index 5d0ccfaff..722595ae9 100644
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
@@ -429,6 +429,123 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index 2d583e1d7..e11631269 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -322,6 +322,89 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }
}
});

+test("Idea Board gates Create Project to Ready ideas and locks converted projects", async ({ page }) => {
+ const server = await startRepoServer({
+ gameJourneyCompletionMetricsLegacyDbPath: null,
+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(),
+ });
+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL;
+ const previousSupabaseEnv = {
+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL,
+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY,
+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY,
+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL,
+ };
+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`;
+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl;
+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board";
+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key";
+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key";
+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`;
+ const createGameRequests = [];
+ const failedRequests = [];
+ const pageErrors = [];
+ const consoleErrors = [];
+
+ page.on("request", (request) => {
+ const requestUrl = request.url();
+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) {
+ createGameRequests.push(request.postDataJSON());
+ }
+ });
+ page.on("response", (response) => {
+ if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`);
+ });
+ page.on("pageerror", (error) => {
+ const text = error.stack || error.message;
+ if (!isBrowserExtensionNoise(text)) pageErrors.push(error.message);
+ });
+ page.on("console", (message) => {
+ if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text());
+ });
+
+ try {
+ await page.route("**/api/platform-settings/banner", async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: { banner: { active: false, message: "", tone: "info" } },
+ ok: true,
+ }),
+ });
+test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => {
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: { result: null },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
+ await page.route("**/api/toolbox/registry/snapshot", async (route) => {
+ await route.fulfill({
+ contentType: "application/json",
+ body: JSON.stringify({
+ data: {
+ activeTools: [],
+ readinessByStatus: {},
+ tools: [],
+ toolboxContract: {},
+ });
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: {
+ result: {
+ gameStatus: "No Game",
+ gameProgress: "No active game",
+ publishingProgress: "Not started",
+ currentFocus: "Create a game",
+ recommendedNextTool: "Game Hub",
+ progressChecklist: [],
+ },
+ ok: true,
+ }),
+ });
+ },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
+ await page.request.post(`${server.baseUrl}/api/session/user`, {
+ data: { userKey: MOCK_DB_KEYS.users.user1 },
+ });
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ data: { result: [] },
+ ok: true,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 200,
+ });
+ });
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
+
+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" });
+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-idea-row='clockwork-courier'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
+
+ await page.locator("[data-idea-board-add-idea]").click();
+ await page.locator("[data-idea-board-idea-input]").fill("Validation Reef");
+ await page.locator("[data-idea-board-pitch-input]").fill("Verify project gating and read-only conversion.");
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining");
+ await page.locator("[data-idea-board-idea-action='save']").click();
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Refining");
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']")).toHaveCount(0);
+ expect(createGameRequests).toEqual([]);
+
+ await page.locator("[data-idea-board-idea-cell='validation-reef']").click();
+ await page.locator("[data-idea-board-add-note='validation-reef']").click();
+ await page.locator("[data-idea-board-note-input]").fill("This note should become read-only project context.");
+ await page.locator("[data-idea-board-note-action='save']").click();
+ await expect(page.locator("[data-idea-board-notes-count='validation-reef']")).toHaveText("1 Note");
+
+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']").click();
+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready");
+ await page.locator("[data-idea-board-idea-action='save']").click();
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Ready");
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]);
+ try {
+ await expect(page.locator("[data-active-game-name]")).toHaveText("No game open");
+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Create a game to start building.");
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
+ await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i);
+ await expectNoPageFailures(failures);
+ } finally {
+ await failures.server.close();
+ }
+});
+
+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']").click();
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Project");
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]);
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-add-note='validation-reef']")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-notes-table='validation-reef'] [data-idea-board-note-action]")).toHaveCount(0);
+ await expect(page.locator("[data-idea-board-note-input-row]")).toHaveCount(0);
+ expect(createGameRequests).toHaveLength(1);
+ expect(Object.keys(createGameRequests[0].args[0]).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]);
+test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => {
+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => {
+ await route.fulfill({
+ body: JSON.stringify({
+ error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace",
+ ok: false,
+ rule: "Browser -> Server API -> Data Source",
+ }),
+ contentType: "application/json; charset=utf-8",
+ status: 503,
+ });
+ });
+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() });
+
+ expect(failedRequests).toEqual([]);
+ expect(pageErrors).toEqual([]);
+ expect(consoleErrors).toEqual([]);
+ try {
+ expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true);
+ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.");
+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0);
+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.");
+ await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i);
+ expect(failures.pageErrors).toEqual([]);
+ expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]);
+ } finally {
+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl);
+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl);
+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value));
+ await server.close();
+ await failures.server.close();
+ }
+});
+
test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => {
const server = await startRepoServer();
const previousApiUrl = process.env.GAMEFOUNDRY_API_URL;
test("Game Hub shows active-game errors without throwing", async ({ page }) => {
await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => {
await route.fulfill({
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
index d2260fe04..2d6f321e4 100644
--- a/toolbox/game-hub/game-hub.js
+++ b/toolbox/game-hub/game-hub.js
@@ -217,6 +217,14 @@ function createGameButton(game, isActive) {
return button;
}

+function createGameListStatus(message, state) {
+ const emptyState = document.createElement("p");
+ emptyState.className = "status";
+ emptyState.dataset.gameListStatus = state;
+ emptyState.textContent = message;
+ return emptyState;
+}
+
function renderProjectInformation(activeGame, currentMember, progress) {
if (!elements.projectRecordsTable) {
return;
@@ -252,22 +260,23 @@ function renderGameList() {
const activeGame = normalizeActiveGame(repository.getActiveGame());
const gameUserKey = currentGameUserKey(activeGame);
const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {});
- const games = Array.isArray(listResult) ? listResult : [];
- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) {
- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly.");
- }

elements.gameList.replaceChildren();

- if (games.length === 0) {
- const emptyState = document.createElement("p");
- emptyState.className = "status";
- emptyState.textContent = "No games. Create a game to continue.";
- elements.gameList.append(emptyState);
+ if (!Array.isArray(listResult)) {
+ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly.";
+ reportRepositoryError(listResult, "Game Hub projects");
+ setStatusLog(message);
+ elements.gameList.append(createGameListStatus(message, "unavailable"));
+ return;
+ }
+
+ if (listResult.length === 0) {
+ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Create a game to start building.", "empty"));
return;
}

- games.forEach((game) => {
+ listResult.forEach((game) => {
const row = document.createElement("article");
row.className = "callout";
row.dataset.gameRow = game.id;
Loading
Loading