From 2a38796f81aae2dceb4151095b8f89a276cd2d32 Mon Sep 17 00:00:00 2001 From: DavidQ Date: Sat, 20 Jun 2026 16:29:17 -0400 Subject: [PATCH] Wire Idea Board projects into Game Hub --- .../dev/reports/codex_changed_files.txt | 94 +- docs_build/dev/reports/codex_review.diff | 2646 +++++++---------- .../APPLY_PR.md | 17 + .../BUILD_PR.md | 56 + .../PLAN_PR.md | 31 + .../game-workspace-mock-repository.js | 36 +- .../GameWorkspaceMockRepository.spec.mjs | 64 +- .../tools/IdeaBoardTableNotes.spec.mjs | 73 +- .../tools/ToolboxRoutePages.spec.mjs | 4 - toolbox/game-workspace/game-workspace.js | 133 +- toolbox/game-workspace/index.html | 128 +- toolbox/idea-board/index.html | 2 +- toolbox/idea-board/index.js | 138 +- 13 files changed, 1655 insertions(+), 1767 deletions(-) create mode 100644 docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md create mode 100644 docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md create mode 100644 docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index e0820a401..0407cd1bd 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,53 +1,51 @@ -# Codex Changed Files - PR_26171_065-message-studio-parent-child-table-foundation +PR_26171_044-idea-board-game-hub-project-flow -## Git Status Short -```text -M docs_build/dev/reports/coverage_changed_js_guardrail.txt - M docs_build/dev/reports/playwright_v8_coverage_report.txt - M tests/playwright/tools/MessagesTool.spec.mjs - M toolbox/messages/index.html - M toolbox/messages/message-tts-service-registry.js - M toolbox/messages/messages.js -?? docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md -?? docs_build/dev/reports/PR_26171_065-manual-validation-notes.md -?? docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md -?? docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md -?? docs_build/dev/reports/PR_26171_065-validation.md -``` - -## Scoped Diff Stat -```text -.../dev/reports/coverage_changed_js_guardrail.txt | 2 + - .../dev/reports/playwright_v8_coverage_report.txt | 43 +- - tests/playwright/tools/MessagesTool.spec.mjs | 317 +++++--------- - toolbox/messages/index.html | 29 +- - toolbox/messages/message-tts-service-registry.js | 8 +- - toolbox/messages/messages.js | 486 ++++++++++++--------- - 6 files changed, 415 insertions(+), 470 deletions(-) -``` - -## Changed Files -- toolbox/messages/index.html -- toolbox/messages/messages.js -- toolbox/messages/message-tts-service-registry.js -- tests/playwright/tools/MessagesTool.spec.mjs -- docs_build/dev/reports/coverage_changed_js_guardrail.txt -- docs_build/dev/reports/playwright_v8_coverage_report.txt -- docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md -- docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md -- docs_build/dev/reports/PR_26171_065-validation.md -- docs_build/dev/reports/PR_26171_065-manual-validation-notes.md -- docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md +Changed files: +- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md +- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md +- docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md - docs_build/dev/reports/codex_review.diff - docs_build/dev/reports/codex_changed_files.txt +- src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js +- tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs +- tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +- tests/playwright/tools/ToolboxRoutePages.spec.mjs +- toolbox/game-workspace/game-workspace.js +- toolbox/game-workspace/index.html +- toolbox/idea-board/index.html +- toolbox/idea-board/index.js + +Requirement PASS evidence: +- PASS: Create Project appears only for Ready ideas. Evidence: Idea Board actions remain Edit/Create Project/Delete for Ready rows and switch to Open in Game Hub/Archive after creation in targeted Playwright. +- PASS: Create Project uses the shared Game Hub project contract. Evidence: Idea Board calls the existing game-workspace server repository client createGame method; Playwright asserts the createGame repository request occurs. +- PASS: Create Project sets Idea status to Project. Evidence: targeted Playwright asserts the row status becomes Project. +- PASS: Project ideas and notes are read-only. Evidence: targeted Playwright asserts no Edit/Delete/Add Note/note actions remain for the Project row. +- PASS: Project row actions are Open in Game Hub and Archive. Evidence: targeted Playwright asserts exact action labels. +- PASS: Archived ideas remain hidden by default and show Restore/Delete when visible. Evidence: targeted Playwright archives, shows Archived through the filter, and asserts Restore/Delete. +- PASS: Game Hub shows creator-facing Project Information. Evidence: Game Hub HTML/JS replaces internal record display with Project Information and targeted Game Hub Playwright passes. +- PASS: Game Hub shows read-only Source Idea fields. Evidence: Idea Board-to-Game Hub Playwright asserts Idea, Pitch, and Notes from the source idea render in Game Hub. +- PASS: Game Hub avoids internal IDs, DB/API/mock/debug/seed wording in creator-facing display. Evidence: touched runtime files pass creator-visible text scan and targeted Playwright checks the navigated Game Hub main content. +- PASS: Open in Game Hub navigates to the related project. Evidence: targeted Playwright waits for /toolbox/game-workspace/index.html?game=lantern-reef-* and asserts the linked project renders. +- PASS: No Game Journey expansion beyond existing link/reference. Evidence: implementation does not add Game Journey creation or new journey wiring. -## Validation -- PASS: `node --check toolbox\messages\messages.js`. -- PASS: `node --check toolbox\messages\message-tts-service-registry.js`. -- PASS: `node --check tests\playwright\tools\MessagesTool.spec.mjs`. -- PASS: HTML inline style/script/event scan for `toolbox/messages/index.html`. -- PASS: `npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --project=playwright --workers=1 --reporter=list`. -- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace). +Validation evidence: +- PASS: node --check toolbox/idea-board/index.js +- PASS: node --check toolbox/game-workspace/game-workspace.js +- PASS: node --check toolbox/game-workspace/game-workspace-api-client.js +- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js +- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +- PASS: node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs +- PASS: node --check tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs +- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line +- PASS: npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Idea Board launches" +- PASS: npx playwright test tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub" +- PASS: npm run test:workspace-v2 +- PASS: git diff --check -## ZIP -- Path: `tmp/PR_26171_065-message-studio-parent-child-table-foundation_delta.zip`. +Git workflow: +- Current branch: codex/pr-26171-044-idea-board-game-hub-project-flow +- Created branch: codex/pr-26171-044-idea-board-game-hub-project-flow +- Push result: to be reported after push +- PR URL: to be reported after PR creation +- Merge result: to be reported after merge +- Final main commit: to be reported after returning to main and pulling latest diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 39d7f84a5..cffbc3823 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,1576 +1,1210 @@ -diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 15213f1d6..e0820a401 100644 ---- a/docs_build/dev/reports/codex_changed_files.txt -+++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,78 +1,53 @@ --# PR_26171_042 Codex Changed Files Report -- --## Instruction Compliance Gate -- --- Current branch before execution: `main` --- Required execution branch before PR branch: `main` --- Branch validation: PASS --- Clean repository before branch creation: PASS --- PR owner/parity: PASS, `042` is even and Idea/Tool Display navigation scope maps to PC / Environment 1. --- Implementation path: PASS, active Idea Board and Theme V2 files only. --- Validation scope: PASS, targeted Idea Board, targeted Toolbox route for Idea Board, and workspace contract lane because shared Tool Display Mode behavior changed. --- Required reports: PASS, `docs_build/dev/reports/codex_review.diff` and this report are updated. --- ZIP requirement: PASS, `tmp/PR_26171_042-idea-board-navigation-fallback-cleanup_delta.zip` is required and produced before final delivery. -- --## Git Workflow Fields -- --- Created branch: `codex/pr-26171-042-idea-board-navigation-fallback-cleanup` --- Push result: PASS, branch pushed to `origin/codex/pr-26171-042-idea-board-navigation-fallback-cleanup`. --- PR URL: `https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/19` --- Merge result: recorded in final Codex delivery after GitHub merge returns the merge SHA. --- Final main commit: recorded in final Codex delivery after returning to `main` and pulling latest. --- Conflict resolution: PASS, merged latest `origin/main` (`9df4942226b0c1a25cfc9567040fc237d90df8f9`) and resolved conflicts only in singleton generated report artifacts. -- --## Scoped Files -- --- `assets/theme-v2/js/tool-display-mode.js` --- `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` --- `tests/playwright/tools/ToolboxRoutePages.spec.mjs` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/PLAN_PR.md` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/BUILD_PR.md` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/APPLY_PR.md` --- `docs_build/dev/reports/coverage_changed_js_guardrail.txt` --- `docs_build/dev/reports/playwright_v8_coverage_report.txt` --- `docs_build/dev/reports/codex_review.diff` --- `docs_build/dev/reports/codex_changed_files.txt` -- --## Requirement Evidence -- --- PASS: Removed creator-visible Tool Display Mode navigation diagnostic fallback. `tool-display-mode.js` now logs the navigation load failure to console only and appends no status paragraph. --- PASS: Removed visible message `Tool navigation is temporarily unavailable. Refresh the page or try again shortly.` --- PASS: Idea Board stays usable when registry-backed navigation cannot load. Static/no-registry Playwright path expands notes and adds a note successfully. --- PASS: Creator-facing UI does not mention server, API, local server, port, registry, snapshot, or implementation details in the navigation fallback area. --- PASS: Navigation failure does not affect Idea Board table functionality. --- PASS: API-backed local route validated by targeted Idea Board and Toolbox route Playwright. --- PASS: Static/no-registry route behavior validated by targeted Idea Board Playwright with the registry snapshot returning no data. --- PASS: Optional previous/next navigation is omitted when unavailable. --- PASS: Idea Board lifecycle, Show filter, Create Project, Archive, chevron, and table row editing behavior were not changed. -+# Codex Changed Files - PR_26171_065-message-studio-parent-child-table-foundation +diff --git a/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md +new file mode 100644 +index 000000000..2fba95562 +--- /dev/null ++++ b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/APPLY_PR.md +@@ -0,0 +1,17 @@ ++# PR_26171_044-idea-board-game-hub-project-flow Apply + -+## Git Status Short -+```text -+M docs_build/dev/reports/coverage_changed_js_guardrail.txt -+ M docs_build/dev/reports/playwright_v8_coverage_report.txt -+ M tests/playwright/tools/MessagesTool.spec.mjs -+ M toolbox/messages/index.html -+ M toolbox/messages/message-tts-service-registry.js -+ M toolbox/messages/messages.js -+?? docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md -+?? docs_build/dev/reports/PR_26171_065-manual-validation-notes.md -+?? docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md -+?? docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md -+?? docs_build/dev/reports/PR_26171_065-validation.md -+``` ++## Apply Steps + -+## Scoped Diff Stat -+```text -+.../dev/reports/coverage_changed_js_guardrail.txt | 2 + -+ .../dev/reports/playwright_v8_coverage_report.txt | 43 +- -+ tests/playwright/tools/MessagesTool.spec.mjs | 317 +++++--------- -+ toolbox/messages/index.html | 29 +- -+ toolbox/messages/message-tts-service-registry.js | 8 +- -+ toolbox/messages/messages.js | 486 ++++++++++++--------- -+ 6 files changed, 415 insertions(+), 470 deletions(-) -+``` ++1. Start from clean `main`. ++2. Pull latest `main`. ++3. Create `codex/pr-26171-044-idea-board-game-hub-project-flow`. ++4. Implement only the BUILD scope. ++5. Run requested validation. ++6. Stage scoped files only. ++7. Commit. ++8. Push branch. ++9. Create PR. ++10. Resolve conflicts if encountered and rerun validation. ++11. Merge after validation passes. ++12. Return to `main` and pull latest. ++13. Produce final report with PR URL, merge result, final main commit, ZIP path, ZIP size, ZIP contents, and requirement PASS evidence. +diff --git a/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md +new file mode 100644 +index 000000000..228f7703f +--- /dev/null ++++ b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/BUILD_PR.md +@@ -0,0 +1,56 @@ ++# PR_26171_044-idea-board-game-hub-project-flow Build + -+## Changed Files -+- toolbox/messages/index.html -+- toolbox/messages/messages.js -+- toolbox/messages/message-tts-service-registry.js -+- tests/playwright/tools/MessagesTool.spec.mjs -+- docs_build/dev/reports/coverage_changed_js_guardrail.txt -+- docs_build/dev/reports/playwright_v8_coverage_report.txt -+- docs_build/dev/reports/PR_26171_065-message-studio-parent-child-table-foundation.md -+- docs_build/dev/reports/PR_26171_065-parent-child-table-checklist.md -+- docs_build/dev/reports/PR_26171_065-validation.md -+- docs_build/dev/reports/PR_26171_065-manual-validation-notes.md -+- docs_build/dev/reports/PR_26171_065-instruction-compliance-checklist.md -+- docs_build/dev/reports/codex_review.diff -+- docs_build/dev/reports/codex_changed_files.txt - - ## Validation -- --- PASS: `node --check assets/theme-v2/js/tool-display-mode.js` --- PASS: `node --check toolbox/idea-board/index.js` --- PASS: `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` --- PASS: `node --check tests/playwright/tools/ToolboxRoutePages.spec.mjs` --- PASS: `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line --timeout=90000` --- PASS: `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --project=playwright --workers=1 --reporter=line -g "Idea Board launches" --timeout=90000` --- PASS: `npm run test:workspace-v2` --- PASS: `git diff --check` --- SKIP: Full samples smoke, per user instruction. -- --## Coverage Evidence -- --- PASS: `docs_build/dev/reports/playwright_v8_coverage_report.txt` updated. --- PASS: `docs_build/dev/reports/coverage_changed_js_guardrail.txt` updated. --- PASS: Changed runtime JS coverage lists `assets/theme-v2/js/tool-display-mode.js` at 64% advisory function coverage. -- --## ZIP Contents -- --- `assets/theme-v2/js/tool-display-mode.js` --- `docs_build/dev/reports/codex_changed_files.txt` --- `docs_build/dev/reports/codex_review.diff` --- `docs_build/dev/reports/coverage_changed_js_guardrail.txt` --- `docs_build/dev/reports/playwright_v8_coverage_report.txt` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/APPLY_PR.md` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/BUILD_PR.md` --- `docs_build/pr/PR_26171_042-idea-board-navigation-fallback-cleanup/PLAN_PR.md` --- `tests/playwright/tools/IdeaBoardTableNotes.spec.mjs` --- `tests/playwright/tools/ToolboxRoutePages.spec.mjs` -+- PASS: `node --check toolbox\messages\messages.js`. -+- PASS: `node --check toolbox\messages\message-tts-service-registry.js`. -+- PASS: `node --check tests\playwright\tools\MessagesTool.spec.mjs`. -+- PASS: HTML inline style/script/event scan for `toolbox/messages/index.html`. -+- PASS: `npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --project=playwright --workers=1 --reporter=list`. -+- PASS: `npm run test:workspace-v2` (legacy command name; user-facing language is Project Workspace). ++## Source Of Truth ++ ++Use the user request for `PR_26171_044-idea-board-game-hub-project-flow`, `docs_build/dev/PROJECT_INSTRUCTIONS.md`, `docs_build/dev/PROJECT_MULTI_PC.txt`, and this BUILD doc. ++ ++## Singular Purpose ++ ++Complete the Idea Board to Game Hub creator project handoff. ++ ++## Exact Targets ++ ++- `toolbox/idea-board/index.js` ++- `toolbox/game-workspace/game-workspace.js` ++- Existing targeted Playwright specs for Idea Board and Game Hub/Game Workspace. ++- Any smallest existing shared project or handoff contract file required by the flow. ++- `docs_build/dev/reports/codex_review.diff` ++- `docs_build/dev/reports/codex_changed_files.txt` ++ ++## Requirements ++ ++- Create Project appears only for Ready ideas. ++- Clicking Create Project creates or links a Game Hub project using an existing/shared project contract if available. ++- Clicking Create Project sets Idea status to Project. ++- Project ideas and their notes become read-only. ++- Project row actions are Open in Game Hub and Archive. ++- Project ideas cannot be edited or deleted. ++- Project ideas cannot add, edit, or delete notes. ++- Archived ideas remain hidden by default through existing filter behavior and show Restore and Delete when visible. ++- Game Hub displays creator-facing project data only. ++- Game Hub shows Project Information. ++- Game Hub shows a read-only Source Idea section with Idea, Pitch, and Notes. ++- Game Hub must not show internal IDs, DB/API/mock/debug/seed wording, or implementation details. ++- Open in Game Hub from Idea Board navigates to the related Game Hub project view. ++- If existing project handoff, route, or mock adapter wiring is missing, add the smallest fix needed. ++- Do not expand into Game Journey unless required as a stub/reference for the handoff. ++ ++## Non-Goals ++ ++- Do not change unrelated Game Hub workflows. ++- Do not introduce real database persistence. ++- Do not add SQLite. ++- Do not change full samples smoke behavior. ++ ++## Validation ++ ++- `node --check toolbox/idea-board/index.js` ++- Changed-file syntax checks for Game Hub JavaScript. ++- Targeted Idea Board Playwright. ++- Targeted Game Hub Playwright if existing coverage exists. ++- `npm run test:workspace-v2` ++- `git diff --check` + +## ZIP -+- Path: `tmp/PR_26171_065-message-studio-parent-child-table-foundation_delta.zip`. -diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt -index 242796c4f..6fbeaba58 100644 ---- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt -+++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt -@@ -7,6 +7,8 @@ Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. - - Changed runtime JS files considered: - (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14 -+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118 -+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6 - - Guardrail warnings: - (100%) none - no changed runtime JS coverage warnings -diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt -index 17eb943d2..578a19ddc 100644 ---- a/docs_build/dev/reports/playwright_v8_coverage_report.txt -+++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt -@@ -12,45 +12,34 @@ Note: entry percentages use function coverage when available, otherwise line cov - Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran. - - Exercised tool entry points detected: --(72%) Toolbox Index - exercised 10 runtime JS files -+(83%) Toolbox Index - exercised 4 runtime JS files - (0%) Tool Template V2 - not exercised by this Playwright run --(61%) Theme V2 Shared JS - exercised 7 runtime JS files -+(56%) Theme V2 Shared JS - exercised 2 runtime JS files - - Changed runtime JS files covered: - (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14 -+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118 -+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6 - - Files with executed line/function counts where available: --(14%) assets/theme-v2/js/account-auth-service.js - executed lines 64/64; executed functions 1/7 --(14%) assets/theme-v2/js/admin-setup-actions.js - executed lines 55/55; executed functions 1/7 --(18%) assets/theme-v2/js/account-page-data.js - executed lines 150/150; executed functions 3/17 --(20%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 2/10 --(25%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 1/4 --(25%) src/api/session-api-client.js - executed lines 68/68; executed functions 3/12 --(29%) src/engine/input/NormalizedInputRegistry.js - executed lines 341/341; executed functions 6/21 --(33%) src/api/admin-setup-api-client.js - executed lines 13/13; executed functions 1/3 --(33%) src/api/toolbox-votes-api-client.js - executed lines 46/46; executed functions 2/6 --(56%) toolbox/colors/colors.js - executed lines 1848/1848; executed functions 115/204 --(58%) src/api/server-api-client.js - executed lines 167/167; executed functions 11/19 -+(34%) src/engine/audio/TextToSpeechEngine.js - executed lines 412/412; executed functions 15/44 -+(36%) src/api/server-api-client.js - executed lines 167/167; executed functions 5/14 -+(38%) src/api/public-config-client.js - executed lines 209/209; executed functions 10/26 -+(54%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 46/85 -+(58%) toolbox/messages/messages-api-client.js - executed lines 64/64; executed functions 11/19 - (64%) assets/theme-v2/js/tool-display-mode.js - executed lines 204/204; executed functions 9/14 --(64%) toolbox/controls/controls.js - executed lines 610/610; executed functions 36/56 --(65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26 --(67%) src/api/game-journey-completion-api-client.js - executed lines 15/15; executed functions 2/3 --(74%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 977/977; executed functions 67/90 --(75%) toolbox/game-workspace/game-workspace.js - executed lines 458/458; executed functions 33/44 --(89%) toolbox/tools-page-accordions.js - executed lines 1156/1156; executed functions 105/118 --(90%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 26/29 --(91%) toolbox/game-design/game-design.js - executed lines 254/254; executed functions 21/23 --(94%) assets/theme-v2/js/marketplace-page.js - executed lines 170/170; executed functions 16/17 --(100%) src/api/marketplace-api-client.js - executed lines 16/16; executed functions 3/3 --(100%) toolbox/colors/palette-api-client.js - executed lines 28/28; executed functions 4/4 --(100%) toolbox/controls/controls-api-client.js - executed lines 33/33; executed functions 5/5 --(100%) toolbox/game-design/game-design-api-client.js - executed lines 13/13; executed functions 2/2 --(100%) toolbox/game-workspace/game-workspace-api-client.js - executed lines 20/20; executed functions 3/3 -+(76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29 -+(87%) toolbox/messages/messages.js - executed lines 1233/1233; executed functions 103/118 -+(100%) src/engine/audio/TextToSpeechDefaults.js - executed lines 108/108; executed functions 1/1 -+(100%) toolbox/messages/message-tts-service-registry.js - executed lines 42/42; executed functions 6/6 - - Uncovered or low-coverage changed JS files: - (100%) none - no low-coverage changed runtime JS files - - Changed JS files considered: - (0%) tests/playwright/tools/IdeaBoardTableNotes.spec.mjs - changed JS file not collected as browser runtime coverage -+(0%) tests/playwright/tools/MessagesTool.spec.mjs - changed JS file not collected as browser runtime coverage - (0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage - (64%) assets/theme-v2/js/tool-display-mode.js - changed JS file with browser V8 coverage -+(87%) toolbox/messages/messages.js - changed JS file with browser V8 coverage -+(100%) toolbox/messages/message-tts-service-registry.js - changed JS file with browser V8 coverage -diff --git a/tests/playwright/tools/MessagesTool.spec.mjs b/tests/playwright/tools/MessagesTool.spec.mjs -index b120f19a3..451c4439f 100644 ---- a/tests/playwright/tools/MessagesTool.spec.mjs -+++ b/tests/playwright/tools/MessagesTool.spec.mjs -@@ -31,7 +31,7 @@ async function jsonRequest(url, options = {}) { - test.beforeEach(async ({ page }) => { - await installPlaywrightStorageIsolation(page, { - lane: "messages-tool", -- surface: "Message Studio Local API, legacy SQLite technical debt adapter, and Theme V2 tool", -+ surface: "Message Studio parent/child table, Local API, and Theme V2 tool", - }); - }); - -@@ -134,42 +134,24 @@ async function closeMessagesRun(failures, page) { - } - } - --async function addEmotionProfile(page, name) { -- await page.locator("[data-messages-emotion-add-row]").click(); -- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-name]").fill(name); -- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-volume]").fill("0.9"); -- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-pitch]").fill("0.85"); -- await page.locator("[data-messages-emotion-editor='__new__'] [data-emotion-rate]").fill("0.95"); -- await page.locator("[data-messages-emotion-commit='__new__']").click(); --} -- --async function addTtsProfile(page, name) { -- await page.locator("[data-messages-tts-add-row]").click(); -- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-name]").fill(name); -- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-provider]").fill("browser-speech"); -- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-voice]").fill("Test Voice"); -- await page.locator("[data-messages-tts-editor='__new__'] [data-tts-language]").fill("en-US"); -- await page.locator("[data-messages-tts-commit='__new__']").click(); --} -- --async function addMessageRow(page, values) { -- await page.locator("[data-messages-add-row]").click(); -+async function addMessage(page, values) { -+ await page.getByRole("button", { name: "Add Message" }).click(); - await page.locator("[data-messages-row-editor='__new__'] [data-message-name]").fill(values.name); -- await page.locator("[data-messages-row-editor='__new__'] [data-message-emotion]").selectOption({ label: values.emotion }); -+ await page.locator("[data-messages-row-editor-details='__new__'] [data-message-emotion]").selectOption({ label: values.emotion }); - await page.locator("[data-messages-row-editor-details='__new__'] [data-message-text]").fill(values.text); - await page.locator("[data-messages-row-editor-details='__new__'] [data-message-notes]").fill(values.notes || ""); - await page.locator("[data-messages-commit='__new__']").click(); - } - --async function addSegmentRow(page, values) { -- await page.locator("[data-messages-segment-add-row]").click(); -+async function addPart(page, values) { -+ await page.getByRole("button", { name: "Add Part" }).click(); - await page.locator("[data-messages-segment-editor='__new__'] [data-segment-order]").fill(String(values.order)); -- await page.locator("[data-messages-segment-editor='__new__'] [data-segment-emotion]").selectOption({ label: values.emotion }); - await page.locator("[data-messages-segment-editor='__new__'] [data-segment-text]").fill(values.text); -+ await page.locator("[data-messages-segment-editor='__new__'] [data-segment-emotion]").selectOption({ label: values.emotion }); - await page.locator("[data-messages-segment-commit='__new__']").click(); - } - --test("Message Studio uses table governance, validates rows, and persists through the Local API", async ({ page }) => { -+test("Message Studio renders Messages with child Message Parts and plays ordered parts", async ({ page }) => { - const sqlitePath = messagesDbPath(); - await fs.rm(sqlitePath, { force: true }); - const failures = await openMessagesPage(page, sqlitePath); -@@ -179,242 +161,155 @@ test("Message Studio uses table governance, validates rows, and persists through - await expect(page.locator(".tool-workspace")).toBeVisible(); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); - await expect(page.locator("[data-messages-category-name]")).toHaveCount(0); -- await expect(page.locator("[data-messages-category]")).toHaveCount(0); -- await expect(page.getByRole("button", { name: /delete/i })).toHaveCount(0); -- await expect(page.locator("[data-messages-persistence-engine]")).toHaveText("Postgres target"); -- await expect(page.locator("[data-messages-tts-service]")).toHaveValue("browser-speech-synthesis"); -- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Select a message row or segment row before testing speech."); -- await expect(page.locator("[data-messages-test-speech]")).toBeDisabled(); -- await expect(page.locator("[data-messages-preview-message], [data-messages-preview-segments], [data-messages-preview-stop]")).toHaveCount(0); -- -- await expect(page.locator("[data-messages-emotions]")).toContainText("Calm"); -- await expect(page.locator("[data-messages-emotions]")).toContainText("Urgent"); -+ await expect(page.locator("[data-messages-tts-add-row]")).toHaveCount(0); -+ await expect(page.getByRole("columnheader", { name: "Message Name" })).toBeVisible(); -+ await expect(page.getByRole("columnheader", { name: "Default TTS Profile" })).toBeVisible(); - await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Browser Speech Default"); -+ await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Owned by TTS Studio"); - -- await addEmotionProfile(page, "Robot"); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated emotion profile Robot."); -- await expect(page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" })).toContainText("0.9"); -- await page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" }).getByRole("button", { name: "Edit" }).click(); -- await page.locator("[data-messages-emotion-editor] [data-emotion-rate]").fill("1.05"); -- await page.locator("[data-messages-emotion-editor] [data-messages-emotion-commit]").click(); -- await page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" }).getByRole("button", { name: "Disable" }).click(); -- await expect(page.locator("[data-messages-emotion-row]").filter({ hasText: "Robot" })).toContainText("Inactive"); -- -- await addTtsProfile(page, "Arcade Browser Voice"); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated TTS profile Arcade Browser Voice."); -- await expect(page.locator("[data-messages-tts-profiles]")).toContainText("Arcade Browser Voice"); -- await expect(page.locator("[data-messages-tts-count]")).toHaveText("3"); -- await page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" }).getByRole("button", { name: "Edit" }).click(); -- await page.locator("[data-messages-tts-editor] [data-tts-language]").fill("en-GB"); -- await page.locator("[data-messages-tts-editor] [data-messages-tts-commit]").click(); -- await expect(page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" })).toContainText("Active"); -- -- const ttsProfilesResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/tts-profiles`); -- expect(ttsProfilesResult.response.ok).toBe(true); -- expect(ttsProfilesResult.payload.ok).toBe(true); -- const createdTtsProfile = ttsProfilesResult.payload.data.ttsProfiles.find((profile) => profile.name === "Arcade Browser Voice"); -- expect(createdTtsProfile).toEqual(expect.objectContaining({ -- active: true, -- language: "en-GB", -- providerKey: "browser-speech", -- voiceName: "Test Voice", -- })); -- expect(createdTtsProfile.key).toMatch(ULID_PATTERN); -- expect(createdTtsProfile.createdBy).toMatch(ULID_PATTERN); -- expect(createdTtsProfile.updatedBy).toMatch(ULID_PATTERN); -- -- await page.locator("[data-messages-add-row]").click(); -- await expect(page.locator("[data-messages-row-editor='__new__']")).toBeVisible(); -+ await page.getByRole("button", { name: "Add Message" }).click(); - await page.locator("[data-messages-commit='__new__']").click(); - await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); - await expect(page.locator("[data-messages-validation-errors]")).toContainText("Message Name is required."); - await expect(page.locator("[data-messages-validation-errors]")).toContainText("Emotion Profile is required."); - await expect(page.locator("[data-messages-validation-errors]")).toContainText("Message Text is required."); -- - await page.locator("[data-messages-cancel='__new__']").click(); -- await addMessageRow(page, { + -+ await addMessage(page, { - emotion: "Urgent", -- name: "Forest Warning", -- notes: "Opening forest danger line.", -- text: "The forest gets darker beyond this point.\nWe are being attacked by bats.", -+ name: "Bat Encounter", -+ notes: "Opening combat line.", -+ text: "Bats drop from the rafters.", - }); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Forest Warning."); -- await expect(page.locator("[data-messages-count]")).toHaveText("1"); -- await expect(page.locator("[data-messages-table]")).toContainText("Forest Warning"); -- await expect(page.locator("[data-messages-selected-text]")).toHaveText("The forest gets darker beyond this point.\nWe are being attacked by bats."); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Bat Encounter."); -+ await expect(page.locator("[data-messages-table]")).toContainText("Bat Encounter"); -+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("Dialog"); -+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("0"); -+ await expect(page.locator("[data-message-default-tts-profile]").first()).toHaveValue(/.+/); ++Produce `tmp/PR_26171_044-idea-board-game-hub-project-flow_delta.zip` with repo-structured changed files only. +diff --git a/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md +new file mode 100644 +index 000000000..c93dea32e +--- /dev/null ++++ b/docs_build/pr/PR_26171_044-idea-board-game-hub-project-flow/PLAN_PR.md +@@ -0,0 +1,31 @@ ++# PR_26171_044-idea-board-game-hub-project-flow Plan + -+ const messageRow = page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }); -+ await messageRow.click(); - await expect(page.locator("[data-messages-segment-host]")).toBeVisible(); -+ await expect(page.getByRole("heading", { name: "Message Parts" })).toBeVisible(); -+ await expect(page.getByRole("columnheader", { name: "Order" })).toBeVisible(); -+ await expect(page.getByRole("columnheader", { name: "Text" })).toBeVisible(); -+ await expect(page.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible(); - -- const listResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages`); -- expect(listResult.response.ok).toBe(true); -- expect(listResult.payload.ok).toBe(true); -- const createdMessage = listResult.payload.data.messages.find((message) => message.name === "Forest Warning"); -- expect(createdMessage).toEqual(expect.objectContaining({ -- active: true, -- emotionProfileName: "Urgent", -- messageText: "The forest gets darker beyond this point.\nWe are being attacked by bats.", -- notes: "Opening forest danger line.", -- })); -- expect(createdMessage.key).toMatch(ULID_PATTERN); -- expect(createdMessage.createdBy).toMatch(ULID_PATTERN); -- expect(createdMessage.updatedBy).toMatch(ULID_PATTERN); -- -- await page.locator("[data-messages-segment-add-row]").click(); -+ await page.getByRole("button", { name: "Add Part" }).click(); - await page.locator("[data-messages-segment-editor='__new__'] [data-segment-order]").fill(""); - await page.locator("[data-messages-segment-commit='__new__']").click(); -- await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); -- await expect(page.locator("[data-messages-validation-errors]")).toContainText("Segment Text is required."); -+ await expect(page.locator("[data-messages-validation-errors]")).toContainText("Part Text is required."); - await expect(page.locator("[data-messages-validation-errors]")).toContainText("Emotion Profile is required."); - await expect(page.locator("[data-messages-validation-errors]")).toContainText("Display Order is required."); - await page.locator("[data-messages-segment-cancel='__new__']").click(); - -- await addSegmentRow(page, { -+ await addPart(page, { - emotion: "Calm", - order: 1, -- text: "The forest gets darker beyond this point.", -+ text: "Bats drop from the rafters.", - }); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 1."); -- await addSegmentRow(page, { -+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 1."); -+ await addPart(page, { - emotion: "Urgent", - order: 2, -- text: "We are being attacked by bats.", -+ text: "Keep your torch high.", - }); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 2."); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 2."); - await expect(page.locator("[data-messages-segment-row]")).toHaveCount(2); -+ await expect(page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" })).toContainText("2"); - -+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Play Message" }).click(); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Play Message queued 2 parts for Bat Encounter."); - let speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); -- expect(speechCalls).toEqual([]); -- -- await page.locator("[data-messages-preview-tts-profile]").selectOption({ label: "Arcade Browser Voice" }); -- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning" }).click(); -- await expect(page.locator("[data-messages-speech-test-target]")).toHaveText("Message Row: Forest Warning"); -- await expect(page.locator("[data-messages-test-speech]")).toBeEnabled(); -- await page.locator("[data-messages-test-speech]").click(); -- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Speech test started for Message Row: Forest Warning using Browser Speech Synthesis."); -- speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); -+ expect(speechCalls.slice(-2).map((call) => call.text)).toEqual([ -+ "Bats drop from the rafters.", -+ "Keep your torch high.", -+ ]); - expect(speechCalls.at(-1)).toEqual(expect.objectContaining({ -- lang: "en-GB", -- pitch: 1.08, -- rate: 1.15, -- text: "The forest gets darker beyond this point.\nWe are being attacked by bats.", -+ lang: "en-US", - type: "speak", - voiceName: "Test Voice", -- volume: 1, - })); - -- await page.locator("[data-messages-segment-row]").filter({ hasText: "The forest gets darker beyond this point." }).click(); -- await expect(page.locator("[data-messages-speech-test-target]")).toHaveText("Segment 1"); -- await page.locator("[data-messages-test-speech]").click(); -- await expect(page.locator("[data-messages-preview-status]")).toHaveText("Speech test started for Segment 1 using Browser Speech Synthesis."); -+ await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).getByRole("button", { name: "Play Part" }).click(); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Play Part queued Part 2 using Browser Speech Default."); - speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); - expect(speechCalls.at(-1)).toEqual(expect.objectContaining({ -- lang: "en-GB", -- pitch: 1, -- rate: 1, -- text: "The forest gets darker beyond this point.", -+ text: "Keep your torch high.", - type: "speak", - voiceName: "Test Voice", -- volume: 1, - })); - -+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Edit Message" }).click(); -+ await page.locator("[data-messages-row-editor] [data-message-name]").fill("Bat Encounter Updated"); -+ await page.locator("[data-messages-row-editor] [data-messages-commit]").click(); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Bat Encounter Updated."); ++## Purpose + -+ await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).getByRole("button", { name: "Edit Part" }).click(); -+ await page.locator("[data-messages-segment-editor] [data-segment-text]").fill("Keep your torch high and shield raised."); -+ await page.locator("[data-messages-segment-editor] [data-messages-segment-commit]").click(); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Updated message part 2."); ++Wire the Idea Board Create Project action into a creator-facing Game Hub project view while preserving table-first Idea Board behavior. + -+ const listResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages`); -+ expect(listResult.response.ok).toBe(true); -+ expect(listResult.payload.ok).toBe(true); -+ const createdMessage = listResult.payload.data.messages.find((message) => message.name === "Bat Encounter Updated"); -+ expect(createdMessage).toEqual(expect.objectContaining({ -+ active: true, -+ categoryName: "Dialog", -+ emotionProfileName: "Urgent", -+ messageText: "Bats drop from the rafters.", -+ notes: "Opening combat line.", -+ })); -+ expect(createdMessage.key).toMatch(ULID_PATTERN); ++## Scope + - const segmentsResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/segments`); - expect(segmentsResult.response.ok).toBe(true); -- expect(segmentsResult.payload.ok).toBe(true); - const createdSegments = segmentsResult.payload.data.segments.filter((segment) => segment.messageKey === createdMessage.key); -- expect(createdSegments).toHaveLength(2); - expect(createdSegments.map((segment) => segment.displayOrder)).toEqual([1, 2]); -+ expect(createdSegments.map((segment) => segment.segmentText)).toEqual([ -+ "Bats drop from the rafters.", -+ "Keep your torch high and shield raised.", -+ ]); - createdSegments.forEach((segment) => { - expect(segment.key).toMatch(ULID_PATTERN); - expect(segment.createdBy).toMatch(ULID_PATTERN); - expect(segment.updatedBy).toMatch(ULID_PATTERN); - }); - -- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats." }).getByRole("button", { name: "Move Up" }).click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Segment order updated."); -- await expect(page.locator("[data-messages-segment-row]").first()).toContainText("We are being attacked by bats."); -- -- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats." }).getByRole("button", { name: "Edit" }).click(); -- await page.locator("[data-messages-segment-editor] [data-segment-text]").fill("We are being attacked by bats right now."); -- await page.locator("[data-messages-segment-editor] [data-messages-segment-commit]").click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated segment row 1."); -- await expect(page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." })).toBeVisible(); -- -- await page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." }).getByRole("button", { name: "Disable" }).click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled segment row 1."); -- await expect(page.locator("[data-messages-segment-row]").filter({ hasText: "We are being attacked by bats right now." })).toContainText("Inactive"); -- -- const updatedSegmentsResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/segments`); -- const disabledSegment = updatedSegmentsResult.payload.data.segments.find((segment) => segment.segmentText === "We are being attacked by bats right now."); -- expect(disabledSegment).toEqual(expect.objectContaining({ -- active: false, -- displayOrder: 1, -- emotionProfileName: "Urgent", -- messageKey: createdMessage.key, -- messageName: "Forest Warning", -- segmentText: "We are being attacked by bats right now.", -- })); -- -- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning" }).getByRole("button", { name: "Edit" }).click(); -- await page.locator("[data-messages-row-editor] [data-message-name]").fill("Forest Warning Updated"); -- await page.locator("[data-messages-row-editor-details] [data-message-text]").fill("The forest gets darker beyond this point."); -- await page.locator("[data-messages-row-editor] [data-messages-commit]").click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Updated row Forest Warning Updated."); -- await expect(page.locator("[data-messages-table]")).toContainText("Forest Warning Updated"); -- -- await page.locator("[data-messages-row]").filter({ hasText: "Forest Warning Updated" }).getByRole("button", { name: "Disable" }).click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled row Forest Warning Updated."); -- await expect(page.locator("[data-messages-row]").filter({ hasText: "Forest Warning Updated" })).toContainText("Inactive"); -- -- const updateResult = await jsonRequest(`${failures.server.baseUrl}/api/messages/messages/${createdMessage.key}`); -- expect(updateResult.payload.data.message).toEqual(expect.objectContaining({ -- active: false, -- key: createdMessage.key, -- messageText: "The forest gets darker beyond this point.", -- name: "Forest Warning Updated", -- })); -- -- await page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" }).getByRole("button", { name: "Disable" }).click(); -- await expect(page.locator("[data-messages-log]")).toHaveText("Disabled TTS profile Arcade Browser Voice."); -- await expect(page.locator("[data-messages-tts-row]").filter({ hasText: "Arcade Browser Voice" })).toContainText("Inactive"); -- -- for (const url of [ -- `${failures.server.baseUrl}/api/messages/messages/${createdMessage.key}`, -- `${failures.server.baseUrl}/api/messages/segments/${disabledSegment.key}`, -- `${failures.server.baseUrl}/api/messages/tts-profiles/${createdTtsProfile.key}`, -- ]) { -- const deleteResult = await fetch(url, { method: "DELETE" }); -- expect(deleteResult.status).toBe(404); -- } -+ expect(failures.failedRequests).toEqual([]); -+ expect(failures.pageErrors).toEqual([]); -+ expect(failures.consoleErrors).toEqual([]); -+ } finally { -+ await closeMessagesRun(failures, page); -+ await fs.rm(sqlitePath, { force: true }); -+ } -+}); - -- await failures.server.close(); -- const restartedServer = await startRepoServer(); -- failures.server = restartedServer; -- const persistedResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/messages/${createdMessage.key}`); -- expect(persistedResult.response.ok).toBe(true); -- expect(persistedResult.payload.data.message).toEqual(expect.objectContaining({ -- active: false, -- key: createdMessage.key, -- name: "Forest Warning Updated", -- messageText: "The forest gets darker beyond this point.", -- })); -+test("Message Studio shows actionable playback error when audio engine is unavailable", async ({ page }) => { -+ const sqlitePath = messagesDbPath(); -+ await fs.rm(sqlitePath, { force: true }); -+ const failures = await openMessagesPage(page, sqlitePath, { speechAvailable: false }); - -- const persistedSegmentResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/segments/${disabledSegment.key}`); -- expect(persistedSegmentResult.response.ok).toBe(true); -- expect(persistedSegmentResult.payload.data.segment).toEqual(expect.objectContaining({ -- active: false, -- displayOrder: 1, -- key: disabledSegment.key, -- segmentText: "We are being attacked by bats right now.", -- })); -+ try { -+ await addMessage(page, { -+ emotion: "Urgent", -+ name: "Bat Encounter", -+ text: "Bats drop from the rafters.", -+ }); -+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).click(); -+ await addPart(page, { -+ emotion: "Urgent", -+ order: 1, -+ text: "Bats drop from the rafters.", -+ }); - -- const persistedTtsProfileResult = await jsonRequest(`${restartedServer.baseUrl}/api/messages/tts-profiles/${createdTtsProfile.key}`); -- expect(persistedTtsProfileResult.response.ok).toBe(true); -- expect(persistedTtsProfileResult.payload.data.ttsProfile).toEqual(expect.objectContaining({ -- active: false, -- key: createdTtsProfile.key, -- name: "Arcade Browser Voice", -- providerKey: "browser-speech", -- voiceName: "Test Voice", -- })); -+ await page.locator("[data-messages-row]").filter({ hasText: "Bat Encounter" }).getByRole("button", { name: "Play Message" }).click(); -+ await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); -+ await expect(page.locator("[data-messages-validation-errors]")).toContainText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio."); -+ await expect(page.locator("[data-messages-log]")).toHaveText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio."); -+ await expect(page.locator("[data-messages-preview-status]")).toHaveText("Audio engine is unavailable. Use a browser with SpeechSynthesis support and reload Message Studio."); -+ expect(await page.evaluate(() => window.__messagesSpeechCalls)).toEqual([]); - - expect(failures.failedRequests).toEqual([]); - expect(failures.pageErrors).toEqual([]); -diff --git a/toolbox/messages/index.html b/toolbox/messages/index.html -index 2f1945bbe..5df9a0290 100644 ---- a/toolbox/messages/index.html -+++ b/toolbox/messages/index.html -@@ -39,7 +39,7 @@ -
- Row Workflow -
--

Review existing rows, select one to update, or add a row from the Message Rows table.

-+

Review messages, open a message to manage parts, or add a message from the Messages table.

-

Disable rows instead of deleting game text.

-
-
-@@ -48,9 +48,9 @@ -
-
-

Message Studio

--

Message rows define what is said. Emotion profiles and TTS profiles define future delivery settings only.

-+

Messages own game text and ordered parts. Audio playback is delegated to the audio engine.

-
--
0Message Rows
-+
0Messages
-
0Emotion Profiles
-
0TTS Profiles
-
-@@ -58,17 +58,17 @@ -
-
-
Game Text Repository
--

Message Rows

-+

Messages

-
-
-- -+
- - -- -- -- -- -+ -+ - -+ -+ - - - -@@ -78,7 +78,7 @@ -
NamePrimary EmotionSegmentsTagsMessage NameTypeStatusPartsDefault TTS ProfileActions
-
-
-- -+ -
-
-@@ -153,7 +150,7 @@ -
-

Name: None

-

Emotion Profile: None

--

Segment: None

-+

Part: None

-

Status: None

-

Text:

-
No message selected.
-@@ -184,7 +181,7 @@ -
- Future Compatibility -
--

Message segments are stored as ordered text with emotion profiles.

-+

Message parts are stored as ordered text with emotion profiles.

-

This tool stores message text exactly as entered.

-
-
-diff --git a/toolbox/messages/message-tts-service-registry.js b/toolbox/messages/message-tts-service-registry.js -index ce914c832..e5cf310ba 100644 ---- a/toolbox/messages/message-tts-service-registry.js -+++ b/toolbox/messages/message-tts-service-registry.js -@@ -31,7 +31,13 @@ function createMessageStudioTtsServiceRegistry({ - ok: false, - }; - } -- return engine.speak(options); -+ const selectedVoice = String(options?.voice || "").trim() -+ || engine.voiceOptions()[0]?.value -+ || ""; -+ return engine.speak({ -+ ...options, -+ voice: selectedVoice, -+ }); - }, - }; - } -diff --git a/toolbox/messages/messages.js b/toolbox/messages/messages.js -index 7843a8d2c..a2407ba95 100644 ---- a/toolbox/messages/messages.js -+++ b/toolbox/messages/messages.js -@@ -2,7 +2,6 @@ import { - createMessage, - createMessageSegment, - createEmotionProfile, -- createTtsProfile, - listEmotionProfiles, - listMessages, - listMessageSegments, -@@ -10,11 +9,23 @@ import { - updateEmotionProfile, - updateMessage, - updateMessageSegment, -- updateTtsProfile, - } from "./messages-api-client.js"; - import { createMessageStudioTtsServiceRegistry } from "./message-tts-service-registry.js"; - - const NEW_ROW_KEY = "__new__"; -+const DEFAULT_TTS_PROFILE_KEY = "__default-balanced-tts__"; -+const DEFAULT_TTS_PROFILE = Object.freeze({ -+ active: true, -+ description: "Balanced local browser playback option until authored TTS profiles are available.", -+ key: DEFAULT_TTS_PROFILE_KEY, -+ language: "en-US", -+ name: "Default Balanced TTS", -+ pitch: 1, -+ providerKey: "browser-speech", -+ rate: 1, -+ voiceName: "", -+ volume: 1, -+}); - const ttsServiceRegistry = createMessageStudioTtsServiceRegistry(); - - const elements = { -@@ -37,7 +48,6 @@ const elements = { - speechTestTarget: document.querySelector("[data-messages-speech-test-target]"), - table: document.querySelector("[data-messages-table]"), - testSpeech: document.querySelector("[data-messages-test-speech]"), -- ttsAddRow: document.querySelector("[data-messages-tts-add-row]"), - ttsCount: document.querySelector("[data-messages-tts-count]"), - ttsRows: document.querySelector("[data-messages-tts-profiles]"), - ttsService: document.querySelector("[data-messages-tts-service]"), -@@ -49,9 +59,10 @@ const state = { - editingEmotionKey: "", - editingMessageKey: "", - editingSegmentKey: "", -- editingTtsKey: "", - emotionProfiles: [], -+ messageTtsProfileKeys: new Map(), - messages: [], -+ segmentTtsProfileKeys: new Map(), - segments: [], - selectedMessageKey: "", - selectedSegmentKey: "", -@@ -148,6 +159,19 @@ function createSelect(value, dataName, options, placeholder) { - return select; - } - -+function createTtsProfileSelect(value, dataName, identityKey) { -+ const select = createSelect( -+ value || defaultTtsProfileKey(), -+ dataName, -+ activeTtsProfileOptions(), -+ "Select TTS profile", -+ ); -+ if (identityKey) { -+ select.dataset.messagesTtsIdentity = identityKey; ++- Create or link a Game Hub project when a Ready idea uses Create Project. ++- Move created Project ideas into a read-only Idea Board state with Open in Game Hub and Archive actions. ++- Keep Archived ideas hidden by default and restorable/deletable when visible. ++- Productionize Game Hub project display with Project Information and read-only Source Idea fields. ++- Add the smallest handoff/project-contract fix needed to complete the flow. ++ ++## Validation ++ ++- `node --check toolbox/idea-board/index.js` ++- Changed-file syntax checks for Game Hub JavaScript. ++- Targeted Idea Board Playwright. ++- Targeted Game Hub Playwright if existing coverage exists. ++- `npm run test:workspace-v2` ++- Do not run full samples smoke. ++ ++## Reports ++ ++- `docs_build/dev/reports/codex_review.diff` ++- `docs_build/dev/reports/codex_changed_files.txt` ++ ++## Delivery ++ ++- Commit, push, create PR, merge after validation passes, return to `main`, pull latest `main`, and produce `tmp/PR_26171_044-idea-board-game-hub-project-flow_delta.zip`. +diff --git a/src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js b/src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js +index ad5602999..fff42032d 100644 +--- a/src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js ++++ b/src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js +@@ -156,8 +156,37 @@ export const GAME_WORKSPACE_SCHEMA = Object.freeze({ + game_members: Object.freeze(["gameId", "userKey", "permission", "role"]), + }); + ++function normalizeSourceIdea(sourceIdea) { ++ if (!sourceIdea || typeof sourceIdea !== "object") { ++ return null; + } -+ return select; -+} + - function populateSelect(select, options, placeholder) { - if (!select) { - return; -@@ -225,12 +249,35 @@ function emotionProfileByKey(profileKey) { - return state.emotionProfiles.find((profile) => profile.key === profileKey) || null; - } - --function ttsProfileByKey(profileKey) { -- return state.ttsProfiles.find((profile) => profile.key === profileKey) || null; -+function activeTtsProfileOptions() { -+ const activeProfiles = state.ttsProfiles.filter((profile) => profile.active); -+ return activeProfiles.length ? activeProfiles : [DEFAULT_TTS_PROFILE]; -+} ++ const idea = String(sourceIdea.idea || "").trim(); ++ const pitch = String(sourceIdea.pitch || "").trim(); ++ const notes = Array.isArray(sourceIdea.notes) ++ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) ++ : []; + -+function defaultTtsProfileKey() { -+ return activeTtsProfileOptions()[0]?.key || DEFAULT_TTS_PROFILE_KEY; -+} ++ if (!idea && !pitch && !notes.length) { ++ return null; ++ } + -+function ttsProfileOptionByKey(profileKey) { -+ return activeTtsProfileOptions().find((profile) => profile.key === profileKey) -+ || activeTtsProfileOptions()[0] -+ || DEFAULT_TTS_PROFILE; ++ return { idea, pitch, notes }; +} + -+function selectedTtsProfileForMessage(messageKey) { -+ return ttsProfileOptionByKey(state.messageTtsProfileKeys.get(messageKey) || defaultTtsProfileKey()); ++function cloneRow(row) { ++ const cloned = { ...row }; ++ const sourceIdea = normalizeSourceIdea(row.sourceIdea); ++ if (sourceIdea) { ++ cloned.sourceIdea = sourceIdea; ++ } else { ++ delete cloned.sourceIdea; ++ } ++ return cloned; +} + -+function selectedTtsProfileForSegment(segmentKey, messageKey = state.selectedMessageKey) { -+ return ttsProfileOptionByKey( -+ state.segmentTtsProfileKeys.get(segmentKey) -+ || state.messageTtsProfileKeys.get(messageKey) -+ || defaultTtsProfileKey(), -+ ); + function cloneRows(rows) { +- return rows.map((row) => ({ ...row })); ++ return rows.map(cloneRow); } - - function selectedTtsProfile() { -- return ttsProfileByKey(elements.previewTtsProfile?.value || ""); -+ return ttsProfileOptionByKey(elements.previewTtsProfile?.value || defaultTtsProfileKey()); - } - - function selectedTtsService() { -@@ -243,10 +290,11 @@ function selectedSpeechTarget() { + + function cloneTables(tables) { +@@ -227,6 +256,7 @@ export function createGameWorkspaceMockRepository() { return { - emotionProfile: emotionProfileByKey(segment.emotionProfileKey), - id: segment.key, -- label: `Segment ${segment.displayOrder}`, -- name: `${segment.messageName || "Message"} segment ${segment.displayOrder}`, -+ label: `Part ${segment.displayOrder}`, -+ name: `${segment.messageName || "Message"} part ${segment.displayOrder}`, -+ profile: selectedTtsProfileForSegment(segment.key, segment.messageKey), - text: segment.segmentText, -- type: "segment", -+ type: "part", + ...game, + purpose: game.purpose || "Game", ++ sourceIdea: normalizeSourceIdea(game.sourceIdea), + ownerDisplayName: owner?.displayName || game.ownerKey, + members: getGameMembers(game.id), }; - } - const message = selectedMessage(); -@@ -256,8 +304,9 @@ function selectedSpeechTarget() { - return { - emotionProfile: emotionProfileByKey(message.emotionProfileKey), - id: message.key, -- label: `Message Row: ${message.name}`, -+ label: `Message: ${message.name}`, - name: message.name, -+ profile: selectedTtsProfileForMessage(message.key), - text: message.messageText, - type: "message", - }; -@@ -293,7 +342,7 @@ function selectOptionsWithCurrent(currentKey) { - function renderCounts() { - setText(elements.count, String(state.messages.length)); - setText(elements.emotionCount, String(state.emotionProfiles.length)); -- setText(elements.ttsCount, String(state.ttsProfiles.length)); -+ setText(elements.ttsCount, String(activeTtsProfileOptions().length)); - } - - function renderPersistence(persistence = {}) { -@@ -330,7 +379,7 @@ function renderTtsServiceOptions() { - } - - function renderTtsProfileOptions() { -- const activeProfiles = state.ttsProfiles.filter((profile) => profile.active); -+ const activeProfiles = activeTtsProfileOptions(); - populateSelect(elements.previewTtsProfile, activeProfiles, "Select TTS profile"); - const selected = selectedTtsProfile(); - if (!selected && activeProfiles[0]) { -@@ -361,9 +410,6 @@ function speechTestReadiness() { - if (!profile) { - return { message: "Select an active TTS profile before testing speech.", ok: false }; - } -- if (!String(profile.voiceName || "").trim()) { -- return { message: "Select a TTS profile with a voice before testing speech.", ok: false }; -- } - if (!target.emotionProfile) { - return { message: "Selected item needs an emotion profile before testing speech.", ok: false }; - } -@@ -393,22 +439,27 @@ function createMessageEditRows(message = null) { - const nameCell = document.createElement("td"); - nameCell.append(createInput(message?.name || "", "messageName")); - -- const emotionCell = document.createElement("td"); -- emotionCell.append(createSelect(message?.emotionProfileKey || "", "messageEmotion", selectOptionsWithCurrent(message?.emotionProfileKey || ""), "Select emotion profile")); -- -- const segmentCell = createCell(message ? String(messageSegments(message.key).length) : "0"); -- const tagsCell = createCell("Tags planned"); -+ const typeCell = createCell(message?.categoryName || "Dialog"); - - const statusCell = document.createElement("td"); - statusCell.append(createCheckbox(message?.active !== false, "messageActive")); - -+ const partCell = createCell(message ? String(messageSegments(message.key).length) : "0"); +@@ -373,6 +403,10 @@ export function createGameWorkspaceMockRepository() { + purpose, + status, + }; ++ const sourceIdea = normalizeSourceIdea(input.sourceIdea); ++ if (sourceIdea) { ++ game.sourceIdea = sourceIdea; ++ } + + tables.games.push(game); + tables.game_members.push({ +diff --git a/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs b/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs +index 1787b86d8..1c1f742a9 100644 +--- a/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs ++++ b/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs +@@ -15,6 +15,14 @@ const SUPABASE_ENV_KEYS = Object.freeze([ + let fakeSupabaseServer; + let previousSupabaseEnv; + ++function restoreEnvValue(key, value) { ++ if (value === undefined) { ++ delete process.env[key]; ++ return; ++ } ++ process.env[key] = value; ++} + -+ const ttsCell = document.createElement("td"); -+ ttsCell.append(createTtsProfileSelect( -+ message ? selectedTtsProfileForMessage(message.key).key : defaultTtsProfileKey(), -+ "messageDefaultTtsProfile", -+ key, -+ )); + function startFakeSupabaseServer() { + const tables = { + roles: [], +@@ -123,6 +131,16 @@ test.afterAll(async () => { + + async function openRepoPage(page, pathName, options = {}) { + const server = await startRepoServer(); ++ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; ++ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; ++ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; ++ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; ++ const closeServer = server.close.bind(server); ++ server.close = async () => { ++ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); ++ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); ++ await closeServer(); ++ }; + const failedRequests = []; + const pageErrors = []; + const consoleErrors = []; +@@ -235,12 +253,10 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await expect(page.getByRole("button", { name: "Create Game" })).toBeEnabled(); + 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("[data-project-record-status]")).toContainText("Game Hub records loaded from the project records service"); +- await expect(page.locator("[data-project-record-status]")).toContainText("authoritative keys managed by service"); +- await expect(page.locator("[data-project-record-status]")).toContainText("asset references linked to storage object keys: 0"); ++ await expect(page.locator("[data-project-record-status]")).toHaveText("Project Information loaded."); ++ await expect(page.locator("[data-game-project-information]")).toContainText("Project Information"); + await expect(page.locator("[data-project-records-table]")).toContainText("Demo Game"); +- await expect(page.locator("[data-project-records-table]")).toContainText("Project records service"); +- await expect(page.locator("[data-project-records-table]")).toContainText("asset refs 0"); ++ await expect(page.locator("[data-source-idea-section]")).toContainText("No source idea yet"); + await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); + await expect(page.locator("[data-active-game-purpose]")).toHaveText("Game"); + await expect(page.locator("[data-current-user-role]")).toHaveText("Owner"); +@@ -257,7 +273,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { + await page.getByRole("button", { name: "Create Game" }).click(); + await expect(page.locator("[data-active-game-name]")).toHaveText("Launch Test Game"); + await expect(page.locator("[data-game-list]")).toContainText("Launch Test Game"); +- await expect(page.locator("[data-project-records-table]")).toContainText("Launch Test Game"); ++ await expect(page.locator("[data-game-project-information]")).toContainText("Launch Test Game"); + await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Open Launch Test Game (Active)" })).toHaveClass(/primary/); + await expect(page.locator("[data-game-workspace-log]")).toHaveText("Created and opened Launch Test Game."); + +@@ -287,11 +303,8 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page } + try { + await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); + await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo"); +- await expect(page.locator("[data-project-record-status]")).toContainText("guest browsing enabled; guest saving blocked"); +- await expect(page.locator("[data-project-record-status]")).toContainText("project records service"); +- await expect(page.locator("[data-project-record-status]")).toContainText("asset references linked to storage object keys: 0"); ++ await expect(page.locator("[data-project-record-status]")).toHaveText("Project Information loaded. Sign in to save changes."); + await expect(page.locator("[data-project-records-table]")).toContainText("Demo Game"); +- await expect(page.locator("[data-project-records-table]")).toContainText("Project records service"); + await expect(page.getByRole("button", { name: "Create Game" })).toBeDisabled(); + await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeDisabled(); + await expect(page.getByLabel("Game Name")).toBeDisabled(); +@@ -301,7 +314,7 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page } + + await page.getByRole("button", { name: "Open Gravity Demo" }).click(); + await expect(page.locator("[data-active-game-name]")).toHaveText("Gravity Demo"); +- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Guest browsing enabled; sign in required to save Game Hub project records."); ++ await expect(page.locator("[data-game-workspace-log]")).toHaveText("Sign in to create or update Game Hub projects."); + + await expectNoPageFailures(failures); + } finally { +@@ -309,7 +322,7 @@ test("Game Hub preserves guest browsing and blocks guest saves", async ({ page } + } + }); + +-test("Game Hub shows active-game API diagnostics without throwing", async ({ page }) => { ++test("Game Hub shows active-game errors without throwing", async ({ page }) => { + await page.route("**/api/toolbox/game-workspace/repositories/*/methods/getActiveGame", async (route) => { + await route.fulfill({ + body: JSON.stringify({ +@@ -326,7 +339,7 @@ test("Game Hub shows active-game API diagnostics without throwing", async ({ pag + try { + expect(failures.failedRequests.some((request) => request.includes("502") && request.includes("/methods/getActiveGame"))).toBe(true); + await expect(page.locator("[data-active-game-name]")).toHaveText("No game open"); +- await expect(page.locator("[data-game-workspace-log]")).toContainText("Active game unavailable for validation."); ++ await expect(page.locator("[data-game-workspace-log]")).toContainText("Active game is temporarily unavailable."); + expect(failures.pageErrors).toEqual([]); + expect(failures.consoleErrors.filter((message) => !message.includes("status of 502"))).toEqual([]); + } finally { +@@ -356,7 +369,7 @@ test("Game Hub reports malformed active-game payloads without throwing", async ( + try { + await expect(page.locator("[data-active-game-name]")).toHaveText("No game open"); + await expect(page.locator("[data-current-user-role]")).toHaveText("Viewer"); +- await expect(page.locator("[data-game-workspace-log]")).toContainText("Active game response is malformed."); ++ await expect(page.locator("[data-game-workspace-log]")).toContainText("Active game is temporarily unavailable."); + await expect(page.getByLabel("Game Purpose")).toBeDisabled(); + + await expectNoPageFailures(failures); +@@ -395,7 +408,6 @@ test("Game Hub displays and edits game purpose and member role", async ({ page } + await expect(page.getByLabel("Game Purpose")).toHaveValue("Game"); + await expect(page.getByLabel("Game Status")).toHaveValue("Under Construction"); + await expect(page.getByLabel("Current User Role")).toHaveValue("Owner"); +- await expect(page.locator("[data-game-members-table]")).toContainText("Owner"); + + await page.getByLabel("Game Purpose").selectOption("Learning Game"); + await expect(page.locator("[data-active-game-purpose]")).toHaveText("Learning Game"); +@@ -407,7 +419,6 @@ test("Game Hub displays and edits game purpose and member role", async ({ page } + + await page.getByLabel("Current User Role").selectOption("Designer"); + await expect(page.locator("[data-current-user-role]")).toHaveText("Designer"); +- await expect(page.locator("[data-game-members-table]")).toContainText("Designer"); + await expect(page.locator("[data-game-workspace-log]")).toHaveText("Updated current user role to Designer."); + + await page.getByLabel("Game Purpose").selectOption("Capability Demo"); +@@ -435,23 +446,22 @@ test("Game Hub progress panels update from mock game state", async ({ page }) => + await expect(page.locator("[data-recommended-next-tool]").first()).toHaveText("Game Configuration"); + await expect(page.locator("[data-game-progress-checklist]")).toContainText("Game identity: Complete"); + await expect(page.locator("[data-game-output-panels] summary")).toHaveText([ +- "Readiness Output", +- "Repository Tables", +- "Team Members" ++ "Readiness Output" + ]); + await expect(page.locator("aside.tool-column").last().getByText("Readiness Output")).toHaveCount(0); +- await expect(page.locator("aside.tool-column").last().getByText("Repository Tables")).toHaveCount(0); +- await expect(page.locator("aside.tool-column").last().getByText("Team Members")).toHaveCount(0); + const panelOrderIsCorrect = await page.locator(".tool-center-panel").evaluate((panel) => { ++ const projectInformation = panel.querySelector("[data-game-project-information]"); ++ const sourceIdea = panel.querySelector("[data-source-idea-section]"); + const staticOverlay = panel.querySelector("[data-game-workspace-foundation]"); + const outputPanels = panel.querySelector("[data-game-output-panels]"); +- const missingRequirements = panel.querySelector("[data-missing-requirements]"); + return Boolean( ++ projectInformation && ++ sourceIdea && + staticOverlay && + outputPanels && +- missingRequirements && +- (staticOverlay.compareDocumentPosition(outputPanels) & Node.DOCUMENT_POSITION_FOLLOWING) && +- (outputPanels.compareDocumentPosition(missingRequirements) & Node.DOCUMENT_POSITION_FOLLOWING) ++ (projectInformation.compareDocumentPosition(sourceIdea) & Node.DOCUMENT_POSITION_FOLLOWING) && ++ (sourceIdea.compareDocumentPosition(staticOverlay) & Node.DOCUMENT_POSITION_FOLLOWING) && ++ (staticOverlay.compareDocumentPosition(outputPanels) & Node.DOCUMENT_POSITION_FOLLOWING) + ); + }); + expect(panelOrderIsCorrect).toBe(true); +@@ -460,9 +470,7 @@ test("Game Hub progress panels update from mock game state", async ({ page }) => + await page.getByRole("button", { name: "Create Game" }).click(); + await expect(page.locator("[data-game-status]")).toHaveText("Under Construction"); + await expect(page.locator("[data-game-progress]")).toHaveText("Progress Review Game identity ready"); +- await expect(page.locator("[data-table-counts], [data-game-table-counts]")).toContainText("games"); +- await expect(page.locator("[data-game-table-counts]")).toContainText("5"); +- await expect(page.locator("[data-game-members-table]")).toContainText("Owner"); ++ await expect(page.locator("[data-game-project-information]")).toContainText("Progress Review Game"); + + await page.getByRole("button", { name: "Delete Open Game" }).click(); + await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); +diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +index 1a42e53d7..a18205fa4 100644 +--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs ++++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +@@ -1,4 +1,5 @@ + import { expect, test } from "@playwright/test"; ++import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; + import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs"; + import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; + +@@ -114,8 +115,18 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + const server = await startRepoServer(); + 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 failedRequests = []; + const pageErrors = []; + const consoleErrors = []; +@@ -139,6 +150,32 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + }); + + 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, ++ }), ++ }); ++ }); ++ await page.route("**/api/toolbox/registry/snapshot", async (route) => { ++ await route.fulfill({ ++ contentType: "application/json", ++ body: JSON.stringify({ ++ data: { ++ activeTools: [], ++ readinessByStatus: {}, ++ tools: [], ++ toolboxContract: {}, ++ }, ++ ok: true, ++ }), ++ }); ++ }); ++ await page.request.post(`${server.baseUrl}/api/session/user`, { ++ data: { userKey: MOCK_DB_KEYS.users.user1 }, ++ }); + await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); + await expect(page.getByRole("heading", { level: 1, name: "Idea Board" })).toBeVisible(); + await expectProductionCopy(page); +@@ -278,6 +315,13 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-idea-board-notes-count='lantern-reef']")).toHaveText("0 Notes"); + await expect(page.locator("[data-idea-board-expanded-row='lantern-reef']")).toHaveCount(0); + ++ await page.locator("[data-idea-board-idea-cell='lantern-reef']").click(); ++ await expect(page.locator("[data-idea-board-expanded-row='lantern-reef']")).toBeVisible(); ++ await page.locator("[data-idea-board-add-note='lantern-reef']").click(); ++ await page.locator("[data-idea-board-note-input]").fill("Use dusk tide changes as the first Game Hub planning note."); ++ await page.locator("[data-idea-board-note-action='save']").click(); ++ await expect(page.locator("[data-idea-board-notes-count='lantern-reef']")).toHaveText("1 Note"); + - const actions = document.createElement("td"); - actions.append(createActionGroup( -- createButton("Update Row", "messagesCommit", key), -+ createButton("Save", "messagesCommit", key), - createButton("Cancel", "messagesCancel", key), - )); - -- row.append(nameCell, emotionCell, segmentCell, tagsCell, statusCell, actions); -+ row.append(nameCell, typeCell, statusCell, partCell, ttsCell, actions); - - const detailRow = document.createElement("tr"); - detailRow.dataset.messagesRowEditorDetails = key; -@@ -417,6 +468,7 @@ function createMessageEditRows(message = null) { - const stack = document.createElement("div"); - stack.className = "content-stack"; - stack.append( -+ createField("Primary Emotion", createSelect(message?.emotionProfileKey || "", "messageEmotion", selectOptionsWithCurrent(message?.emotionProfileKey || ""), "Select emotion profile")), - createField("Message Text", createTextarea(message?.messageText || "", "messageText", 6)), - createField("Notes", createTextarea(message?.notes || "", "messageNotes", 3)), - ); -@@ -432,19 +484,19 @@ function createMessageSegmentTable() { - - const context = document.createElement("div"); - context.className = "kicker"; -- context.textContent = "Message Row / Segment Table"; -+ context.textContent = "Message / Message Parts"; - const heading = document.createElement("h3"); -- heading.textContent = "Message Segments"; -+ heading.textContent = "Message Parts"; - wrapper.append(context, heading); - - const tableWrapper = document.createElement("div"); - tableWrapper.className = "table-wrapper"; - const table = document.createElement("table"); - table.className = "data-table"; -- table.setAttribute("aria-label", "Selected message segments"); -+ table.setAttribute("aria-label", "Message parts"); - const thead = document.createElement("thead"); - const headerRow = document.createElement("tr"); -- ["Order", "Emotion", "Text", "Status", "Actions"].forEach((label) => { -+ ["Order", "Text", "Emotion", "TTS Profile", "Status", "Actions"].forEach((label) => { - const header = document.createElement("th"); - header.scope = "col"; - header.textContent = label; -@@ -456,7 +508,7 @@ function createMessageSegmentTable() { - - const segments = selectedMessageSegments(); - if (!segments.length && state.editingSegmentKey !== NEW_ROW_KEY) { -- tbody.append(tableMessage(5, "No segments saved for this message.")); -+ tbody.append(tableMessage(6, "No message parts saved for this message.")); + await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='edit']").click(); + await expect(page.locator("[data-idea-board-idea-input-row] [data-idea-board-idea-action]")).toHaveText(["Save", "Cancel"]); + await expect(page.locator("[data-idea-board-idea-status-input]")).toHaveCount(1); +@@ -287,10 +331,11 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); + await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='create-project']").click(); + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Project"); +- await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Open Project", "Archive"]); ++ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]); ++ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='edit']")).toHaveCount(0); + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); +- await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='open-project']").click(); +- await expect(page.locator("[data-idea-board-status]")).toHaveText("Opening Lantern Reef."); ++ await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0); ++ await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0); + await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click(); + await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0); + await page.locator("[data-idea-board-status-filter-option][value='Archived']").check(); +@@ -299,23 +344,25 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Restore", "Delete"]); + await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='restore']").click(); + await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Project"); +- await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click(); +- await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Restore", "Delete"]); +- await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']").click(); +- await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0); +- await page.locator("[data-idea-board-filter-clear-all]").click(); +- await expect(page.locator("[data-idea-board-idea-row]")).toHaveCount(0); +- await expect(page.locator("[data-idea-board-add-idea]")).toBeVisible(); +- await page.locator("[data-idea-board-filter-select-all]").click(); +- await expect(page.locator("[data-idea-board-idea-row]")).toHaveCount(3); ++ await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]); ++ await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='open-project']").click(); ++ await page.waitForURL(/\/toolbox\/game-workspace\/index\.html\?game=lantern-reef-\d+$/); ++ await expect(page.getByRole("heading", { level: 1, name: "Game Hub" })).toBeVisible(); ++ await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef"); ++ await expect(page.locator("[data-source-idea-display]")).toHaveText("Lantern Reef"); ++ await expect(page.locator("[data-source-idea-pitch]")).toHaveText("Guide light through a reef that rearranges at dusk."); ++ await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note."); ++ await expect(page.locator("main")).not.toContainText(/\bproject records\b|\bAPI\b|\bDB\b|\bmock\b|\bseed\b|\bdebug\b|\binternal\b/i); + +- expect(mutatingApiRequests).toEqual([]); ++ expect(mutatingApiRequests.some((request) => request.includes("/api/toolbox/game-workspace/repositories"))).toBe(true); ++ expect(mutatingApiRequests.some((request) => request.includes("/methods/createGame"))).toBe(true); + expect(failedRequests).toEqual([]); + expect(pageErrors).toEqual([]); + expect(consoleErrors).toEqual([]); + } finally { + restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); + restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); ++ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); + await server.close(); } - - segments.forEach((segment, index) => { -@@ -472,15 +524,23 @@ function createMessageSegmentTable() { - moveUp.disabled = index === 0; - moveDown.disabled = index === segments.length - 1; - actions.append(createActionGroup( -- createButton("Edit", "messagesSegmentEdit", segment.key), -+ createButton("Play Part", "messagesSegmentPlay", segment.key), -+ createButton("Edit Part", "messagesSegmentEdit", segment.key), - moveUp, - moveDown, - segment.active ? createButton("Disable", "messagesSegmentDisable", segment.key) : null, - )); -+ const ttsCell = document.createElement("td"); -+ ttsCell.append(createTtsProfileSelect( -+ selectedTtsProfileForSegment(segment.key, segment.messageKey).key, -+ "segmentTtsProfile", -+ segment.key, -+ )); - row.append( - createCell(String(segment.displayOrder)), -- createCell(segment.emotionProfileName || "Unknown"), - createCell(segment.segmentText), -+ createCell(segment.emotionProfileName || "Unknown"), -+ ttsCell, - createCell(statusForActive(segment.active)), - actions, - ); -@@ -495,7 +555,7 @@ function createMessageSegmentTable() { - tableWrapper.append(table); - const actionGroup = document.createElement("div"); - actionGroup.className = "action-group"; -- actionGroup.append(createButton("Add Segment Row", "messagesSegmentAddRow", state.selectedMessageKey)); -+ actionGroup.append(createButton("Add Part", "messagesSegmentAddRow", state.selectedMessageKey)); - wrapper.append(tableWrapper, actionGroup); - return wrapper; + }); +diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs +index b2d410c0f..bfe555c39 100644 +--- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs ++++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs +@@ -366,10 +366,6 @@ test("Idea Board launches from Toolbox with accordion table notes model", async + 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='launch-tile'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); +- await page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action='create-project']").click(); +- await expect(page.locator("[data-idea-board-idea-row='launch-tile'] td").nth(1)).toHaveText("Project"); +- await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Open Project", "Archive"]); +- await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action='delete']")).toHaveCount(0); + expect(mutatingApiRequests).toEqual([]); + + expect(failedRequests).toEqual([]); +diff --git a/toolbox/game-workspace/game-workspace.js b/toolbox/game-workspace/game-workspace.js +index d57ed37ef..90aea08f8 100644 +--- a/toolbox/game-workspace/game-workspace.js ++++ b/toolbox/game-workspace/game-workspace.js +@@ -3,12 +3,10 @@ import { + GAME_WORKSPACE_GAME_PURPOSES, + GAME_WORKSPACE_GAME_STATUSES, + createGameWorkspaceApiRepository, +- readProjectWorkspaceProjectRecords, + } from "./game-workspace-api-client.js"; + import { getSessionCurrent } from "../../src/api/session-api-client.js"; + + const repository = createGameWorkspaceApiRepository(); +-let projectRecordContract = null; + + const elements = { + activeGameName: document.querySelector("[data-active-game-name]"), +@@ -29,6 +27,10 @@ const elements = { + projectRecordStatus: document.querySelector("[data-project-record-status]"), + projectRecordsTable: document.querySelector("[data-project-records-table]"), + purposeInput: document.querySelector("[data-game-purpose-input]"), ++ sourceIdeaDisplay: document.querySelector("[data-source-idea-display]"), ++ sourceIdeaName: document.querySelector("[data-source-idea-name]"), ++ sourceIdeaNotes: document.querySelector("[data-source-idea-notes]"), ++ sourceIdeaPitch: document.querySelector("[data-source-idea-pitch]"), + gameStatus: document.querySelector("[data-game-status]"), + gameStatusInput: document.querySelector("[data-game-status-input]"), + publishingProgress: document.querySelector("[data-publishing-progress]"), +@@ -63,7 +65,7 @@ function isRepositoryErrorResult(value) { } -@@ -508,22 +568,29 @@ function createSegmentEditRow(segment = null) { - const orderCell = document.createElement("td"); - orderCell.append(createNumberInput(segment?.displayOrder || nextSegmentOrder(), "segmentOrder", { min: 1, step: 1 })); - -+ const textCell = document.createElement("td"); -+ textCell.append(createTextarea(segment?.segmentText || "", "segmentText", 3)); -+ - const emotionCell = document.createElement("td"); - emotionCell.append(createSelect(segment?.emotionProfileKey || "", "segmentEmotion", selectOptionsWithCurrent(segment?.emotionProfileKey || ""), "Select emotion")); - -- const textCell = document.createElement("td"); -- textCell.append(createTextarea(segment?.segmentText || "", "segmentText", 3)); -+ const ttsCell = document.createElement("td"); -+ ttsCell.append(createTtsProfileSelect( -+ segment ? selectedTtsProfileForSegment(segment.key, segment.messageKey).key : selectedTtsProfileForMessage(state.selectedMessageKey).key, -+ "segmentTtsProfile", -+ key, -+ )); - - const statusCell = document.createElement("td"); - statusCell.append(createCheckbox(segment?.active !== false, "segmentActive")); - - const actions = document.createElement("td"); - actions.append(createActionGroup( -- createButton("Update Row", "messagesSegmentCommit", key), -+ createButton("Save", "messagesSegmentCommit", key), - createButton("Cancel", "messagesSegmentCancel", key), - )); - -- row.append(orderCell, emotionCell, textCell, statusCell, actions); -+ row.append(orderCell, textCell, emotionCell, ttsCell, statusCell, actions); - return row; + + function repositoryErrorMessage(value, context) { +- return String(value?.message || value?.error || `${context} failed through the server API contract.`).trim(); ++ return `${context} is temporarily unavailable. Refresh the page or try again shortly.`; } - -@@ -546,7 +613,7 @@ function renderMessageRows() { + + function reportRepositoryError(value, context) { +@@ -86,7 +88,7 @@ function normalizeActiveGame(value, context = "Active game") { + return null; } - elements.table.replaceChildren(); - if (!state.messages.length && state.editingMessageKey !== NEW_ROW_KEY) { -- elements.table.append(tableMessage(6, "No message rows saved yet.")); -+ elements.table.append(tableMessage(6, "No messages saved yet. Add a message such as Bat Encounter.")); - return; + if (!isRecord(value) || !Array.isArray(value.members)) { +- setStatusLog(`${context} response is malformed. Reload Game Hub after the server API contract is restored.`); ++ setStatusLog(`${context} is temporarily unavailable. Refresh the page or try again shortly.`); + return null; } - -@@ -559,17 +626,28 @@ function renderMessageRows() { - - const row = document.createElement("tr"); - row.dataset.messagesRow = message.key; -+ const nameCell = document.createElement("td"); -+ const isExpanded = state.selectedMessageKey === message.key; -+ nameCell.dataset.messagesNameCell = message.key; -+ nameCell.textContent = `${isExpanded ? "v" : ">"} ${message.name}`; -+ const ttsCell = document.createElement("td"); -+ ttsCell.append(createTtsProfileSelect( -+ selectedTtsProfileForMessage(message.key).key, -+ "messageDefaultTtsProfile", -+ message.key, -+ )); - const actions = document.createElement("td"); - actions.append(createActionGroup( -- createButton("Edit", "messagesEdit", message.key), -+ createButton("Play Message", "messagesPlay", message.key), -+ createButton("Edit Message", "messagesEdit", message.key), - message.active ? createButton("Disable", "messagesDisable", message.key) : null, - )); - row.append( -- createCell(message.name), -- createCell(message.emotionProfileName || "Unknown"), -- createCell(String(messageSegments(message.key).length)), -- createCell("Tags planned"), -+ nameCell, -+ createCell(message.categoryName || "Dialog"), - createCell(statusForActive(message.active)), -+ createCell(String(messageSegments(message.key).length)), -+ ttsCell, - actions, - ); - elements.table.append(row); -@@ -640,63 +718,24 @@ function renderEmotionRows() { + return value; +@@ -96,23 +98,23 @@ function normalizeProgress(value) { + if (reportRepositoryError(value, "Game progress")) { + return { + gameStatus: "No Game", +- gameProgress: "Blocked by server API error", +- publishingProgress: "Blocked", +- currentFocus: "Resolve the server API diagnostic", ++ gameProgress: "Progress is temporarily unavailable", ++ publishingProgress: "Unavailable", ++ currentFocus: "Refresh Game Hub", + recommendedNextTool: "Game Hub", + progressChecklist: [ +- { label: "Restore server API contract", status: "Blocked" }, ++ { label: "Project information", status: "Unavailable" }, + ], + }; } - } - --function createTtsEditRow(profile = null) { -- const key = profile?.key || NEW_ROW_KEY; -- const row = document.createElement("tr"); -- row.dataset.messagesTtsEditor = key; -- -- const nameCell = document.createElement("td"); -- nameCell.append(createInput(profile?.name || "", "ttsName")); -- const providerCell = document.createElement("td"); -- providerCell.append(createInput(profile?.providerKey || "browser-speech", "ttsProvider")); -- const voiceCell = document.createElement("td"); -- voiceCell.append(createInput(profile?.voiceName || "", "ttsVoice")); -- const languageCell = document.createElement("td"); -- languageCell.append(createInput(profile?.language || "en-US", "ttsLanguage")); -- const statusCell = document.createElement("td"); -- statusCell.append(createCheckbox(profile?.active !== false, "ttsActive")); -- const actions = document.createElement("td"); -- actions.append(createActionGroup( -- createButton("Update Row", "messagesTtsCommit", key), -- createButton("Cancel", "messagesTtsCancel", key), -- )); -- row.append(nameCell, providerCell, voiceCell, languageCell, statusCell, actions); -- return row; --} -- - function renderTtsRows() { - if (!elements.ttsRows) { - return; + if (!isRecord(value)) { +- setStatusLog("Game progress response is malformed. Reload Game Hub after the server API contract is restored."); ++ setStatusLog("Game progress is temporarily unavailable. Refresh the page or try again shortly."); } - elements.ttsRows.replaceChildren(); -- state.ttsProfiles.forEach((profile) => { -- if (state.editingTtsKey === profile.key) { -- elements.ttsRows.append(createTtsEditRow(profile)); -- return; -- } -+ activeTtsProfileOptions().forEach((profile) => { - const row = document.createElement("tr"); - row.dataset.messagesTtsRow = profile.key; -- const actions = document.createElement("td"); -- actions.append(createActionGroup( -- createButton("Edit", "messagesTtsEdit", profile.key), -- profile.active ? createButton("Disable", "messagesTtsDisable", profile.key) : null, -- )); - row.append( - createCell(profile.name), - createCell(profile.providerKey), - createCell(profile.voiceName || ""), - createCell(profile.language), - createCell(statusForActive(profile.active)), -- actions, -+ createCell(profile.key === DEFAULT_TTS_PROFILE_KEY ? "Default option" : "Owned by TTS Studio"), - ); - elements.ttsRows.append(row); - }); -- if (state.editingTtsKey === NEW_ROW_KEY) { -- elements.ttsRows.append(createTtsEditRow(null)); -- } -- if (!state.ttsProfiles.length && state.editingTtsKey !== NEW_ROW_KEY) { -- elements.ttsRows.append(tableMessage(6, "No TTS profiles saved yet.")); -- } - } - - function render(persistence = {}) { -@@ -722,7 +761,7 @@ function messageValues(key) { - const details = elements.table?.querySelector(`[data-messages-row-editor-details="${key}"]`); - return { - active: editorChecked(root, "[data-message-active]"), -- emotionProfileKey: editorValue(root, "[data-message-emotion]"), -+ emotionProfileKey: editorValue(details, "[data-message-emotion]"), - messageText: editorValue(details, "[data-message-text]"), - name: editorValue(root, "[data-message-name]"), - notes: editorValue(details, "[data-message-notes]"), -@@ -757,10 +796,10 @@ function segmentValues(key) { - function validateSegment(values) { - const errors = []; - if (!state.selectedMessageKey) { -- errors.push("Select a message row before adding segments."); -+ errors.push("Select a message before adding parts."); + return isRecord(value) ? value : { + gameStatus: "No Game", + gameProgress: "No active game", + publishingProgress: "Not started", +- currentFocus: "Create or seed a game", ++ currentFocus: "Create a game", + recommendedNextTool: "Game Hub", + progressChecklist: [], + }; +@@ -151,8 +153,8 @@ function refreshSaveControls() { } - if (!values.segmentText.trim()) { -- errors.push("Segment Text is required."); -+ errors.push("Part Text is required."); + if (!saveAllowed) { + const currentStatus = String(elements.statusLog?.textContent || ""); +- if (!/Blocked|failed|malformed|Restore|Sign in required|unavailable/i.test(currentStatus)) { +- setStatusLog("Guest browsing enabled; sign in required to save Game Hub project records."); ++ if (!/failed|Sign in required|unavailable/i.test(currentStatus)) { ++ setStatusLog("Sign in to create or update Game Hub projects."); + } } - if (!values.emotionProfileKey) { - errors.push("Emotion Profile is required."); -@@ -795,36 +834,6 @@ function validateEmotion(values) { - return values.name.trim() ? [] : ["Emotion Profile Name is required."]; } - --function ttsValues(key) { -- const root = elements.ttsRows?.querySelector(`[data-messages-tts-editor="${key}"]`); -- const existing = key === NEW_ROW_KEY ? null : ttsProfileByKey(key); -- return { -- active: editorChecked(root, "[data-tts-active]"), -- description: existing?.description || "", -- language: editorValue(root, "[data-tts-language]"), -- name: editorValue(root, "[data-tts-name]"), -- pitch: existing?.pitch ?? 1, -- providerKey: editorValue(root, "[data-tts-provider]"), -- rate: existing?.rate ?? 1, -- voiceName: editorValue(root, "[data-tts-voice]"), -- volume: existing?.volume ?? 1, -- }; --} -- --function validateTts(values) { -- const errors = []; -- if (!values.name.trim()) { -- errors.push("TTS Profile Name is required."); -- } -- if (!values.providerKey.trim()) { -- errors.push("Provider is required."); -- } -- if (!values.language.trim()) { -- errors.push("Language is required."); -- } -- return errors; --} -- - async function loadAll() { - const emotionPayload = listEmotionProfiles(); - const ttsPayload = listTtsProfiles(); -@@ -893,10 +902,10 @@ async function commitSegment(key) { - state.editingSegmentKey = ""; - state.selectedSegmentKey = result.segment.key; - await reloadAfterChange(state.selectedMessageKey, result.segment.key); -- setText(elements.log, `Updated segment row ${result.segment.displayOrder}.`); -+ setText(elements.log, `Updated message part ${result.segment.displayOrder}.`); - } catch (error) { - showValidation([error instanceof Error ? error.message : String(error || "Segment row update failed.")]); -- setText(elements.log, "Segment row update failed."); -+ setText(elements.log, "Message part update failed."); +@@ -161,7 +163,7 @@ function ensureProjectRecordsSaveAllowed(action) { + if (projectRecordsSaveAllowed()) { + return true; } +- const message = `Sign in required to ${action} Game Hub project records.`; ++ const message = `Sign in required to ${action} Game Hub projects.`; + setStatusLog(message); + setProjectRecordStatus(message); + refreshSaveControls(); +@@ -209,53 +211,31 @@ function createGameButton(game, isActive) { + return button; } - -@@ -922,28 +931,6 @@ async function commitEmotion(key) { + +-function renderProjectRecords() { ++function renderProjectInformation(activeGame, currentMember, progress) { + if (!elements.projectRecordsTable) { + return; } - } - --async function commitTts(key) { -- const values = ttsValues(key); -- const errors = validateTts(values); -- if (errors.length) { -- showValidation(errors); -- setText(elements.log, "TTS profile update blocked by validation."); -- return; -- } -- clearValidation(); + - try { -- const result = key === NEW_ROW_KEY -- ? createTtsProfile(values) -- : updateTtsProfile(key, values); -- state.editingTtsKey = ""; -- await reloadAfterChange(); -- setText(elements.log, `Updated TTS profile ${result.ttsProfile.name}.`); +- projectRecordContract = readProjectWorkspaceProjectRecords(); - } catch (error) { -- showValidation([error instanceof Error ? error.message : String(error || "TTS profile update failed.")]); -- setText(elements.log, "TTS profile update failed."); +- projectRecordContract = null; +- setProjectRecordStatus(error instanceof Error ? error.message : "Game Hub project records are unavailable."); +- return; - } --} - - async function disableMessage(key) { - const message = state.messages.find((candidate) => candidate.key === key); - if (!message) { -@@ -981,10 +968,10 @@ async function disableSegment(key) { - }); - state.selectedSegmentKey = result.segment.key; - await reloadAfterChange(segment.messageKey, result.segment.key); -- setText(elements.log, `Disabled segment row ${result.segment.displayOrder}.`); -+ setText(elements.log, `Disabled message part ${result.segment.displayOrder}.`); - } catch (error) { - showValidation([error instanceof Error ? error.message : String(error || "Segment status update failed.")]); -- setText(elements.log, "Segment status update failed."); -+ setText(elements.log, "Message part status update failed."); - } +- const records = Array.isArray(projectRecordContract.records) ? projectRecordContract.records : []; +- const source = projectRecordContract.sourceLabel || "Project records service"; +- const assetReferenceCount = Number(projectRecordContract.assetReferenceCount || 0); +- const saveMode = projectRecordsSaveAllowed() +- ? "signed-in saves enabled" +- : "guest browsing enabled; guest saving blocked"; +- setProjectRecordStatus(`${projectRecordContract.terminology || "Game Hub"} records loaded from the project records service; authoritative keys managed by service; asset references linked to storage object keys: ${assetReferenceCount}; ${saveMode}.`); +- + elements.projectRecordsTable.replaceChildren(); +- if (!records.length) { +- const row = document.createElement("tr"); +- ["No records", "No Game Hub records", "Not started", source].forEach((value) => { +- const cell = document.createElement("td"); +- cell.textContent = value; +- row.append(cell); +- }); +- elements.projectRecordsTable.append(row); +- return; +- } +- +- records.forEach((record) => { +- const row = document.createElement("tr"); +- [ +- record.projectKey || "missing key", +- record.name || "Untitled project", +- record.status || "No status", +- `${source}; asset refs ${Number(record.assetReferenceCount || 0)}`, +- ].forEach((value) => { +- const cell = document.createElement("td"); +- cell.textContent = value; +- row.append(cell); +- }); +- elements.projectRecordsTable.append(row); ++ const row = document.createElement("tr"); ++ [ ++ { datasetName: "activeGameName", value: activeGame?.name || "No game open" }, ++ { datasetName: "activeGameStatus", value: activeGame?.status || progress?.gameStatus || "No Game" }, ++ { datasetName: "activeGamePurpose", value: activeGame?.purpose || "No purpose" }, ++ { datasetName: "activeGameOwner", value: activeGame?.ownerDisplayName || "No owner" }, ++ { datasetName: "currentUserRole", value: currentMember?.role || "Viewer" }, ++ { datasetName: "recommendedNextTool", value: progress?.recommendedNextTool || "Game Hub" }, ++ ].forEach(({ datasetName, value }) => { ++ const cell = document.createElement("td"); ++ cell.dataset[datasetName] = "true"; ++ cell.textContent = value; ++ row.append(cell); + }); ++ elements.projectRecordsTable.append(row); ++ ++ setProjectRecordStatus(projectRecordsSaveAllowed() ++ ? "Project Information loaded." ++ : "Project Information loaded. Sign in to save changes."); } - -@@ -996,25 +983,110 @@ function testSelectedSpeech() { + + function renderGameList() { +@@ -268,7 +248,7 @@ function renderGameList() { + const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {}); + const games = Array.isArray(listResult) ? listResult : []; + if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) { +- setStatusLog("Game list response is malformed. Reload Game Hub after the server API contract is restored."); ++ setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly."); + } + + elements.gameList.replaceChildren(); +@@ -276,7 +256,7 @@ function renderGameList() { + if (games.length === 0) { + const emptyState = document.createElement("p"); + emptyState.className = "status"; +- emptyState.textContent = "No games. Create or seed a game to continue."; ++ emptyState.textContent = "No games. Create a game to continue."; + elements.gameList.append(emptyState); return; } - const service = selectedTtsService(); -- const profile = selectedTtsProfile(); - const target = selectedSpeechTarget(); -- const emotion = target.emotionProfile; -- const result = ttsServiceRegistry.speak(service.key, { -+ const result = speakTarget(service, target, target.profile || selectedTtsProfile()); -+ if (!result.ok) { -+ setText(elements.previewStatus, result.message || "Speech test failed."); -+ setText(elements.log, result.message || "Speech test failed."); -+ return; +@@ -343,7 +323,7 @@ function renderTableCounts() { + ? tableResult + : { users: [], games: [], game_members: [] }; + if ((!isRecord(tableResult) || isRepositoryErrorResult(tableResult)) && !reportRepositoryError(tableResult, "Repository tables")) { +- setStatusLog("Repository tables response is malformed. Reload Game Hub after the server API contract is restored."); ++ setStatusLog("Game Hub project details are temporarily unavailable. Refresh the page or try again shortly."); + } + const rows = [ + ["users", Array.isArray(tables.users) ? tables.users.length : 0], +@@ -366,6 +346,29 @@ function renderTableCounts() { + }); + } + ++function renderSourceIdea(activeGame) { ++ const sourceIdea = isRecord(activeGame?.sourceIdea) ? activeGame.sourceIdea : null; ++ const name = String(sourceIdea?.idea || "").trim(); ++ const pitch = String(sourceIdea?.pitch || "").trim(); ++ const notes = Array.isArray(sourceIdea?.notes) ++ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) ++ : []; ++ ++ setText(elements.sourceIdeaName, name || "No source idea yet"); ++ setText(elements.sourceIdeaDisplay, name || "No source idea yet"); ++ setText(elements.sourceIdeaPitch, pitch || "Create a project from Idea Board to see source details."); ++ ++ if (elements.sourceIdeaNotes) { ++ elements.sourceIdeaNotes.replaceChildren(); ++ const visibleNotes = notes.length ? notes : ["No source notes."]; ++ visibleNotes.forEach((note) => { ++ const item = document.createElement("li"); ++ item.textContent = note; ++ elements.sourceIdeaNotes.append(item); ++ }); + } -+ const message = `Speech test started for ${target.label} using ${service.name}.`; -+ setText(elements.previewStatus, message); -+ setText(elements.log, message); +} + -+function visiblePlaybackError(message) { -+ const safeMessage = message || "Message Studio playback failed. Check the selected message, part, and TTS profile."; -+ showValidation([safeMessage]); -+ setText(elements.previewStatus, safeMessage); -+ setText(elements.log, safeMessage); -+ return { message: safeMessage, ok: false }; + function renderChecklist(progress) { + if (!elements.progressChecklist) { + return; +@@ -419,7 +422,8 @@ function renderWorkspace() { + renderMembersTable(activeGame); + renderTableCounts(); + renderChecklist(progress); +- renderProjectRecords(); ++ renderProjectInformation(activeGame, currentMember, progress); ++ renderSourceIdea(activeGame); + refreshSaveControls(); + } + +@@ -437,7 +441,7 @@ elements.form?.addEventListener("submit", (event) => { + + if (reportRepositoryError(game, "Create Game") || !isRecord(game) || !String(game.name || "").trim()) { + if (!isRepositoryErrorResult(game)) { +- setStatusLog("Create Game did not return a valid game. Restore the server API contract and try again."); ++ setStatusLog("Create Game could not be completed. Refresh the page or try again shortly."); + } + renderWorkspace(); + return; +@@ -528,4 +532,15 @@ elements.currentUserRoleInput?.addEventListener("change", () => { + populateSelect(elements.purposeInput, GAME_WORKSPACE_GAME_PURPOSES); + populateSelect(elements.gameStatusInput, GAME_WORKSPACE_GAME_STATUSES); + populateSelect(elements.currentUserRoleInput, GAME_WORKSPACE_MEMBER_ROLES); ++const requestedGameId = new URL(window.location.href).searchParams.get("game"); ++if (requestedGameId) { ++ const openedGame = repository.openGame(requestedGameId); ++ if (isRepositoryErrorResult(openedGame)) { ++ setStatusLog(repositoryErrorMessage(openedGame, "Open Game")); ++ } else if (openedGame) { ++ setStatusLog(`Opened ${openedGame.name}.`); ++ } else { ++ setStatusLog("That Game Hub project could not be found."); ++ } +} + renderWorkspace(); +diff --git a/toolbox/game-workspace/index.html b/toolbox/game-workspace/index.html +index ac1017b27..f204166d8 100644 +--- a/toolbox/game-workspace/index.html ++++ b/toolbox/game-workspace/index.html +@@ -66,43 +66,58 @@ +
+ +
+-

Game Hub Overview

+-

Game Hub manages game identity, status, progress, and launch readiness through the project records service.

+-
Loading Game Hub project records.
+-
+- +- +- +- +- +- +- +- +- +- +- +- +- +-
Game Hub Project Records
Authoritative KeyProjectStatusSource
LoadingLoadingLoadingProject records service
+-
+-
++

Project Information

++

Review the open project and its source idea.

++
Project Information ready.
++
+
+-
+-
Open Game
+-

Demo Game

+-
+-
+-
Under ConstructionGame Status
+-
GameGame Purpose
+-
No ownerOwner
+-
OwnerCurrent User Role
+-
Game ConfigurationRecommended Next Tool
++
++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++
Project Information
ProjectStatusPurposeOwnerRoleNext Tool
Demo GameUnder ConstructionGameNo ownerOwnerGame Configuration
+
+ +
+
++
++
++
++
Source Idea
++

No source idea yet

++
++
++ ++ ++ ++ ++ ++ ++
IdeaNo source idea yet
PitchCreate a project from Idea Board to see source details.
Notes
  • No source notes.
++
++
++
+
+
+
+@@ -119,7 +134,7 @@ +

Recommended Next Tool

Game Configuration

+

Checklist

  • Game identity: Complete
+
+-
Demo Game seeded.
++
Game Hub ready.
+
+
+
+@@ -141,56 +156,7 @@ +
+ + +-
+- Repository Tables +-
+-
+- +- +- +- +- +- +- +- +- +- +-
Game Hub data row counts
TableRows
users3
games4
game_members12
+-
+-
+-
+-
+- Team Members +-
+-
+- +- +- +- +- +- +- +- +-
Open game members
UserUser KeyRolePermission
No game---
+-
+-
+-
+ +- +
+
-

Game Hub Overview

-

Game Hub manages game identity, status, progress, and launch readiness through the project records service.

-
Loading Game Hub project records.
-
- - - - - - - - - - - - - -
Game Hub Project Records
Authoritative KeyProjectStatusSource
LoadingLoadingLoadingProject records service
-
-
+

Project Information

+

Review the open project and its source idea.

+
Project Information ready.
+
-
-
Open Game
-

Demo Game

-
-
-
Under ConstructionGame Status
-
GameGame Purpose
-
No ownerOwner
-
OwnerCurrent User Role
-
Game ConfigurationRecommended Next Tool
+
+ + + + + + + + + + + + + + + + + + + + + + +
Project Information
ProjectStatusPurposeOwnerRoleNext Tool
Demo GameUnder ConstructionGameNo ownerOwnerGame Configuration
+
+
+
+
Source Idea
+

No source idea yet

+
+
+ + + + + + +
IdeaNo source idea yet
PitchCreate a project from Idea Board to see source details.
Notes
  • No source notes.
+
+
+
@@ -119,7 +134,7 @@

Game Progress

Recommended Next Tool

Game Configuration

Checklist

  • Game identity: Complete
-
Demo Game seeded.
+
Game Hub ready.
@@ -141,56 +156,7 @@

Game Progress

-
- Repository Tables -
-
- - - - - - - - - - -
Game Hub data row counts
TableRows
users3
games4
game_members12
-
-
-
-
- Team Members -
-
- - - - - - - - -
Open game members
UserUser KeyRolePermission
No game---
-
-
-
-