diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md new file mode 100644 index 000000000..63cdb0e21 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md @@ -0,0 +1,22 @@ +# PR_26171_BETA_083 Dead-Code Cleanup Checklist + +## Message Studio +- PASS: Removed duplicate preview TTS Profile control outside the table. +- PASS: Removed duplicate TTS service selector outside the table. +- PASS: Removed dead Test Speech button and selected-item helper text. +- PASS: Removed unused Message Studio speech-test state and render paths. +- PASS: Kept playback routed through selected message part, TTS Profile, and Emotion Setting. +- PASS: Kept Play Part, Play Message, and Stop Playback controls available. + +## TTS Studio +- PASS: Kept Delivery in Emotion Settings. +- PASS: Kept Presets in Emotion Settings as Delivery Preset. +- PASS: Kept Voice Filters in profile controls. +- PASS: No duplicate summary or output panel was added. +- PASS: No separate Emotion Studio was added. + +## Tests +- PASS: Removed dead Message speech-test selector expectations. +- PASS: Added absence checks for removed duplicate controls. +- PASS: Added actionable error coverage for missing TTS Profile. +- PASS: Added actionable error coverage for missing Emotion Setting on the selected profile. diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md new file mode 100644 index 000000000..02ba719f4 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md @@ -0,0 +1,14 @@ +# PR_26171_BETA_083 Manual Validation Notes + +## Manual Review +- Reviewed changed Message Studio markup for removed duplicate controls. +- Reviewed changed Message Studio JavaScript for removed speech-test state and render paths. +- Reviewed changed TTS Studio labels for Profile, Age Filter, and Delivery Preset wording. +- Reviewed Playwright coverage for missing TTS Profile and missing Emotion Setting error states. + +## Browser Validation +- Targeted Playwright validation covered Message Studio and TTS Studio flows. +- No separate manual browser session was required beyond the automated browser validation. + +## Packaging +- Delta ZIP is required under tmp/ and must not be staged. diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md new file mode 100644 index 000000000..7e1b4eb7f --- /dev/null +++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md @@ -0,0 +1,25 @@ +# PR_26171_BETA_083 Message/TTS UX Checklist + +## Message Studio +- PASS: Messages parent table label is creator-friendly. +- PASS: Message Parts child table uses Part Text, Emotion, TTS Profile, Status, and Actions. +- PASS: Play Part label remains clear. +- PASS: Play Message label remains clear. +- PASS: Stop control is labeled Stop Playback. +- PASS: Missing TTS Profile produces a visible actionable error. +- PASS: Missing Emotion Setting on the selected TTS Profile produces a visible actionable error. +- PASS: Duplicate TTS/Profile/Emotion controls were not left outside the tables. + +## TTS Studio +- PASS: TTS Profile parent table uses creator-friendly Profile labeling. +- PASS: Age is clarified as Age Filter. +- PASS: Emotion Settings child table uses Delivery Preset labeling. +- PASS: Delivery settings live in Emotion Settings. +- PASS: Presets live in Emotion Settings. +- PASS: Voice Filters live only in profile controls. +- PASS: Duplicate summary/output panels were not introduced. + +## Ownership +- PASS: Message Studio owns text, ordered message parts, Emotion selection, and TTS Profile selection. +- PASS: TTS Studio owns Voice, Language, Emotion Settings, Delivery, and Presets. +- PASS: src/engine/audio owns playback. diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md new file mode 100644 index 000000000..da9be32f3 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md @@ -0,0 +1,24 @@ +# PR_26171_BETA_083 Validation Report + +## Static Checks +- PASS: node --check toolbox/messages/messages.js +- PASS: node --check toolbox/text-to-speech/text2speech.js +- PASS: node --check tests/playwright/tools/MessagesTool.spec.mjs +- PASS: node --check tests/playwright/tools/TextToSpeechFunctional.spec.mjs + +## Targeted Message Studio Validation +- PASS: npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --reporter=list +- Result: 3 passed in 38.3s + +## Targeted TTS Studio Validation +- PASS: npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs --reporter=list +- Result: 3 passed in 53.1s + +## Project Workspace Validation +- PASS: npm run test:workspace-v2 +- Result: 5 passed in 1.7m +- Note: Command name is legacy; user-facing language is Project Workspace. + +## Validation Notes +- A first parallel Message Studio run timed out while TTS validation was also running. The Message Studio suite passed on the targeted rerun. +- Generated workspace report churn was restored before PR report files were written. diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md new file mode 100644 index 000000000..29a06e3c6 --- /dev/null +++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md @@ -0,0 +1,45 @@ +# PR_26171_BETA_083-message-tts-polish-and-dead-code + +## Summary +- Team: BETA +- Scope: Message Studio and TTS Studio polish after parent/child table and playback integration. +- Branch: pr/26171-BETA-083-message-tts-polish-and-dead-code +- Instruction compliance: PASS + +## Ownership Confirmation +- BETA owns Message Studio work. +- BETA owns TTS Studio and TextToSpeechEngine integration work. +- Message Studio owns message text, ordered message parts, Emotion selection, and TTS Profile selection. +- TTS Studio owns profiles, voices, language, per-profile emotion settings, Delivery, and Presets. +- src/engine/audio owns playback. + +## Changes +- Removed dead Message Studio speech-test controls and unused selection code. +- Replaced Message Studio speech-test status with playback readiness/status. +- Kept Message Studio playback controls visible and clarified Stop as Stop Playback. +- Updated Message parent and part table labels to be more creator-friendly. +- Added explicit missing TTS Profile and missing Emotion Setting validation coverage. +- Clarified TTS Profile table and Emotion Settings table labels. +- Clarified that TTS presets belong to Emotion Settings as Delivery Preset. + +## Architecture Notes +- Theme V2 only. +- External JavaScript only. +- No inline styles, style blocks, inline handlers, page-local CSS, or tool-local CSS were added. +- No database changes. +- No separate Emotion Studio was added. +- Browser-owned product data was not introduced as a source of truth. +- Silent fallback for an explicitly stale TTS Profile selection was removed. + +## Validation Summary +- PASS: node --check toolbox/messages/messages.js +- PASS: node --check toolbox/text-to-speech/text2speech.js +- PASS: node --check tests/playwright/tools/MessagesTool.spec.mjs +- PASS: node --check tests/playwright/tools/TextToSpeechFunctional.spec.mjs +- PASS: npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs --reporter=list +- PASS: npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --reporter=list +- PASS: npm run test:workspace-v2 + +## Notes +- The first parallel Message Studio Playwright run timed out during report contention; the targeted Message Studio rerun passed by itself. +- npm run test:workspace-v2 is a legacy command name; user-facing language remains Project Workspace. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 60d4cdf24..2957acbc7 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,125 +1,13 @@ -PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff - -Git workflow: -- Current branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -- Created branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -- Push result: PASS; pushed to origin/pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/32 -- Merge approval status: pending explicit Team Alpha owner approval -- Merge result: not performed; merge is blocked until explicit Team Alpha owner approval -- Rebase/conflict resolution: PASS; rebased onto origin/main at e8845dae6 after GitHub reported the branch was behind current main. Conflicts were limited to generated Codex report artifacts and reports were regenerated. -- Latest rebase/conflict resolution: PASS; rebased onto origin/main at 195c90a64 after PR_26171_BETA_075 landed. Conflicts were resolved in generated Codex report artifacts and tests/playwright/tools/RootToolsFutureState.spec.mjs; the test expectations now preserve current-main Text To Speech coverage and ALPHA_047 canonical game-hub route/filter behavior. -- Final approved rebase: PASS; Team Alpha approval granted for ALPHA_047, rebased onto origin/main at 1451a1173, with conflicts limited to generated Codex report artifacts. - -Instruction compliance: -- PASS: docs_build/dev/PROJECT_INSTRUCTIONS.md was read before file changes. -- PASS: docs_build/dev/PROJECT_MULTI_PC.txt was read before file changes. -- PASS: BUILD_PR.md was created and read before implementation. -- PASS: Team Alpha owner scope applies to Game Hub, Game Journey handoff, and Idea Board continuity. -- PASS: Full samples smoke was not run. - -Validation: -- PASS: node --check toolbox/game-hub/game-hub.js -- PASS: node --check toolbox/game-hub/game-hub-api-client.js -- PASS: node --check toolbox/idea-board/index.js -- PASS: node --check toolbox/game-journey/game-journey.js -- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -- PASS: node --check src/dev-runtime/server/local-api-router.mjs -- PASS: node --check tests/playwright/tools/GameHubMockRepository.spec.mjs -- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -- PASS: node --check tests/playwright/tools/GameJourneyTool.spec.mjs -- PASS: npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line (11 passed) -- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line (2 passed) -- PASS: npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub hands the active game route to Game Journey|Game Journey requires an active game" (2 passed) -- PASS: npm run test:workspace-v2 (5 passed) -- PASS: git diff --check (line-ending warnings only) -- PASS: Completion revalidation for PR_26171_ALPHA_048 start-gate blocker; ALPHA_047 scoped validation was rerun while still on branch pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff, and ALPHA_048 implementation was not started. -- PASS: Latest post-rebase validation after origin/main 195c90a64: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. -- PASS: Final approved validation after origin/main 1451a1173: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. - -Requirement-by-requirement evidence: -- PASS: Rename active path toolbox/game-workspace/ to toolbox/game-hub/. -- PASS: Rename active game-workspace.js to game-hub.js. -- PASS: Rename active game-workspace-api-client.js to game-hub-api-client.js. -- PASS: Deep-audit and update imports, variables, data attributes, tests, toolbox navigation/registry, tool display mode slug, page titles, and creator-facing labels to Game Hub. -- PASS: Do not leave active navigation pointing to toolbox/game-workspace; active route audit found no active legacy route. -- PASS: Do not create duplicate active Game Hub paths; registry reads filter stale game-workspace rows and active metadata points to game-hub. -- PASS: Idea Board Create Project creates/links a Game Hub project when a Ready idea becomes Project. -- PASS: Project ideas lock original Idea, Pitch, and Notes. -- PASS: Game Hub shows Source Idea/Pitch/Notes as read-only creator-facing content. -- PASS: Game Journey receives executable Journey items from Idea notes when the linked project opens. -- PASS: Original Idea notes are not mutated or moved. -- PASS: Project/source-linked actions avoid Delete and use Open in Game Hub plus Archive. -- PASS: No real database migration was added; existing shared mock/server contracts are reused. -- PASS: Merge is not performed without explicit Team Alpha owner approval. - -Changed files: -- deleted: assets/theme-v2/images/badges/game-workspace-1024.png -- deleted: assets/theme-v2/images/badges/game-workspace.png -- added: assets/theme-v2/images/badges/game-hub-1024.png -- added: assets/theme-v2/images/badges/game-hub.png -- deleted: assets/theme-v2/images/tools/game-workspace-1024.png -- deleted: assets/theme-v2/images/tools/game-workspace.png -- added: assets/theme-v2/images/tools/game-hub-1024.png -- added: assets/theme-v2/images/tools/game-hub.png -- updated: assets/theme-v2/js/account-achievements.js -- updated: assets/theme-v2/js/gamefoundry-partials.js -- updated: assets/theme-v2/partials/header-nav.html -- updated: docs_build/database/ddl/game-workspace.sql -- updated: docs_build/database/dml/DML_INDEX.md -- updated: docs_build/database/dml/game-workspace.sql -- updated: docs_build/database/seed/game-workspace.json -- updated: docs_build/database/seed/guest/game-workspace.json -- updated: docs_build/dev/admin-notes/deployment-uat-prod/index.txt -- added: docs_build/dev/reports/codex_changed_files.txt -- added: docs_build/dev/reports/codex_review.diff -- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md -- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md -- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md -- deleted: learn/game-workspace/index.html -- added: learn/game-hub/index.html -- updated: learn/getting-started/index.html -- updated: learn/index.html -- updated: package.json -- updated: scripts/run-targeted-test-lanes.mjs -- updated: scripts/validate-browser-env-agnostic.mjs -- updated: scripts/validate-local-postgres-runtime.mjs -- updated: src/dev-runtime/admin/header-nav.local.html -- updated: src/dev-runtime/persistence/mock-db-store.js -- updated: src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -- updated: src/dev-runtime/seed/server-seed-loader.mjs -- updated: src/dev-runtime/server/local-api-router.mjs -- updated: src/shared/toolbox/tool-metadata-inventory.js -- updated: tests/dev-runtime/DevRuntimeBoundary.test.mjs -- updated: tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -- updated: tests/dev-runtime/SupabaseProductDataCutover.test.mjs -- updated: tests/playwright/account/AchievementsPage.spec.mjs -- updated: tests/playwright/tools/AdminDbViewer.spec.mjs -- added: tests/playwright/tools/GameHubMockRepository.spec.mjs -- updated: tests/playwright/tools/GameJourneyTool.spec.mjs -- deleted: tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -- updated: tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -- updated: tests/playwright/tools/RootToolsFutureState.spec.mjs -- updated: tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs -- updated: tests/playwright/tools/ToolImageRegistry.spec.mjs -- updated: tests/playwright/tools/ToolNavigationPrevNext.spec.mjs -- updated: tests/playwright/tools/ToolboxRoutePages.spec.mjs -- updated: toolbox/colors/index.html -- updated: toolbox/controls/controls.js -- updated: toolbox/controls/index.html -- updated: toolbox/game-design/index.html -- added: toolbox/game-hub/game-hub-api-client.js -- added: toolbox/game-hub/game-hub.js -- added: toolbox/game-hub/index.html -- updated: toolbox/game-journey/game-journey.js -- deleted: toolbox/game-workspace/game-workspace-api-client.js -- deleted: toolbox/game-workspace/game-workspace.js -- deleted: toolbox/game-workspace/index.html -- updated: toolbox/idea-board/index.js -- updated: toolbox/project-workspace/index.html -- updated: toolbox/tools-page-accordions.js - -ZIP: -- Path: tmp/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff_delta.zip -- Size: generated after branch push; final byte size is reported in the delivery response because the ZIP is not committed. -- Contents: repo-structured scoped delta files. Deleted legacy paths are documented in the changed-files list and are not represented as physical files in the archive. +docs_build/dev/reports/codex_changed_files.txt +docs_build/dev/reports/codex_review.diff +docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md +docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md +docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md +docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md +docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md +tests/playwright/tools/MessagesTool.spec.mjs +tests/playwright/tools/TextToSpeechFunctional.spec.mjs +toolbox/messages/index.html +toolbox/messages/messages.js +toolbox/text-to-speech/index.html +toolbox/text-to-speech/text2speech.js diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 9b7b02d87..e67301574 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,2086 +1,914 @@ -diff --git a/assets/theme-v2/images/badges/game-workspace-1024.png b/assets/theme-v2/images/badges/game-hub-1024.png -similarity index 100% -rename from assets/theme-v2/images/badges/game-workspace-1024.png -rename to assets/theme-v2/images/badges/game-hub-1024.png -diff --git a/assets/theme-v2/images/badges/game-workspace.png b/assets/theme-v2/images/badges/game-hub.png -similarity index 100% -rename from assets/theme-v2/images/badges/game-workspace.png -rename to assets/theme-v2/images/badges/game-hub.png -diff --git a/assets/theme-v2/images/tools/game-workspace-1024.png b/assets/theme-v2/images/tools/game-hub-1024.png -similarity index 100% -rename from assets/theme-v2/images/tools/game-workspace-1024.png -rename to assets/theme-v2/images/tools/game-hub-1024.png -diff --git a/assets/theme-v2/images/tools/game-workspace.png b/assets/theme-v2/images/tools/game-hub.png -similarity index 100% -rename from assets/theme-v2/images/tools/game-workspace.png -rename to assets/theme-v2/images/tools/game-hub.png -diff --git a/assets/theme-v2/js/account-achievements.js b/assets/theme-v2/js/account-achievements.js -index 365fbd818..184e4af67 100644 ---- a/assets/theme-v2/js/account-achievements.js -+++ b/assets/theme-v2/js/account-achievements.js -@@ -7,7 +7,7 @@ const buildRows = document.querySelector("[data-achievements-build-rows]"); - const buildStatus = document.querySelector("[data-achievements-build-status]"); - const createdCount = document.querySelector("[data-achievements-build-created-count]"); - const readyCount = document.querySelector("[data-achievements-build-ready-count]"); --const repository = createServerRepositoryClient("game-workspace"); -+const repository = createServerRepositoryClient("game-hub"); - - function setText(element, value) { - if (element) { -diff --git a/assets/theme-v2/js/gamefoundry-partials.js b/assets/theme-v2/js/gamefoundry-partials.js -index 862614796..44b301916 100644 ---- a/assets/theme-v2/js/gamefoundry-partials.js -+++ b/assets/theme-v2/js/gamefoundry-partials.js -@@ -3,7 +3,7 @@ - home: "index.html", - toolbox: "toolbox/index.html", - "ai-assistant": "toolbox/ai-assistant/index.html", -- "game-workspace": "toolbox/game-workspace/index.html", -+ "game-hub": "toolbox/game-hub/index.html", - "game-journey": "toolbox/game-journey/index.html", - "game-design": "toolbox/game-design/index.html", - "game-configuration": "toolbox/game-configuration/index.html", -diff --git a/assets/theme-v2/partials/header-nav.html b/assets/theme-v2/partials/header-nav.html -index f2c805256..f49f136ce 100644 ---- a/assets/theme-v2/partials/header-nav.html -+++ b/assets/theme-v2/partials/header-nav.html -@@ -35,7 +35,7 @@ - Custom Extensions - Game Configuration - Game Design -- Game Hub -+ Game Hub - Game Journey - - -diff --git a/docs_build/database/ddl/game-workspace.sql b/docs_build/database/ddl/game-workspace.sql -index 5ccff6878..b0b2f08e5 100644 ---- a/docs_build/database/ddl/game-workspace.sql -+++ b/docs_build/database/ddl/game-workspace.sql -@@ -1,5 +1,5 @@ - -- Game Foundry Studio DEV database DDL ---- Group: Game Workspace -+-- Group: Game Hub - -- Ownership: docs_build/database/ddl/game-workspace.sql - -- Target DEV database: gamefoundry_dev - -- Scope: executable grouped table DDL for active Supabase/server API migration. -diff --git a/docs_build/database/dml/DML_INDEX.md b/docs_build/database/dml/DML_INDEX.md -index 642e3ebce..3d8ee81f8 100644 ---- a/docs_build/database/dml/DML_INDEX.md -+++ b/docs_build/database/dml/DML_INDEX.md -@@ -13,7 +13,7 @@ Direct SQL setup is intentionally narrow. Account DEV users now require server-s - | Game Configuration | `game-configuration.sql` | Server-seed-owned | Server-side seed API | - | Game Design | `game-design.sql` | Server-seed-owned | Server-side seed API | - | Game Journey | `game-journey.sql` | Server-seed-owned | Server-side seed API | --| Game Workspace | `game-workspace.sql` | Server-seed-owned | Server-side seed API | -+| Game Hub | `game-workspace.sql` | Server-seed-owned | Server-side seed API | - | Messages | `messages.sql` | Server-seed-owned | Messages Local API/server-side SQLite service | - | Objects | `objects.sql` | Server-seed-owned | Server-side seed API | - | Palette | `palette.sql` | Server-seed-owned | Server-side seed API | -diff --git a/docs_build/database/dml/game-workspace.sql b/docs_build/database/dml/game-workspace.sql -index 331dacd82..a8257f6f6 100644 ---- a/docs_build/database/dml/game-workspace.sql -+++ b/docs_build/database/dml/game-workspace.sql -@@ -1,5 +1,5 @@ - -- Game Foundry Studio DEV database DML / seed review ---- Group: Game Workspace -+-- Group: Game Hub - -- Ownership: docs_build/database/dml/game-workspace.sql - -- Runtime setup/seed operations for this group must run through server-side APIs. - -- Temporary scope: DEV/review artifact only until Admin Site Setup/server seed APIs fully own grouped setup. -diff --git a/docs_build/database/seed/game-workspace.json b/docs_build/database/seed/game-workspace.json -index bdc7afa85..bdc5eef4d 100644 ---- a/docs_build/database/seed/game-workspace.json -+++ b/docs_build/database/seed/game-workspace.json -@@ -1,6 +1,6 @@ - { -- "group": "Game Workspace", -- "groupKey": "game-workspace", -+ "group": "Game Hub", -+ "groupKey": "game-hub", - "owner": "docs_build/database/seed", - "serverSideSeedRequired": true, - "browserAuthoritativeKeyGenerationAllowed": false, -diff --git a/docs_build/database/seed/guest/game-workspace.json b/docs_build/database/seed/guest/game-workspace.json -index 434050b9f..a528c5318 100644 ---- a/docs_build/database/seed/guest/game-workspace.json -+++ b/docs_build/database/seed/guest/game-workspace.json -@@ -1,6 +1,6 @@ - { -- "group": "Game Workspace", -- "groupKey": "game-workspace", -+ "group": "Game Hub", -+ "groupKey": "game-hub", - "owner": "docs_build/database/seed/guest", - "readOnly": true, - "writableByGuest": false, -@@ -11,18 +11,18 @@ - }, - "samplePackages": [ - { -- "key": "guest-game-workspace-starter", -+ "key": "guest-game-hub-starter", - "audience": "guest", - "createdBy": "01K2GFSJ0Y0000000000000054", -- "groupKey": "game-workspace", -- "label": "Guest Game Workspace starter", -- "loadablePath": "toolbox/game-workspace/index.html", -+ "groupKey": "game-hub", -+ "label": "Guest Game Hub starter", -+ "loadablePath": "toolbox/game-hub/index.html", - "readOnly": true, - "sampleKind": "toolSeed", - "signInRedirect": "account/sign-in.html", - "source": "docs_build/database/seed/guest/game-workspace.json", -- "toolKey": "game-workspace", -- "toolName": "Game Workspace", -+ "toolKey": "game-hub", -+ "toolName": "Game Hub", - "writableByGuest": false - } - ] -diff --git a/docs_build/dev/admin-notes/deployment-uat-prod/index.txt b/docs_build/dev/admin-notes/deployment-uat-prod/index.txt -index 27ee1bf8f..0a8b67b53 100644 ---- a/docs_build/dev/admin-notes/deployment-uat-prod/index.txt -+++ b/docs_build/dev/admin-notes/deployment-uat-prod/index.txt -@@ -141,7 +141,7 @@ Example: - "requiredRoutes": [ - "/", - "/toolbox/", -- "/toolbox/game-workspace/", -+ "/toolbox/game-hub/", - "/toolbox/game-journey/", - "/toolbox/controls/", - "/account/user-controls.html" -diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 5a25f4aef..60d4cdf24 100644 ---- a/docs_build/dev/reports/codex_changed_files.txt -+++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,8 +1,125 @@ --docs_build/dev/reports/codex_changed_files.txt --docs_build/dev/reports/codex_review.diff --docs_build/dev/reports/PR_26171_BETA_081-message-playback-through-tts-engine-manual-validation-notes.md --docs_build/dev/reports/PR_26171_BETA_081-message-playback-through-tts-engine-validation-report.md --docs_build/dev/reports/PR_26171_BETA_081-message-playback-through-tts-engine.md --src/dev-runtime/messages/messages-sqlite-service.mjs --tests/playwright/tools/MessagesTool.spec.mjs --toolbox/messages/messages.js -+PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff -+ -+Git workflow: -+- Current branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -+- Created branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -+- Push result: PASS; pushed to origin/pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff -+- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/32 -+- Merge approval status: pending explicit Team Alpha owner approval -+- Merge result: not performed; merge is blocked until explicit Team Alpha owner approval -+- Rebase/conflict resolution: PASS; rebased onto origin/main at e8845dae6 after GitHub reported the branch was behind current main. Conflicts were limited to generated Codex report artifacts and reports were regenerated. -+- Latest rebase/conflict resolution: PASS; rebased onto origin/main at 195c90a64 after PR_26171_BETA_075 landed. Conflicts were resolved in generated Codex report artifacts and tests/playwright/tools/RootToolsFutureState.spec.mjs; the test expectations now preserve current-main Text To Speech coverage and ALPHA_047 canonical game-hub route/filter behavior. -+- Final approved rebase: PASS; Team Alpha approval granted for ALPHA_047, rebased onto origin/main at 1451a1173, with conflicts limited to generated Codex report artifacts. -+ -+Instruction compliance: -+- PASS: docs_build/dev/PROJECT_INSTRUCTIONS.md was read before file changes. -+- PASS: docs_build/dev/PROJECT_MULTI_PC.txt was read before file changes. -+- PASS: BUILD_PR.md was created and read before implementation. -+- PASS: Team Alpha owner scope applies to Game Hub, Game Journey handoff, and Idea Board continuity. -+- PASS: Full samples smoke was not run. -+ -+Validation: -+- PASS: node --check toolbox/game-hub/game-hub.js -+- PASS: node --check toolbox/game-hub/game-hub-api-client.js -+- PASS: node --check toolbox/idea-board/index.js -+- PASS: node --check toolbox/game-journey/game-journey.js -+- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -+- PASS: node --check src/dev-runtime/server/local-api-router.mjs -+- PASS: node --check tests/playwright/tools/GameHubMockRepository.spec.mjs -+- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+- PASS: node --check tests/playwright/tools/GameJourneyTool.spec.mjs -+- PASS: npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line (11 passed) -+- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line (2 passed) -+- PASS: npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub hands the active game route to Game Journey|Game Journey requires an active game" (2 passed) -+- PASS: npm run test:workspace-v2 (5 passed) -+- PASS: git diff --check (line-ending warnings only) -+- PASS: Completion revalidation for PR_26171_ALPHA_048 start-gate blocker; ALPHA_047 scoped validation was rerun while still on branch pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff, and ALPHA_048 implementation was not started. -+- PASS: Latest post-rebase validation after origin/main 195c90a64: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. -+- PASS: Final approved validation after origin/main 1451a1173: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. -+ -+Requirement-by-requirement evidence: -+- PASS: Rename active path toolbox/game-workspace/ to toolbox/game-hub/. -+- PASS: Rename active game-workspace.js to game-hub.js. -+- PASS: Rename active game-workspace-api-client.js to game-hub-api-client.js. -+- PASS: Deep-audit and update imports, variables, data attributes, tests, toolbox navigation/registry, tool display mode slug, page titles, and creator-facing labels to Game Hub. -+- PASS: Do not leave active navigation pointing to toolbox/game-workspace; active route audit found no active legacy route. -+- PASS: Do not create duplicate active Game Hub paths; registry reads filter stale game-workspace rows and active metadata points to game-hub. -+- PASS: Idea Board Create Project creates/links a Game Hub project when a Ready idea becomes Project. -+- PASS: Project ideas lock original Idea, Pitch, and Notes. -+- PASS: Game Hub shows Source Idea/Pitch/Notes as read-only creator-facing content. -+- PASS: Game Journey receives executable Journey items from Idea notes when the linked project opens. -+- PASS: Original Idea notes are not mutated or moved. -+- PASS: Project/source-linked actions avoid Delete and use Open in Game Hub plus Archive. -+- PASS: No real database migration was added; existing shared mock/server contracts are reused. -+- PASS: Merge is not performed without explicit Team Alpha owner approval. -+ -+Changed files: -+- deleted: assets/theme-v2/images/badges/game-workspace-1024.png -+- deleted: assets/theme-v2/images/badges/game-workspace.png -+- added: assets/theme-v2/images/badges/game-hub-1024.png -+- added: assets/theme-v2/images/badges/game-hub.png -+- deleted: assets/theme-v2/images/tools/game-workspace-1024.png -+- deleted: assets/theme-v2/images/tools/game-workspace.png -+- added: assets/theme-v2/images/tools/game-hub-1024.png -+- added: assets/theme-v2/images/tools/game-hub.png -+- updated: assets/theme-v2/js/account-achievements.js -+- updated: assets/theme-v2/js/gamefoundry-partials.js -+- updated: assets/theme-v2/partials/header-nav.html -+- updated: docs_build/database/ddl/game-workspace.sql -+- updated: docs_build/database/dml/DML_INDEX.md -+- updated: docs_build/database/dml/game-workspace.sql -+- updated: docs_build/database/seed/game-workspace.json -+- updated: docs_build/database/seed/guest/game-workspace.json -+- updated: docs_build/dev/admin-notes/deployment-uat-prod/index.txt -+- added: docs_build/dev/reports/codex_changed_files.txt -+- added: docs_build/dev/reports/codex_review.diff -+- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md -+- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md -+- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md -+- deleted: learn/game-workspace/index.html -+- added: learn/game-hub/index.html -+- updated: learn/getting-started/index.html -+- updated: learn/index.html -+- updated: package.json -+- updated: scripts/run-targeted-test-lanes.mjs -+- updated: scripts/validate-browser-env-agnostic.mjs -+- updated: scripts/validate-local-postgres-runtime.mjs -+- updated: src/dev-runtime/admin/header-nav.local.html -+- updated: src/dev-runtime/persistence/mock-db-store.js -+- updated: src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -+- updated: src/dev-runtime/seed/server-seed-loader.mjs -+- updated: src/dev-runtime/server/local-api-router.mjs -+- updated: src/shared/toolbox/tool-metadata-inventory.js -+- updated: tests/dev-runtime/DevRuntimeBoundary.test.mjs -+- updated: tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -+- updated: tests/dev-runtime/SupabaseProductDataCutover.test.mjs -+- updated: tests/playwright/account/AchievementsPage.spec.mjs -+- updated: tests/playwright/tools/AdminDbViewer.spec.mjs -+- added: tests/playwright/tools/GameHubMockRepository.spec.mjs -+- updated: tests/playwright/tools/GameJourneyTool.spec.mjs -+- deleted: tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -+- updated: tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+- updated: tests/playwright/tools/RootToolsFutureState.spec.mjs -+- updated: tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs -+- updated: tests/playwright/tools/ToolImageRegistry.spec.mjs -+- updated: tests/playwright/tools/ToolNavigationPrevNext.spec.mjs -+- updated: tests/playwright/tools/ToolboxRoutePages.spec.mjs -+- updated: toolbox/colors/index.html -+- updated: toolbox/controls/controls.js -+- updated: toolbox/controls/index.html -+- updated: toolbox/game-design/index.html -+- added: toolbox/game-hub/game-hub-api-client.js -+- added: toolbox/game-hub/game-hub.js -+- added: toolbox/game-hub/index.html -+- updated: toolbox/game-journey/game-journey.js -+- deleted: toolbox/game-workspace/game-workspace-api-client.js -+- deleted: toolbox/game-workspace/game-workspace.js -+- deleted: toolbox/game-workspace/index.html -+- updated: toolbox/idea-board/index.js -+- updated: toolbox/project-workspace/index.html -+- updated: toolbox/tools-page-accordions.js -+ -+ZIP: -+- Path: tmp/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff_delta.zip -+- Size: generated after branch push; final byte size is reported in the delivery response because the ZIP is not committed. -+- Contents: repo-structured scoped delta files. Deleted legacy paths are documented in the changed-files list and are not represented as physical files in the archive. -diff --git a/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md +diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md new file mode 100644 -index 000000000..cc605d860 +index 000000000..63cdb0e21 --- /dev/null -+++ b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md -@@ -0,0 +1,48 @@ -+# PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff APPLY -+ -+## Git Workflow -+- Created branch: `pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff` -+- Push result: PASS; pushed to `origin/pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff` -+- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/32 -+- Merge approval status: pending explicit Team Alpha owner approval -+- Merge result: not allowed without explicit Team Alpha owner approval -+- Rebase/conflict resolution: PASS; rebased onto `origin/main` at `e8845dae6` and resolved conflicts limited to generated Codex report artifacts. -+- Latest rebase/conflict resolution: PASS; rebased onto `origin/main` at `195c90a64`, resolved generated report conflicts and `tests/playwright/tools/RootToolsFutureState.spec.mjs` expectations for the canonical `game-hub` route and filtered legacy `game-workspace` registry row. -+- Final approved rebase: PASS; Team Alpha approval granted, rebased onto `origin/main` at `1451a1173`, with conflicts limited to generated Codex report artifacts. -+ -+## Validation -+- `node --check toolbox/game-hub/game-hub.js`: PASS -+- `node --check toolbox/game-hub/game-hub-api-client.js`: PASS -+- `node --check toolbox/idea-board/index.js`: PASS -+- `node --check toolbox/game-journey/game-journey.js`: PASS -+- `node --check src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js`: PASS -+- `node --check src/dev-runtime/server/local-api-router.mjs`: PASS -+- `node --check tests/playwright/tools/GameHubMockRepository.spec.mjs`: PASS -+- `node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs`: PASS -+- `node --check tests/playwright/tools/GameJourneyTool.spec.mjs`: PASS -+- `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line`: PASS, 11 passed -+- `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line`: PASS, 2 passed -+- `npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub hands the active game route to Game Journey|Game Journey requires an active game"`: PASS, 2 passed -+- `npm run test:workspace-v2`: PASS, 5 passed -+- `git diff --check`: PASS, line-ending warnings only -+- Completion revalidation for `PR_26171_ALPHA_048` start-gate blocker: PASS; ALPHA_047 scoped validation was rerun on branch `pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff` before starting ALPHA_048. -+- Latest post-rebase validation: PASS; syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and `npm run test:workspace-v2` were rerun after rebasing onto `195c90a64`. -+- Final approved validation: PASS; syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and `npm run test:workspace-v2` were rerun after rebasing onto `1451a1173`. -+ -+## ZIP -+- Path: `tmp/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff_delta.zip` -+- Size: generated after branch push; final byte size is reported in the delivery response because the ZIP is not committed. -+- Contents: repo-structured scoped delta files, with deleted legacy paths documented in `docs_build/dev/reports/codex_changed_files.txt`. -+ -+## Requirement Evidence -+- Instruction compliance: PASS; PROJECT_INSTRUCTIONS.md, PROJECT_MULTI_PC.txt, and BUILD_PR.md were read before implementation. -+- Team Alpha owner: PASS; scope is Game Hub, Game Journey handoff, and Idea Board continuity. -+- Canonical active path: PASS; active route is `toolbox/game-hub/` with no active navigation pointing to `toolbox/game-workspace/`. -+- Active files renamed: PASS; active Game Hub JS files now use `game-hub.js` and `game-hub-api-client.js`. -+- Deep reference audit: PASS; imports, tests, navigation registry, tool display mode slug, titles, and creator labels were updated to Game Hub. -+- No duplicate active Game Hub path: PASS; legacy Game Workspace registry rows are filtered from active registry reads. -+- Idea to Hub handoff: PASS; Ready ideas create/link a Game Hub project, become Project, lock the source idea and notes, and expose Open in Game Hub/Archive actions. -+- Game Hub source display: PASS; Game Hub renders Source Idea, Pitch, and Notes read-only for source-linked projects. -+- Journey handoff: PASS; opening the project creates executable Game Journey items from source idea notes without moving or mutating the original notes. -+- Delete avoidance: PASS; source-linked projects do not expose Delete Open Game. -+- Merge gate: PASS; merge is intentionally blocked until explicit Team Alpha owner approval. -diff --git a/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md ++++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md +@@ -0,0 +1,22 @@ ++# PR_26171_BETA_083 Dead-Code Cleanup Checklist ++ ++## Message Studio ++- PASS: Removed duplicate preview TTS Profile control outside the table. ++- PASS: Removed duplicate TTS service selector outside the table. ++- PASS: Removed dead Test Speech button and selected-item helper text. ++- PASS: Removed unused Message Studio speech-test state and render paths. ++- PASS: Kept playback routed through selected message part, TTS Profile, and Emotion Setting. ++- PASS: Kept Play Part, Play Message, and Stop Playback controls available. ++ ++## TTS Studio ++- PASS: Kept Delivery in Emotion Settings. ++- PASS: Kept Presets in Emotion Settings as Delivery Preset. ++- PASS: Kept Voice Filters in profile controls. ++- PASS: No duplicate summary or output panel was added. ++- PASS: No separate Emotion Studio was added. ++ ++## Tests ++- PASS: Removed dead Message speech-test selector expectations. ++- PASS: Added absence checks for removed duplicate controls. ++- PASS: Added actionable error coverage for missing TTS Profile. ++- PASS: Added actionable error coverage for missing Emotion Setting on the selected profile. +diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md new file mode 100644 -index 000000000..7b3a0d377 +index 000000000..02ba719f4 --- /dev/null -+++ b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md -@@ -0,0 +1,51 @@ -+# PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff BUILD -+ -+## Start Gate -+- Read `docs_build/dev/PROJECT_INSTRUCTIONS.md`. -+- Read `docs_build/dev/PROJECT_MULTI_PC.txt`. -+- Confirm current branch is clean latest `main` before branch creation. -+- Confirm Team Alpha owns Game Hub, Game Journey, and Idea flow work. -+- Confirm merge requires explicit Team Alpha owner approval. -+ -+## Exact Targets -+- `toolbox/game-workspace/` -> `toolbox/game-hub/` -+- `toolbox/game-hub/index.html` -+- `toolbox/game-hub/game-hub.js` -+- `toolbox/game-hub/game-hub-api-client.js` -+- `toolbox/idea-board/index.js` -+- `toolbox/game-journey/game-journey.js` -+- Active navigation/registry references to `toolbox/game-workspace` or `game-workspace` -+- Targeted Playwright tests for Game Hub, Idea Board handoff, and navigation routes -+- `docs_build/dev/reports/codex_review.diff` -+- `docs_build/dev/reports/codex_changed_files.txt` -+ -+## Required Implementation -+- Rename active path `toolbox/game-workspace/` to `toolbox/game-hub/`. -+- Rename active files to `game-hub.js` and `game-hub-api-client.js`. -+- Do not leave active navigation pointing to `toolbox/game-workspace`. -+- Do not create a duplicate active Game Hub path. -+- Update imports, route references, registry path/folder/entry point, tool display mode slug, page titles, creator-facing labels, and tests. -+- Preserve the existing repository/API contract unless a small compatibility alias is required. -+- Idea Board Create Project opens the canonical Game Hub route. -+- Project source-linked actions avoid Delete and use Open in Game Hub and Archive. -+- When an idea becomes Project, original Idea, Pitch, and Notes remain locked/read-only. -+- Game Hub displays Source Idea, Pitch, and Notes read-only. -+- Game Journey receives executable Journey items from Idea notes without mutating or moving original notes. -+ -+## Required Validation -+- `node --check toolbox/game-hub/game-hub.js` -+- `node --check toolbox/idea-board/index.js` -+- Changed-file syntax checks for affected Game Journey JavaScript. -+- Targeted Game Hub Playwright. -+- Targeted Idea Board Playwright if handoff code changes. -+- `npm run test:workspace-v2` -+- Do not run full samples smoke. -+ -+## Required Delivery -+- Update required reports. -+- Produce repo-structured ZIP under `tmp/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff_delta.zip`. -+- Stage only scoped files. -+- Commit. -+- Push branch. -+- Create PR. -+- Stop before merge unless explicit Team Alpha owner approval is present. -diff --git a/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md ++++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md +@@ -0,0 +1,14 @@ ++# PR_26171_BETA_083 Manual Validation Notes ++ ++## Manual Review ++- Reviewed changed Message Studio markup for removed duplicate controls. ++- Reviewed changed Message Studio JavaScript for removed speech-test state and render paths. ++- Reviewed changed TTS Studio labels for Profile, Age Filter, and Delivery Preset wording. ++- Reviewed Playwright coverage for missing TTS Profile and missing Emotion Setting error states. ++ ++## Browser Validation ++- Targeted Playwright validation covered Message Studio and TTS Studio flows. ++- No separate manual browser session was required beyond the automated browser validation. ++ ++## Packaging ++- Delta ZIP is required under tmp/ and must not be staged. +diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md new file mode 100644 -index 000000000..b307026fb +index 000000000..7e1b4eb7f --- /dev/null -+++ b/docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md -@@ -0,0 +1,27 @@ -+# PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff PLAN -+ -+## Source Of Truth -+- `docs_build/dev/PROJECT_INSTRUCTIONS.md` -+- `docs_build/dev/PROJECT_MULTI_PC.txt` -+- User request: `PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff` -+ -+## Purpose -+Canonicalize the active Game Hub route from `toolbox/game-workspace/` to `toolbox/game-hub/` and preserve the Idea Board to Game Hub to Game Journey handoff. ++++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md +@@ -0,0 +1,25 @@ ++# PR_26171_BETA_083 Message/TTS UX Checklist ++ ++## Message Studio ++- PASS: Messages parent table label is creator-friendly. ++- PASS: Message Parts child table uses Part Text, Emotion, TTS Profile, Status, and Actions. ++- PASS: Play Part label remains clear. ++- PASS: Play Message label remains clear. ++- PASS: Stop control is labeled Stop Playback. ++- PASS: Missing TTS Profile produces a visible actionable error. ++- PASS: Missing Emotion Setting on the selected TTS Profile produces a visible actionable error. ++- PASS: Duplicate TTS/Profile/Emotion controls were not left outside the tables. ++ ++## TTS Studio ++- PASS: TTS Profile parent table uses creator-friendly Profile labeling. ++- PASS: Age is clarified as Age Filter. ++- PASS: Emotion Settings child table uses Delivery Preset labeling. ++- PASS: Delivery settings live in Emotion Settings. ++- PASS: Presets live in Emotion Settings. ++- PASS: Voice Filters live only in profile controls. ++- PASS: Duplicate summary/output panels were not introduced. + +## Ownership -+- Team: Alpha -+- Area: Game Hub, Idea flow, Game Journey handoff -+- Merge gate: do not merge without explicit Team Alpha owner approval. -+ -+## Scope -+- Rename active Game Hub path and active Game Hub JavaScript files. -+- Update active navigation, registry, imports, tests, tool display mode slug, and creator-facing paths. -+- Keep the existing shared repository/API contract unless a minimal alias is required for route compatibility. -+- Ensure Idea Board Create Project opens the canonical Game Hub route and preserves source Idea/Pitch/Notes. -+- Ensure Game Journey can display executable journey items created from source idea notes. ++- PASS: Message Studio owns text, ordered message parts, Emotion selection, and TTS Profile selection. ++- PASS: TTS Studio owns Voice, Language, Emotion Settings, Delivery, and Presets. ++- PASS: src/engine/audio owns playback. +diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md +new file mode 100644 +index 000000000..da9be32f3 +--- /dev/null ++++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md +@@ -0,0 +1,24 @@ ++# PR_26171_BETA_083 Validation Report ++ ++## Static Checks ++- PASS: node --check toolbox/messages/messages.js ++- PASS: node --check toolbox/text-to-speech/text2speech.js ++- PASS: node --check tests/playwright/tools/MessagesTool.spec.mjs ++- PASS: node --check tests/playwright/tools/TextToSpeechFunctional.spec.mjs ++ ++## Targeted Message Studio Validation ++- PASS: npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --reporter=list ++- Result: 3 passed in 38.3s ++ ++## Targeted TTS Studio Validation ++- PASS: npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs --reporter=list ++- Result: 3 passed in 53.1s ++ ++## Project Workspace Validation ++- PASS: npm run test:workspace-v2 ++- Result: 5 passed in 1.7m ++- Note: Command name is legacy; user-facing language is Project Workspace. ++ ++## Validation Notes ++- A first parallel Message Studio run timed out while TTS validation was also running. The Message Studio suite passed on the targeted rerun. ++- Generated workspace report churn was restored before PR report files were written. +diff --git a/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md +new file mode 100644 +index 000000000..29a06e3c6 +--- /dev/null ++++ b/docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md +@@ -0,0 +1,45 @@ ++# PR_26171_BETA_083-message-tts-polish-and-dead-code ++ ++## Summary ++- Team: BETA ++- Scope: Message Studio and TTS Studio polish after parent/child table and playback integration. ++- Branch: pr/26171-BETA-083-message-tts-polish-and-dead-code ++- Instruction compliance: PASS ++ ++## Ownership Confirmation ++- BETA owns Message Studio work. ++- BETA owns TTS Studio and TextToSpeechEngine integration work. ++- Message Studio owns message text, ordered message parts, Emotion selection, and TTS Profile selection. ++- TTS Studio owns profiles, voices, language, per-profile emotion settings, Delivery, and Presets. ++- src/engine/audio owns playback. ++ ++## Changes ++- Removed dead Message Studio speech-test controls and unused selection code. ++- Replaced Message Studio speech-test status with playback readiness/status. ++- Kept Message Studio playback controls visible and clarified Stop as Stop Playback. ++- Updated Message parent and part table labels to be more creator-friendly. ++- Added explicit missing TTS Profile and missing Emotion Setting validation coverage. ++- Clarified TTS Profile table and Emotion Settings table labels. ++- Clarified that TTS presets belong to Emotion Settings as Delivery Preset. ++ ++## Architecture Notes ++- Theme V2 only. ++- External JavaScript only. ++- No inline styles, style blocks, inline handlers, page-local CSS, or tool-local CSS were added. ++- No database changes. ++- No separate Emotion Studio was added. ++- Browser-owned product data was not introduced as a source of truth. ++- Silent fallback for an explicitly stale TTS Profile selection was removed. ++ ++## Validation Summary ++- PASS: node --check toolbox/messages/messages.js ++- PASS: node --check toolbox/text-to-speech/text2speech.js ++- PASS: node --check tests/playwright/tools/MessagesTool.spec.mjs ++- PASS: node --check tests/playwright/tools/TextToSpeechFunctional.spec.mjs ++- PASS: npx playwright test tests/playwright/tools/TextToSpeechFunctional.spec.mjs --reporter=list ++- PASS: npx playwright test tests/playwright/tools/MessagesTool.spec.mjs --reporter=list ++- PASS: npm run test:workspace-v2 ++ ++## Notes ++- The first parallel Message Studio Playwright run timed out during report contention; the targeted Message Studio rerun passed by itself. ++- npm run test:workspace-v2 is a legacy command name; user-facing language remains Project Workspace. +diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt +index 60d4cdf24..2957acbc7 100644 +--- a/docs_build/dev/reports/codex_changed_files.txt ++++ b/docs_build/dev/reports/codex_changed_files.txt +@@ -1,125 +1,13 @@ +-PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff +- +-Git workflow: +-- Current branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff +-- Created branch: pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff +-- Push result: PASS; pushed to origin/pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff +-- PR URL: https://github.com/ToolboxAid/HTML-JavaScript-Gaming/pull/32 +-- Merge approval status: pending explicit Team Alpha owner approval +-- Merge result: not performed; merge is blocked until explicit Team Alpha owner approval +-- Rebase/conflict resolution: PASS; rebased onto origin/main at e8845dae6 after GitHub reported the branch was behind current main. Conflicts were limited to generated Codex report artifacts and reports were regenerated. +-- Latest rebase/conflict resolution: PASS; rebased onto origin/main at 195c90a64 after PR_26171_BETA_075 landed. Conflicts were resolved in generated Codex report artifacts and tests/playwright/tools/RootToolsFutureState.spec.mjs; the test expectations now preserve current-main Text To Speech coverage and ALPHA_047 canonical game-hub route/filter behavior. +-- Final approved rebase: PASS; Team Alpha approval granted for ALPHA_047, rebased onto origin/main at 1451a1173, with conflicts limited to generated Codex report artifacts. +- +-Instruction compliance: +-- PASS: docs_build/dev/PROJECT_INSTRUCTIONS.md was read before file changes. +-- PASS: docs_build/dev/PROJECT_MULTI_PC.txt was read before file changes. +-- PASS: BUILD_PR.md was created and read before implementation. +-- PASS: Team Alpha owner scope applies to Game Hub, Game Journey handoff, and Idea Board continuity. +-- PASS: Full samples smoke was not run. +- +-Validation: +-- PASS: node --check toolbox/game-hub/game-hub.js +-- PASS: node --check toolbox/game-hub/game-hub-api-client.js +-- PASS: node --check toolbox/idea-board/index.js +-- PASS: node --check toolbox/game-journey/game-journey.js +-- PASS: node --check src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js +-- PASS: node --check src/dev-runtime/server/local-api-router.mjs +-- PASS: node --check tests/playwright/tools/GameHubMockRepository.spec.mjs +-- PASS: node --check tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +-- PASS: node --check tests/playwright/tools/GameJourneyTool.spec.mjs +-- PASS: npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --project=playwright --workers=1 --reporter=line (11 passed) +-- PASS: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --project=playwright --workers=1 --reporter=line (2 passed) +-- PASS: npx playwright test tests/playwright/tools/GameJourneyTool.spec.mjs --project=playwright --workers=1 --reporter=line --grep "Game Hub hands the active game route to Game Journey|Game Journey requires an active game" (2 passed) +-- PASS: npm run test:workspace-v2 (5 passed) +-- PASS: git diff --check (line-ending warnings only) +-- PASS: Completion revalidation for PR_26171_ALPHA_048 start-gate blocker; ALPHA_047 scoped validation was rerun while still on branch pr/26171-ALPHA-047-game-hub-canonical-path-journey-handoff, and ALPHA_048 implementation was not started. +-- PASS: Latest post-rebase validation after origin/main 195c90a64: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. +-- PASS: Final approved validation after origin/main 1451a1173: syntax checks, targeted Game Hub Playwright, targeted Idea Board Playwright, targeted Game Journey handoff Playwright, and npm run test:workspace-v2. +- +-Requirement-by-requirement evidence: +-- PASS: Rename active path toolbox/game-workspace/ to toolbox/game-hub/. +-- PASS: Rename active game-workspace.js to game-hub.js. +-- PASS: Rename active game-workspace-api-client.js to game-hub-api-client.js. +-- PASS: Deep-audit and update imports, variables, data attributes, tests, toolbox navigation/registry, tool display mode slug, page titles, and creator-facing labels to Game Hub. +-- PASS: Do not leave active navigation pointing to toolbox/game-workspace; active route audit found no active legacy route. +-- PASS: Do not create duplicate active Game Hub paths; registry reads filter stale game-workspace rows and active metadata points to game-hub. +-- PASS: Idea Board Create Project creates/links a Game Hub project when a Ready idea becomes Project. +-- PASS: Project ideas lock original Idea, Pitch, and Notes. +-- PASS: Game Hub shows Source Idea/Pitch/Notes as read-only creator-facing content. +-- PASS: Game Journey receives executable Journey items from Idea notes when the linked project opens. +-- PASS: Original Idea notes are not mutated or moved. +-- PASS: Project/source-linked actions avoid Delete and use Open in Game Hub plus Archive. +-- PASS: No real database migration was added; existing shared mock/server contracts are reused. +-- PASS: Merge is not performed without explicit Team Alpha owner approval. +- +-Changed files: +-- deleted: assets/theme-v2/images/badges/game-workspace-1024.png +-- deleted: assets/theme-v2/images/badges/game-workspace.png +-- added: assets/theme-v2/images/badges/game-hub-1024.png +-- added: assets/theme-v2/images/badges/game-hub.png +-- deleted: assets/theme-v2/images/tools/game-workspace-1024.png +-- deleted: assets/theme-v2/images/tools/game-workspace.png +-- added: assets/theme-v2/images/tools/game-hub-1024.png +-- added: assets/theme-v2/images/tools/game-hub.png +-- updated: assets/theme-v2/js/account-achievements.js +-- updated: assets/theme-v2/js/gamefoundry-partials.js +-- updated: assets/theme-v2/partials/header-nav.html +-- updated: docs_build/database/ddl/game-workspace.sql +-- updated: docs_build/database/dml/DML_INDEX.md +-- updated: docs_build/database/dml/game-workspace.sql +-- updated: docs_build/database/seed/game-workspace.json +-- updated: docs_build/database/seed/guest/game-workspace.json +-- updated: docs_build/dev/admin-notes/deployment-uat-prod/index.txt +-- added: docs_build/dev/reports/codex_changed_files.txt +-- added: docs_build/dev/reports/codex_review.diff +-- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/APPLY_PR.md +-- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/BUILD_PR.md +-- added: docs_build/pr/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff/PLAN_PR.md +-- deleted: learn/game-workspace/index.html +-- added: learn/game-hub/index.html +-- updated: learn/getting-started/index.html +-- updated: learn/index.html +-- updated: package.json +-- updated: scripts/run-targeted-test-lanes.mjs +-- updated: scripts/validate-browser-env-agnostic.mjs +-- updated: scripts/validate-local-postgres-runtime.mjs +-- updated: src/dev-runtime/admin/header-nav.local.html +-- updated: src/dev-runtime/persistence/mock-db-store.js +-- updated: src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js +-- updated: src/dev-runtime/seed/server-seed-loader.mjs +-- updated: src/dev-runtime/server/local-api-router.mjs +-- updated: src/shared/toolbox/tool-metadata-inventory.js +-- updated: tests/dev-runtime/DevRuntimeBoundary.test.mjs +-- updated: tests/dev-runtime/ProductDataProviderContractHardening.test.mjs +-- updated: tests/dev-runtime/SupabaseProductDataCutover.test.mjs +-- updated: tests/playwright/account/AchievementsPage.spec.mjs +-- updated: tests/playwright/tools/AdminDbViewer.spec.mjs +-- added: tests/playwright/tools/GameHubMockRepository.spec.mjs +-- updated: tests/playwright/tools/GameJourneyTool.spec.mjs +-- deleted: tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs +-- updated: tests/playwright/tools/IdeaBoardTableNotes.spec.mjs +-- updated: tests/playwright/tools/RootToolsFutureState.spec.mjs +-- updated: tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs +-- updated: tests/playwright/tools/ToolImageRegistry.spec.mjs +-- updated: tests/playwright/tools/ToolNavigationPrevNext.spec.mjs +-- updated: tests/playwright/tools/ToolboxRoutePages.spec.mjs +-- updated: toolbox/colors/index.html +-- updated: toolbox/controls/controls.js +-- updated: toolbox/controls/index.html +-- updated: toolbox/game-design/index.html +-- added: toolbox/game-hub/game-hub-api-client.js +-- added: toolbox/game-hub/game-hub.js +-- added: toolbox/game-hub/index.html +-- updated: toolbox/game-journey/game-journey.js +-- deleted: toolbox/game-workspace/game-workspace-api-client.js +-- deleted: toolbox/game-workspace/game-workspace.js +-- deleted: toolbox/game-workspace/index.html +-- updated: toolbox/idea-board/index.js +-- updated: toolbox/project-workspace/index.html +-- updated: toolbox/tools-page-accordions.js +- +-ZIP: +-- Path: tmp/PR_26171_ALPHA_047-game-hub-canonical-path-journey-handoff_delta.zip +-- Size: generated after branch push; final byte size is reported in the delivery response because the ZIP is not committed. +-- Contents: repo-structured scoped delta files. Deleted legacy paths are documented in the changed-files list and are not represented as physical files in the archive. ++docs_build/dev/reports/codex_changed_files.txt ++docs_build/dev/reports/codex_review.diff ++docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-dead-code-cleanup-checklist.md ++docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-manual-validation-notes.md ++docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-message-tts-ux-checklist.md ++docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code-validation-report.md ++docs_build/dev/reports/PR_26171_BETA_083-message-tts-polish-and-dead-code.md ++tests/playwright/tools/MessagesTool.spec.mjs ++tests/playwright/tools/TextToSpeechFunctional.spec.mjs ++toolbox/messages/index.html ++toolbox/messages/messages.js ++toolbox/text-to-speech/index.html ++toolbox/text-to-speech/text2speech.js +diff --git a/tests/playwright/tools/MessagesTool.spec.mjs b/tests/playwright/tools/MessagesTool.spec.mjs +index 6c00fa6f5..69068a2e2 100644 +--- a/tests/playwright/tools/MessagesTool.spec.mjs ++++ b/tests/playwright/tools/MessagesTool.spec.mjs +@@ -167,11 +167,16 @@ test("Message Studio renders Messages with child Message Parts and plays ordered + await expect(page.locator("[data-messages-tts-add-row]")).toHaveCount(0); + await expect(page.locator("[data-messages-emotions]")).toHaveCount(0); + await expect(page.locator("[data-messages-tts-profiles]")).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-preview-tts-profile]")).toHaveCount(0); ++ await expect(page.locator("[data-messages-tts-service]")).toHaveCount(0); ++ await expect(page.locator("[data-messages-test-speech]")).toHaveCount(0); ++ await expect(page.getByText("Speech Test", { exact: true })).toHaveCount(0); ++ await expect(page.getByRole("columnheader", { name: "Message", exact: true })).toBeVisible(); ++ await expect(page.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible(); + await expect(page.locator("[data-messages-segment-count]")).toHaveText("0"); + await expect(page.locator("[data-messages-add-control-row]")).toBeVisible(); +- await expect(page.getByRole("button", { name: "Stop" })).toBeEnabled(); ++ await expect(page.getByRole("button", { name: "Stop Playback" })).toBeEnabled(); ++ await expect(page.locator("[data-messages-playback-status]")).toHaveText("Ready for Message Studio playback."); + + await page.getByRole("button", { name: "Add Message" }).click(); + await expect(page.locator("[data-messages-row-editor='__new__']")).toBeVisible(); +@@ -209,7 +214,7 @@ test("Message Studio renders Messages with child Message Parts and plays ordered + await expect(page.getByRole("heading", { name: "Message Parts" })).toBeVisible(); + const partsTable = page.getByLabel("Message parts"); + await expect(partsTable.getByRole("columnheader", { name: "Order" })).toHaveCount(0); +- await expect(partsTable.getByRole("columnheader", { name: "Text" })).toBeVisible(); ++ await expect(partsTable.getByRole("columnheader", { name: "Part Text" })).toBeVisible(); + await expect(partsTable.getByRole("columnheader", { name: "Emotion" })).toBeVisible(); + await expect(partsTable.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible(); + await expect(partsTable.getByRole("columnheader", { name: "Status" })).toBeVisible(); +@@ -251,6 +256,20 @@ test("Message Studio renders Messages with child Message Parts and plays ordered + volume: 1, + }), + ])); ++ const urgentPartRow = page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }); ++ await urgentPartRow.locator("[data-segment-tts-profile]").evaluate((select) => { ++ const option = document.createElement("option"); ++ option.value = "missing-profile"; ++ option.textContent = "Missing Profile"; ++ select.append(option); ++ select.value = option.value; ++ select.dispatchEvent(new Event("change", { bubbles: true })); ++ }); ++ await urgentPartRow.getByRole("button", { name: "Play Part" }).click(); ++ await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); ++ await expect(page.locator("[data-messages-validation-errors]")).toContainText("Select a TTS profile before playback."); ++ await expect(page.locator("[data-messages-playback-status]")).toHaveText("Select a TTS profile before playback."); ++ await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).locator("[data-segment-tts-profile]").selectOption({ label: "Default Balanced TTS Profile" }); + + 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."); +@@ -274,7 +293,7 @@ test("Message Studio renders Messages with child Message Parts and plays ordered + type: "speak", + voiceName: "Test Voice", + })); +- await page.getByRole("button", { name: "Stop" }).click(); ++ await page.getByRole("button", { name: "Stop Playback" }).click(); + await expect(page.locator("[data-messages-log]")).toHaveText("Message Studio playback stopped. Cleared 2 queued items."); + speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); + expect(speechCalls.at(-1)).toEqual({ type: "cancel" }); +@@ -359,13 +378,72 @@ test("Message Studio shows actionable playback error when audio engine is unavai + 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."); ++ await expect(page.locator("[data-messages-playback-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([]); ++ expect(failures.consoleErrors).toEqual([]); ++ } finally { ++ await closeMessagesRun(failures, page); ++ await fs.rm(sqlitePath, { force: true }); ++ } ++}); ++ ++test("Message Studio shows actionable playback error when selected TTS profile lacks the selected emotion", async ({ page }) => { ++ const sqlitePath = messagesDbPath(); ++ await fs.rm(sqlitePath, { force: true }); ++ await page.route("**/api/messages/tts-profiles", async (route) => { ++ if (route.request().method() !== "GET") { ++ await route.continue(); ++ return; ++ } ++ await route.fulfill({ ++ contentType: "application/json", ++ body: JSON.stringify({ ++ data: { ++ persistence: { owner: "messages" }, ++ ttsProfiles: [{ ++ active: true, ++ emotionSettings: [{ active: true, emotion: "calm", emotionLabel: "Calm", pitch: 1, rate: 1, ssmlLikePreset: "normal", volume: 1 }], ++ key: "calm-only-profile", ++ language: "en-US", ++ name: "Calm Only Profile", ++ providerKey: "browser-speech", ++ voiceName: "", ++ }], ++ }, ++ ok: true, ++ }), ++ }); ++ }); ++ const failures = await openMessagesPage(page, sqlitePath); ++ ++ try { ++ await addMessage(page, { ++ emotion: "Urgent", ++ name: "Urgent Encounter", ++ text: "Danger is close.", ++ }); ++ await openMessageParts(page, "Urgent Encounter"); ++ await addPart(page, { ++ emotion: "Urgent", ++ order: 1, ++ text: "Danger is close.", ++ }); + -+## Non-Scope -+- No full database migration. -+- No real Postgres/API expansion. -+- No duplicate active Game Hub path. -+- No merge without Team Alpha approval. -diff --git a/learn/game-workspace/index.html b/learn/game-hub/index.html -similarity index 77% -rename from learn/game-workspace/index.html -rename to learn/game-hub/index.html -index 5353eeb4b..ac0d7e308 100644 ---- a/learn/game-workspace/index.html -+++ b/learn/game-hub/index.html -@@ -4,8 +4,8 @@ - - - -- Game Workspace Learn - GameFoundryStudio -- -+ Game Hub Learn - GameFoundryStudio -+ - - - -@@ -15,8 +15,8 @@ -
-
-
--
Learn / Game Workspace
--

Game Workspace

-+
Learn / Game Hub
-+

Game Hub

-

Plan current focus, readiness, and recommended next tool for one game project.

-
-
-@@ -26,37 +26,37 @@ -
-
-

Overview

--

Game Workspace guidance overview placeholder for the future Learn system.

-+

Game Hub guidance overview placeholder for the future Learn system.

-
-
-
-
-

Quick Start

--

Static quick-start checklist for reviewing Game Workspace before real tutorials exist.

-+

Static quick-start checklist for reviewing Game Hub before real tutorials exist.

-
-
-
-
-

Common Tasks

--

Task cards will eventually describe frequent Game Workspace creator workflows.

-+

Task cards will eventually describe frequent Game Hub creator workflows.

-
-
-
-
-

Related Documentation

--

Documentation links for Game Workspace will be connected after the docs registry exists.

-+

Documentation links for Game Hub will be connected after the docs registry exists.

-
-
-
-
-

Related Videos

--

Video references for Game Workspace are planned; no embeds are active in this wireframe.

-+

Video references for Game Hub are planned; no embeds are active in this wireframe.

-
-
-
-
-

Examples

--

Example games and snippets for Game Workspace will be linked in a future implementation.

-+

Example games and snippets for Game Hub will be linked in a future implementation.

-
-
- -@@ -68,7 +68,7 @@ -

Continue learning

-
- All Learn -- Open Tool -+ Open Tool -
- - -diff --git a/learn/getting-started/index.html b/learn/getting-started/index.html -index e1d340e29..2ab1e9005 100644 ---- a/learn/getting-started/index.html -+++ b/learn/getting-started/index.html -@@ -68,7 +68,7 @@ -

Continue learning

-
- All Learn -- Open Game Workspace -+ Open Game Hub -
- - -diff --git a/learn/index.html b/learn/index.html -index 9f40647b7..8c0ba1f40 100644 ---- a/learn/index.html -+++ b/learn/index.html -@@ -79,7 +79,7 @@ - Colors - Game Configuration - Game Design -- Game Workspace -+ Game Hub - Getting Started - Objects - Publish -diff --git a/package.json b/package.json -index 505b71d5f..d13364e3f 100644 ---- a/package.json -+++ b/package.json -@@ -17,7 +17,7 @@ - "test:lane:tool-images": "node ./scripts/run-targeted-test-lanes.mjs --lane tool-images", - "test:lane:game-configuration": "node ./scripts/run-targeted-test-lanes.mjs --lane game-configuration", - "test:lane:game-design": "node ./scripts/run-targeted-test-lanes.mjs --lane game-design", -- "test:lane:game-workspace": "node ./scripts/run-targeted-test-lanes.mjs --lane game-workspace", -+ "test:lane:game-hub": "node ./scripts/run-targeted-test-lanes.mjs --lane game-hub", - "test:lane:tool-runtime": "node ./scripts/run-targeted-test-lanes.mjs --lane tool-runtime", - "test:lane:game-runtime": "node ./scripts/run-targeted-test-lanes.mjs --lane game-runtime", - "test:lane:integration": "node ./scripts/run-targeted-test-lanes.mjs --lane integration", -diff --git a/scripts/run-targeted-test-lanes.mjs b/scripts/run-targeted-test-lanes.mjs -index 095584f64..3fef73319 100644 ---- a/scripts/run-targeted-test-lanes.mjs -+++ b/scripts/run-targeted-test-lanes.mjs -@@ -122,17 +122,17 @@ const laneDefinitions = Object.freeze({ - requiresPreflight: true, - reason: "Workspace V2 command now validates the future-state tools surface without exercising deprecated toolbox/old_* routes." - }, -- "game-workspace": { -- affectedSurface: "Game Workspace mock repository, Game Workspace UI, and Toolbox Progress/Build Path game-state bridge", -+ "game-hub": { -+ affectedSurface: "Game Hub mock repository, Game Hub UI, and Toolbox Progress/Build Path game-state bridge", - commands: [ -- playwrightCommand("tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs") -+ playwrightCommand("tests/playwright/tools/GameHubMockRepository.spec.mjs") - ], - dependencies: [], - discoveryTargets: [ -- "tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs" -+ "tests/playwright/tools/GameHubMockRepository.spec.mjs" - ], - fixtures: [ -- "repo-served Game Workspace page", -+ "repo-served Game Hub page", - "repo-served Toolbox page with role simulation", - "in-memory SQL-shaped mock game repository" - ], -@@ -140,7 +140,7 @@ const laneDefinitions = Object.freeze({ - ownership: "tools", - playwrightDir: "tests/playwright/tools", - requiresPreflight: true, -- reason: "Game Workspace rebuild slice validates mock users/games/game_members data actions, game lifecycle controls, and game-driven Progress/Build Path copy without exercising unrelated toolbox routes." -+ reason: "Game Hub rebuild slice validates mock users/games/game_members data actions, game lifecycle controls, and game-driven Progress/Build Path copy without exercising unrelated toolbox routes." - }, - "game-design": { - affectedSurface: "Game Design mock repository, project purpose flow, validation overlay, capability demo authoring, and Toolbox progress handoff", -@@ -155,7 +155,7 @@ const laneDefinitions = Object.freeze({ - "repo-served Game Design page", - "repo-served Toolbox Progress and Build Path views", - "in-memory SQL-shaped Game Design mock repository", -- "Game Workspace mock game context" -+ "Game Hub mock game context" - ], - fixturePaths: [], - ownership: "tools", -@@ -218,7 +218,7 @@ const laneDefinitions = Object.freeze({ - fixtures: [ - "repo-served Toolbox page", - "repo-served Admin Tools Progress page", -- "Game Workspace mock game context", -+ "Game Hub mock game context", - "Toolbox role simulation" - ], - fixturePaths: [], -@@ -259,7 +259,7 @@ const laneDefinitions = Object.freeze({ - ], - fixtures: [ - "repo-served Admin Tools Progress page", -- "repo-served Game Workspace, Game Design, and Game Configuration tool pages", -+ "repo-served Game Hub, Game Design, and Game Configuration tool pages", - "repo-served Toolbox Group view with URL-selected accordion", - "Toolbox registry build sequence and route metadata" - ], -@@ -279,7 +279,7 @@ const laneDefinitions = Object.freeze({ - "tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs" - ], - fixtures: [ -- "repo-served Game Workspace, Game Design, Game Configuration, and AI Assistant tool pages", -+ "repo-served Game Hub, Game Design, Game Configuration, and AI Assistant tool pages", - "repo-served Toolbox Group view with URL-selected accordion", - "Toolbox registry build sequence and route metadata", - "shared Theme V2 Tool Display Mode script" -diff --git a/scripts/validate-browser-env-agnostic.mjs b/scripts/validate-browser-env-agnostic.mjs -index 2f8492652..ac69a4324 100644 ---- a/scripts/validate-browser-env-agnostic.mjs -+++ b/scripts/validate-browser-env-agnostic.mjs -@@ -66,7 +66,7 @@ const productApiClientFiles = Object.freeze([ - "toolbox/game-configuration/game-configuration-api-client.js", - "toolbox/game-design/game-design-api-client.js", - "toolbox/game-journey/game-journey-api-client.js", -- "toolbox/game-workspace/game-workspace-api-client.js", -+ "toolbox/game-hub/game-hub-api-client.js", - "toolbox/objects/objects-api-client.js", - "toolbox/tags/tags-api-client.js", - ]); -diff --git a/scripts/validate-local-postgres-runtime.mjs b/scripts/validate-local-postgres-runtime.mjs -index 26da8b5cd..23aeac932 100644 ---- a/scripts/validate-local-postgres-runtime.mjs -+++ b/scripts/validate-local-postgres-runtime.mjs -@@ -294,13 +294,13 @@ async function validateApiRuntime({ adapter, client }) { - const messageRow = savedBannerRows.find((row) => row.settingKey === "platform.banner.message"); - assert(messageRow?.settingValue === message, "Local Postgres platform_settings message row was not updated."); - -- const repository = await apiJson(server.baseUrl, "/api/toolbox/game-workspace/repositories", { -+ const repository = await apiJson(server.baseUrl, "/api/toolbox/game-hub/repositories", { - body: {}, - method: "POST", - }); -- assert(repository.repositoryId, "Game Workspace repository was not created."); -+ assert(repository.repositoryId, "Game Hub repository was not created."); - -- const created = await apiJson(server.baseUrl, `/api/toolbox/game-workspace/repositories/${repository.repositoryId}/methods/createGame`, { -+ const created = await apiJson(server.baseUrl, `/api/toolbox/game-hub/repositories/${repository.repositoryId}/methods/createGame`, { - body: { - args: [ - { -@@ -312,14 +312,14 @@ async function validateApiRuntime({ adapter, client }) { - }, - method: "POST", - }); -- assert(created.result?.name === GAME_NAME, "Game Workspace create did not return the validation game."); -+ assert(created.result?.name === GAME_NAME, "Game Hub create did not return the validation game."); - -- const active = await apiJson(server.baseUrl, `/api/toolbox/game-workspace/repositories/${repository.repositoryId}/methods/getActiveGame`, { -+ const active = await apiJson(server.baseUrl, `/api/toolbox/game-hub/repositories/${repository.repositoryId}/methods/getActiveGame`, { - body: { args: [] }, - method: "POST", - }); -- assert(active.result?.name === GAME_NAME, "Game Workspace active game did not resolve the validation game."); -- assert(Array.isArray(active.result?.members), "Game Workspace active game did not include members."); -+ assert(active.result?.name === GAME_NAME, "Game Hub active game did not resolve the validation game."); -+ assert(Array.isArray(active.result?.members), "Game Hub active game did not include members."); ++ await page.locator("[data-messages-segment-row]").filter({ hasText: "Danger is close." }).getByRole("button", { name: "Play Part" }).click(); ++ const expectedError = 'Selected TTS Profile "Calm Only Profile" does not include an Emotion Setting for "Urgent".'; ++ await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); ++ await expect(page.locator("[data-messages-validation-errors]")).toContainText(expectedError); ++ await expect(page.locator("[data-messages-playback-status]")).toHaveText(expectedError); + expect(await page.evaluate(() => window.__messagesSpeechCalls)).toEqual([]); - const gameRows = await adapter.getProductTableRows("game_workspace_games"); - assert(gameRows.some((row) => row.name === GAME_NAME), "Local Postgres game_workspace_games did not include the validation game."); -@@ -373,8 +373,8 @@ async function main() { - console.log(`PASS - Seeded identity rows: users=${identity.written.users}, roles=${identity.written.roles}, user_roles=${identity.written.user_roles}.`); - console.log(`PASS - Banner saved and read through API; local platform_settings banner rows observed=${apiEvidence.bannerRows}.`); - console.log(`PASS - Supabase Auth health calls=${fakeSupabase.requests.filter((request) => request.path === "/auth/v1/health").length}; Supabase platform_settings REST calls=0.`); -- console.log(`PASS - Game Workspace create/getActiveGame used repository ${apiEvidence.repositoryId}; local game rows observed=${apiEvidence.gameRows}.`); -- console.log("PASS - Validation cleanup restored banner rows and removed PR197 Game Workspace rows."); -+ console.log(`PASS - Game Hub create/getActiveGame used repository ${apiEvidence.repositoryId}; local game rows observed=${apiEvidence.gameRows}.`); -+ console.log("PASS - Validation cleanup restored banner rows and removed PR197 Game Hub rows."); + expect(failures.failedRequests).toEqual([]); + expect(failures.pageErrors).toEqual([]); + expect(failures.consoleErrors).toEqual([]); } finally { - await fakeSupabase.close(); - Object.entries(originalSupabaseEnv).forEach(([key, value]) => { -diff --git a/src/dev-runtime/admin/header-nav.local.html b/src/dev-runtime/admin/header-nav.local.html -index f2c805256..f49f136ce 100644 ---- a/src/dev-runtime/admin/header-nav.local.html -+++ b/src/dev-runtime/admin/header-nav.local.html -@@ -35,7 +35,7 @@ - Custom Extensions - Game Configuration - Game Design -- Game Hub -+ Game Hub - Game Journey - - -diff --git a/src/dev-runtime/persistence/mock-db-store.js b/src/dev-runtime/persistence/mock-db-store.js -index 543834d94..2305910bc 100644 ---- a/src/dev-runtime/persistence/mock-db-store.js -+++ b/src/dev-runtime/persistence/mock-db-store.js -@@ -34,7 +34,7 @@ export const MOCK_DB_SESSION_MODES = Object.freeze([ - ]); - - export const MOCK_DB_TOOL_GROUPS = Object.freeze({ -- "game-workspace": Object.freeze({ -+ "game-hub": Object.freeze({ - label: "Game Hub", - tableNames: Object.freeze(["game_workspace_games", "game_workspace_progress"]), - }), -diff --git a/src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js b/src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -index bd537110b..1bbbc2527 100644 ---- a/src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -+++ b/src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js -@@ -100,6 +100,7 @@ const GENERATED_ULID_SEQUENCE = Object.freeze({ - }); - const RECOMMENDED_TARGET_LINKED_RECORD_TYPE = "recommended-target"; - const RECOMMENDED_TARGET_NOTE_KEY = GAME_JOURNEY_KEYS.notes.designPass; -+const SOURCE_IDEA_LINKED_RECORD_TYPE = "source-idea-note"; - - export const GAME_JOURNEY_STATUSES = [ - { -@@ -645,6 +646,13 @@ export function createGameJourneyMockRepository(options = {}) { - return item?.linkedRecordType === RECOMMENDED_TARGET_LINKED_RECORD_TYPE; ++ await page.unroute("**/api/messages/tts-profiles"); + await closeMessagesRun(failures, page); + await fs.rm(sqlitePath, { force: true }); } +diff --git a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs +index c9a659a4d..bf68c80f4 100644 +--- a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs ++++ b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs +@@ -120,6 +120,8 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as + await expect(page.locator("[data-tts-profile-table]")).toContainText("Default Balanced Profile"); + await expect(page.locator("[data-tts-profile-table]")).toContainText("Man Profile 1"); + await expect(page.locator("[data-tts-profile-table]")).toContainText("Woman Profile 2"); ++ await expect(page.getByRole("columnheader", { exact: true, name: "Profile" })).toBeVisible(); ++ await expect(page.getByRole("columnheader", { name: "Age Filter" })).toBeVisible(); + const defaultProfileRow = page.locator("[data-tts-profile-row]").filter({ hasText: "Default Balanced Profile" }); + await expect(defaultProfileRow.getByRole("button", { name: "Delete" })).toBeDisabled(); + await expect(page.locator("[data-tts-emotion-host]")).toHaveCount(0); +@@ -130,7 +132,7 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as + await expect(defaultProfileRow.locator("[data-tts-profile-name-cell]")).toHaveAttribute("aria-expanded", "true"); + await expect(page.getByRole("heading", { name: "Emotion Settings" })).toBeVisible(); + await expect(page.getByRole("columnheader", { name: "Emotion", exact: true })).toBeVisible(); +- await expect(page.getByRole("columnheader", { name: "Preset" })).toBeVisible(); ++ await expect(page.getByRole("columnheader", { name: "Delivery Preset" })).toBeVisible(); + await expect(page.locator("[data-tts-emotion-row]")).toHaveCount(4); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" }).getByRole("button", { name: "Delete" })).toBeDisabled(); + await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Happy" })).toBeVisible(); +diff --git a/toolbox/messages/index.html b/toolbox/messages/index.html +index 55cbd6ccb..363fe4a34 100644 +--- a/toolbox/messages/index.html ++++ b/toolbox/messages/index.html +@@ -64,11 +64,11 @@ + + + +- ++ + + + +- ++ + + + +@@ -109,18 +109,12 @@ + + +
+- Speech Test ++ Playback +
+- +- +- +- +-

Selected item: None

+
+- +- ++ +
+-
Select a message row or segment row before testing speech.
++
Loading playback engine.
+
+
+
+diff --git a/toolbox/messages/messages.js b/toolbox/messages/messages.js +index 4af71adaa..5fdd0a47b 100644 +--- a/toolbox/messages/messages.js ++++ b/toolbox/messages/messages.js +@@ -39,20 +39,16 @@ const elements = { + persistenceEngine: document.querySelector("[data-messages-persistence-engine]"), + persistenceOwner: document.querySelector("[data-messages-persistence-owner]"), + persistenceSource: document.querySelector("[data-messages-persistence-source]"), +- previewStatus: document.querySelector("[data-messages-preview-status]"), +- previewTtsProfile: document.querySelector("[data-messages-preview-tts-profile]"), ++ playbackStatus: document.querySelector("[data-messages-playback-status]"), + selectedEmotion: document.querySelector("[data-messages-selected-emotion]"), + selectedName: document.querySelector("[data-messages-selected-name]"), + selectedSegment: document.querySelector("[data-messages-selected-segment]"), + selectedStatus: document.querySelector("[data-messages-selected-status]"), + selectedText: document.querySelector("[data-messages-selected-text]"), + segmentCount: document.querySelector("[data-messages-segment-count]"), +- speechTestTarget: document.querySelector("[data-messages-speech-test-target]"), + stopSpeech: document.querySelector("[data-messages-stop-speech]"), + table: document.querySelector("[data-messages-table]"), +- testSpeech: document.querySelector("[data-messages-test-speech]"), + ttsCount: document.querySelector("[data-messages-tts-count]"), +- ttsService: document.querySelector("[data-messages-tts-service]"), + validationCard: document.querySelector("[data-messages-validation-card]"), + validationErrors: document.querySelector("[data-messages-validation-errors]"), + }; +@@ -173,28 +169,6 @@ function createTtsProfileSelect(value, dataName, identityKey) { + return select; + } -+ function journeyGameKey(game) { -+ const gameId = String(game?.id || "").trim(); -+ return !gameId || gameId === GAME_JOURNEY_ROUTE_GAME_ALIAS -+ ? GAME_JOURNEY_KEYS.game -+ : `game-hub:${gameId}`; -+ } -+ - function currentUserCanSeeNote(note) { - const sessionUser = currentSessionUser(); - return Boolean(sessionUser.userKey && (sessionUser.isAdmin || note.ownerKey === sessionUser.userKey)); -@@ -679,6 +687,102 @@ export function createGameJourneyMockRepository(options = {}) { - persistTables(); - } +-function populateSelect(select, options, placeholder) { +- if (!select) { +- return; +- } +- const currentValue = select.value; +- select.replaceChildren(); +- const placeholderOption = document.createElement("option"); +- placeholderOption.value = ""; +- placeholderOption.textContent = placeholder; +- select.append(placeholderOption); +- options.forEach((optionValue) => { +- const option = document.createElement("option"); +- option.value = optionValue.key; +- option.textContent = optionValue.name; +- option.disabled = optionValue.disabled === true; +- select.append(option); +- }); +- select.value = options.some((optionValue) => optionValue.key === currentValue && optionValue.disabled !== true) +- ? currentValue +- : ""; +-} +- + function createField(labelText, field) { + const label = document.createElement("label"); + const span = document.createElement("span"); +@@ -307,57 +281,32 @@ function defaultTtsProfileKey() { + } -+ function slugSegment(value, fallback = "source-idea") { -+ const slug = String(value || "") -+ .toLowerCase() -+ .replace(/[^a-z0-9]+/g, "-") -+ .replace(/^-+|-+$/g, ""); -+ return slug || fallback; -+ } -+ -+ function normalizeSourceIdeaNotes(sourceIdea) { -+ return Array.isArray(sourceIdea?.notes) -+ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) -+ : []; -+ } -+ -+ function sourceIdeaJourneyNoteName(sourceIdea) { -+ const idea = String(sourceIdea?.idea || "").trim(); -+ return idea ? `Source Idea: ${idea}` : "Source Idea"; -+ } -+ -+ function ensureSourceIdeaJourneyItems(activeGame) { -+ const sourceIdea = activeGame?.sourceIdea && typeof activeGame.sourceIdea === "object" -+ ? activeGame.sourceIdea -+ : null; -+ const sourceNotes = normalizeSourceIdeaNotes(sourceIdea); -+ if (!activeGame || !sourceNotes.length) { -+ return []; -+ } -+ -+ const ownerKey = safeCurrentUserKey(); -+ const timestampValue = new Date().toISOString(); -+ const noteSlug = `source-idea-${slugSegment(activeGame.id || sourceIdea?.idea)}`; -+ let note = tables.game_journey_notes.find( -+ (candidate) => candidate.gameKey === activeGame.key && candidate.slug === noteSlug, -+ ); -+ -+ if (!note) { -+ note = { -+ key: makeUlid(nextNoteNumber), -+ slug: noteSlug, -+ gameKey: activeGame.key, -+ ownerKey, -+ name: sourceIdeaJourneyNoteName(sourceIdea), -+ typeKey: GAME_JOURNEY_KEYS.noteTypes.idea, -+ createdAt: timestampValue, -+ updatedAt: timestampValue, -+ createdBy: ownerKey, -+ updatedBy: ownerKey, -+ }; -+ nextNoteNumber += 1; -+ tables.game_journey_notes.push(note); -+ } -+ -+ const existingLinkedIds = new Set( -+ getItemsForNote(note.key) -+ .filter((item) => item.linkedRecordType === SOURCE_IDEA_LINKED_RECORD_TYPE) -+ .map((item) => item.linkedRecordId), -+ ); -+ const created = []; -+ sourceNotes.forEach((sourceNote, index) => { -+ const linkedRecordId = `${slugSegment(activeGame.id || activeGame.key)}:${index + 1}:${slugSegment(sourceNote).slice(0, 48)}`; -+ if (existingLinkedIds.has(linkedRecordId)) { -+ return; -+ } -+ const existingItems = getItemsForNote(note.key); -+ const item = { -+ key: makeUlid(nextItemNumber), -+ gameKey: activeGame.key, -+ noteKey: note.key, -+ status: "not-started", -+ title: sourceNote, -+ userDetails: "", -+ createdBy: ownerKey, -+ updatedBy: ownerKey, -+ templateKey: "", -+ linkedRecordType: SOURCE_IDEA_LINKED_RECORD_TYPE, -+ linkedRecordId, -+ indent: 0, -+ order: existingItems.length + 1, -+ createdAt: timestampValue, -+ updatedAt: timestampValue, -+ }; -+ nextItemNumber += 1; -+ tables.game_journey_items.push(item); -+ created.push(hydrateItem(item)); -+ }); -+ -+ if (created.length) { -+ selectedNoteKey = note.key; -+ selectedItemKey = created[0]?.key || selectedItemKey; -+ touchNote(note.key, ownerKey); -+ addActivity(activeGame.key, note.key, `Created ${created.length} Game Journey item${created.length === 1 ? "" : "s"} from Source Idea.`, ownerKey); -+ } -+ -+ return created; + function ttsProfileOptionByKey(profileKey) { +- return activeTtsProfileOptions().find((profile) => profile.key === profileKey) +- || activeTtsProfileOptions()[0] +- || DEFAULT_TTS_PROFILE; ++ const activeProfiles = activeTtsProfileOptions(); ++ if (!profileKey) { ++ return activeProfiles[0] || DEFAULT_TTS_PROFILE; + } -+ - function getActiveGame() { - const game = gameWorkspaceRepository.getActiveGame(); - if (!game) { -@@ -686,7 +790,7 @@ export function createGameJourneyMockRepository(options = {}) { - } - return { - ...game, -- key: GAME_JOURNEY_KEYS.game, -+ key: journeyGameKey(game), - }; - } ++ return activeProfiles.find((profile) => profile.key === profileKey) || null; + } -@@ -1392,7 +1496,11 @@ export function createGameJourneyMockRepository(options = {}) { - function openGame(gameId) { - const workspaceGameId = - gameId === GAME_JOURNEY_KEYS.game ? GAME_JOURNEY_ROUTE_GAME_ALIAS : gameId; -- return gameWorkspaceRepository.openGame(workspaceGameId); -+ const openedGame = gameWorkspaceRepository.openGame(workspaceGameId); -+ if (openedGame) { -+ ensureSourceIdeaJourneyItems(getActiveGame()); -+ } -+ return openedGame; - } + function selectedTtsProfileForMessage(messageKey) { +- return ttsProfileOptionByKey(state.messageTtsProfileKeys.get(messageKey) || defaultTtsProfileKey()); ++ const profileKey = state.messageTtsProfileKeys.get(messageKey); ++ return ttsProfileOptionByKey(profileKey || defaultTtsProfileKey()); + } - return { -diff --git a/src/dev-runtime/seed/server-seed-loader.mjs b/src/dev-runtime/seed/server-seed-loader.mjs -index 170f40137..f67e53065 100644 ---- a/src/dev-runtime/seed/server-seed-loader.mjs -+++ b/src/dev-runtime/seed/server-seed-loader.mjs -@@ -649,7 +649,7 @@ function humanToolStateSampleRows(generateKey) { - { displayName: "User 1", toolKey: "game-journey", userKey: SEED_DB_KEYS.users.user1 }, - { displayName: "User 2", toolKey: "palette", userKey: SEED_DB_KEYS.users.user2 }, - { displayName: "User 3", toolKey: "asset", userKey: SEED_DB_KEYS.users.user3 }, -- { displayName: "DavidQ", toolKey: "game-workspace", userKey: SEED_DB_KEYS.users.admin }, -+ { displayName: "DavidQ", toolKey: "game-hub", userKey: SEED_DB_KEYS.users.admin }, - ]; - return users.map((user, index) => { - const tool = toolByKey.get(user.toolKey) || tools[index] || { -diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs -index 9b98fca14..79a846f6d 100644 ---- a/src/dev-runtime/server/local-api-router.mjs -+++ b/src/dev-runtime/server/local-api-router.mjs -@@ -201,7 +201,7 @@ const DB_VIEWER_GROUP_ORDER = Object.freeze([ - Object.freeze({ id: "game-configuration", label: "Game Configuration", ownerId: "game-configuration", type: "tool" }), - Object.freeze({ id: "game-design", label: "Game Design", ownerId: "game-design", type: "tool" }), - Object.freeze({ id: "game-journey", label: "Game Journey", ownerId: "game-journey", type: "tool" }), -- Object.freeze({ id: "game-workspace", label: "Game Hub", ownerId: "game-workspace", type: "tool" }), -+ Object.freeze({ id: "game-hub", label: "Game Hub", ownerId: "game-hub", type: "tool" }), - Object.freeze({ id: "objects", label: "Objects", ownerId: "objects", type: "tool" }), - Object.freeze({ id: "palette", label: "Palette", ownerId: "palette", type: "tool" }), - Object.freeze({ id: "tags", label: "Tags", ownerId: "tags", type: "tool" }), -@@ -1427,7 +1427,7 @@ function normalizedToolKey(row) { - return String(row?.toolKey || row?.toolId || row?.id || "").trim(); + function selectedTtsProfileForSegment(segmentKey, messageKey = state.selectedMessageKey) { +- return ttsProfileOptionByKey( +- state.segmentTtsProfileKeys.get(segmentKey) +- || state.messageTtsProfileKeys.get(messageKey) +- || defaultTtsProfileKey(), +- ); +-} +- +-function selectedTtsProfile() { +- return ttsProfileOptionByKey(elements.previewTtsProfile?.value || defaultTtsProfileKey()); ++ const segmentProfileKey = state.segmentTtsProfileKeys.get(segmentKey); ++ if (segmentProfileKey) { ++ return ttsProfileOptionByKey(segmentProfileKey); ++ } ++ const messageProfileKey = state.messageTtsProfileKeys.get(messageKey); ++ if (messageProfileKey) { ++ return ttsProfileOptionByKey(messageProfileKey); ++ } ++ return ttsProfileOptionByKey(defaultTtsProfileKey()); } --const SOURCE_CONTROLLED_TOOLBOX_TOOL_IDS = new Set(["game-workspace", "idea-board", "messages", "tags", "text-to-speech", "users"]); -+const SOURCE_CONTROLLED_TOOLBOX_TOOL_IDS = new Set(["game-hub", "idea-board", "messages", "tags", "text-to-speech", "users"]); - const SOURCE_CONTROLLED_TOOLBOX_METADATA_FIELDS = Object.freeze([ - "active", - "adminOnly", -@@ -1450,6 +1450,20 @@ const SOURCE_CONTROLLED_TOOLBOX_METADATA_FIELDS = Object.freeze([ - "toolboxGroup", - "visibleInToolsList", - ]); -+ -+function isGameHubToolId(toolId) { -+ return ["game-hub", "game-workspace"].includes(String(toolId || "")); -+} -+ -+function isLegacyGameWorkspaceToolId(toolId) { -+ return String(toolId || "") === "game-workspace"; -+} -+ -+function withoutLegacyGameWorkspaceToolRows(rows) { -+ return Array.isArray(rows) -+ ? rows.filter((row) => !isLegacyGameWorkspaceToolId(normalizedToolKey(row))) -+ : []; -+} - const SOURCE_CONTROLLED_TOOLBOX_PLANNING_FIELDS = Object.freeze([ - "progressChecklist", - ]); -@@ -2027,7 +2041,7 @@ function gameWorkspaceGameKey(gameId) { - if (isUlidKey(normalizedGameId)) { - return normalizedGameId; - } -- return runtimeGeneratedKeyForSource(`game-workspace-game:${normalizedGameId}`); -+ return runtimeGeneratedKeyForSource(`game-hub-game:${normalizedGameId}`); + function selectedTtsService() { +- return state.ttsServices.find((service) => service.key === elements.ttsService?.value) || null; +-} +- +-function selectedSpeechTarget() { +- const segment = selectedSegment(); +- if (segment) { +- return { +- emotionProfile: emotionProfileByKey(segment.emotionProfileKey), +- id: segment.key, +- label: `Part ${segment.displayOrder}`, +- name: `${segment.messageName || "Message"} part ${segment.displayOrder}`, +- profile: selectedTtsProfileForSegment(segment.key, segment.messageKey), +- text: segment.segmentText, +- type: "part", +- }; +- } +- const message = selectedMessage(); +- if (!message) { +- return null; +- } +- return { +- emotionProfile: emotionProfileByKey(message.emotionProfileKey), +- id: message.key, +- label: `Message: ${message.name}`, +- name: message.name, +- profile: selectedTtsProfileForMessage(message.key), +- text: message.messageText, +- type: "message", +- }; ++ return state.ttsServices.find((service) => service.available) || null; } - function votePercent(count, total) { -@@ -2055,11 +2069,11 @@ function gameWorkspaceTables(repository) { - status: project.status, - })); - const activeGameKey = gameWorkspaceGameKey(activeGame?.id); -- return normalizeOwnedTables("game-workspace", { -+ return normalizeOwnedTables("game-hub", { - game_workspace_games: gameWorkspaceGames, - game_workspace_progress: activeGame ? [{ - ...snapshotAuditFields(80, SEED_DB_KEYS.users.user1), -- key: runtimeGeneratedKeyForSource(`game-workspace-progress:${activeGameKey}`), -+ key: runtimeGeneratedKeyForSource(`game-hub-progress:${activeGameKey}`), - gameKey: activeGameKey, - currentFocus: progress.currentFocus, - gameProgress: progress.gameProgress, -@@ -4680,8 +4694,14 @@ LIMIT 1; + function messageSegments(messageKey) { +@@ -409,78 +358,25 @@ function renderSelectedMessage() { + setText(elements.selectedText, segment?.segmentText || selected?.messageText || "No message selected."); + } - ensureToolboxToolMetadataRows() { - const rows = this.toolboxToolMetadataRows(); -- const activeTools = getActiveToolRegistry(); - let changed = false; -+ for (let index = rows.length - 1; index >= 0; index -= 1) { -+ if (isLegacyGameWorkspaceToolId(normalizedToolKey(rows[index]))) { -+ rows.splice(index, 1); -+ changed = true; -+ } -+ } -+ const activeTools = getActiveToolRegistry(); - activeTools.forEach((tool, index) => { - const defaults = this.defaultToolboxMetadata(tool, index); - const existingRow = rows.find((row) => (row.toolKey || row.toolId) === tool.id); -@@ -4796,7 +4816,7 @@ LIMIT 1; +-function renderTtsServiceOptions() { ++function refreshTtsServices() { + state.ttsServices = ttsServiceRegistry.listServices(); +- const options = state.ttsServices.map((service) => ({ +- disabled: !service.available, +- key: service.key, +- name: service.available ? service.name : `${service.name} unavailable`, +- })); +- populateSelect(elements.ttsService, options, "No TTS service available"); +- const availableService = state.ttsServices.find((service) => service.available); +- if (!elements.ttsService?.value && availableService) { +- elements.ttsService.value = availableService.key; +- } +- if (elements.ttsService) { +- elements.ttsService.disabled = !availableService; +- } + } - async supabaseToolboxToolMetadataRows() { - const adapter = this.supabaseDatabaseAdapter("Reading Supabase Toolbox tool metadata"); -- const rows = await adapter.getProductTableRows("toolbox_tool_metadata"); -+ const rows = withoutLegacyGameWorkspaceToolRows(await adapter.getProductTableRows("toolbox_tool_metadata")); - const activeTools = getActiveToolRegistry(); - const rowsByToolKey = new Map(rows.map((row) => [row.toolKey || row.toolId, row])); - const existingToolKeys = new Set(rowsByToolKey.keys()); -@@ -4818,7 +4838,7 @@ LIMIT 1; - }); - if (missingRows.length || syncedRows.length) { - await adapter.upsertProductTable("toolbox_tool_metadata", [...missingRows, ...syncedRows]); -- return adapter.getProductTableRows("toolbox_tool_metadata"); -+ return withoutLegacyGameWorkspaceToolRows(await adapter.getProductTableRows("toolbox_tool_metadata")); +-function renderTtsProfileOptions() { +- const activeProfiles = activeTtsProfileOptions(); +- populateSelect(elements.previewTtsProfile, activeProfiles, "Select TTS profile"); +- const selected = selectedTtsProfile(); +- if (!selected && activeProfiles[0]) { +- elements.previewTtsProfile.value = activeProfiles[0].key; +- } +- if (elements.previewTtsProfile) { +- elements.previewTtsProfile.disabled = activeProfiles.length === 0; +- } +-} +- +-function speechTestReadiness() { ++function playbackReadinessMessage() { ++ refreshTtsServices(); + const service = selectedTtsService(); +- const profile = selectedTtsProfile(); +- const target = selectedSpeechTarget(); + if (!service) { + const unavailableService = state.ttsServices.find((candidate) => !candidate.available); + if (unavailableService) { +- return { message: unavailableService.unavailableMessage || "No TTS service is available in this browser.", ok: false }; ++ return unavailableService.unavailableMessage || "No TTS service is available in this browser."; } - return rows; +- return { message: "No TTS service is selected.", ok: false }; +- } +- if (!service.available) { +- return { message: service.unavailableMessage || "Selected TTS service is unavailable.", ok: false }; +- } +- if (!target) { +- return { message: "Select a message row or segment row before testing speech.", ok: false }; +- } +- if (!profile) { +- return { message: "Select an active TTS profile before testing speech.", ok: false }; +- } +- if (!target.emotionProfile) { +- return { message: "Selected item needs an Emotion before testing speech.", ok: false }; +- } +- const emotionSetting = selectedEmotionSettingForProfile(profile, target.emotionProfile); +- if (!emotionSetting.ok) { +- return { message: emotionSetting.message, ok: false }; +- } +- if (!String(target.text || "").trim()) { +- return { message: "Selected item needs message text before testing speech.", ok: false }; ++ return "No TTS service is available in this browser."; } -@@ -4912,7 +4932,7 @@ LIMIT 1; - toolRegistrySnapshot() { - const planningRows = this.ensureToolboxToolPlanningRows(); - const planningByToolKey = new Map(planningRows.map((row) => [row.toolKey, row])); -- const tools = this.ensureToolboxToolMetadataRows() -+ const tools = withoutLegacyGameWorkspaceToolRows(this.ensureToolboxToolMetadataRows()) - .map((row, index) => serverRegistryTool({ - ...row, - ...planningByToolKey.get(normalizedToolKey(row)), -@@ -5082,7 +5102,7 @@ LIMIT 1; - repositoryForTool(toolId) { - this.assertProductDatabaseProvider(`Opening ${toolId} repository`); - if (toolId === "workspace") return this.gameWorkspaceRepository; -- if (toolId === "game-workspace") return this.gameWorkspaceRepository; -+ if (isGameHubToolId(toolId)) return this.gameWorkspaceRepository; - if (toolId === "game-design") return this.gameDesignRepository; - if (toolId === "game-configuration") return this.gameConfigurationRepository; - if (toolId === "objects") return this.objectsRepository; -@@ -5097,7 +5117,7 @@ LIMIT 1; - } - - constantsForTool(toolId) { -- if (toolId === "game-workspace") { -+ if (isGameHubToolId(toolId)) { - return { - GAME_WORKSPACE_MEMBER_ROLES, - GAME_WORKSPACE_GAME_PURPOSES, -diff --git a/src/shared/toolbox/tool-metadata-inventory.js b/src/shared/toolbox/tool-metadata-inventory.js -index 9c81eb6ef..7a4cb7587 100644 ---- a/src/shared/toolbox/tool-metadata-inventory.js -+++ b/src/shared/toolbox/tool-metadata-inventory.js -@@ -133,16 +133,16 @@ export const TOOL_REGISTRY = Object.freeze([ - "toolboxGroup": "Create" - }, - { -- "id": "game-workspace", -+ "id": "game-hub", - "name": "Game Hub", - "displayName": "Game Hub", - "shortDescription": "Coordinate Build, Play, and Share readiness for one game project.", - "shortLabel": "Game Hub", -- "path": "game-workspace", -- "folderName": "game-workspace", -- "entryPoint": "game-workspace/index.html", -- "badge": "/assets/theme-v2/images/badges/game-workspace.png", -- "tool": "/assets/theme-v2/images/tools/game-workspace.png", -+ "path": "game-hub", -+ "folderName": "game-hub", -+ "entryPoint": "game-hub/index.html", -+ "badge": "/assets/theme-v2/images/badges/game-hub.png", -+ "tool": "/assets/theme-v2/images/tools/game-hub.png", - "description": "Coordinate Build, Play, and Share readiness for one game project.", - "category": "Build/Create", - "colorGroup": "tool-group-build", -@@ -173,8 +173,8 @@ export const TOOL_REGISTRY = Object.freeze([ - "path": "game-journey", - "folderName": "game-journey", - "entryPoint": "game-journey/index.html", -- "badge": "/assets/theme-v2/images/badges/game-workspace.png", -- "tool": "/assets/theme-v2/images/tools/game-workspace.png", -+ "badge": "/assets/theme-v2/images/badges/game-hub.png", -+ "tool": "/assets/theme-v2/images/tools/game-hub.png", - "description": "Track game notes, nested work rows, status counts, and suggested toolbox handoffs.", - "category": "Build/Create", - "colorGroup": "tool-group-build", -@@ -184,7 +184,7 @@ export const TOOL_REGISTRY = Object.freeze([ - "requiredForTestable": true, - "requiredForPublish": true, - "requires": [ -- "game-workspace" -+ "game-hub" - ], - "status": "beta", - "releaseChannel": "beta", -@@ -219,7 +219,7 @@ export const TOOL_REGISTRY = Object.freeze([ - "requiredForTestable": true, - "requiredForPublish": true, - "requires": [ -- "game-workspace" -+ "game-hub" - ], - "status": "Ready", - "releaseChannel": "beta", -@@ -371,7 +371,7 @@ export const TOOL_REGISTRY = Object.freeze([ - "requiredForTestable": true, - "requiredForPublish": true, - "requires": [ -- "game-workspace" -+ "game-hub" - ], - "status": "beta", - "releaseChannel": "beta", -@@ -1680,7 +1680,7 @@ const AVAILABLE_TOOL_IMAGE_PATHS = Object.freeze([ - "/assets/theme-v2/images/badges/particles.png", - "/assets/theme-v2/images/badges/performance.png", - "/assets/theme-v2/images/badges/platform-settings.png", -- "/assets/theme-v2/images/badges/game-workspace.png", -+ "/assets/theme-v2/images/badges/game-hub.png", - "/assets/theme-v2/images/badges/publish-studio.png", - "/assets/theme-v2/images/badges/publish.png", - "/assets/theme-v2/images/badges/ratings.png", -@@ -1743,7 +1743,7 @@ const AVAILABLE_TOOL_IMAGE_PATHS = Object.freeze([ - "/assets/theme-v2/images/tools/particles.png", - "/assets/theme-v2/images/tools/performance.png", - "/assets/theme-v2/images/tools/platform-settings.png", -- "/assets/theme-v2/images/tools/game-workspace.png", -+ "/assets/theme-v2/images/tools/game-hub.png", - "/assets/theme-v2/images/tools/publish-studio.png", - "/assets/theme-v2/images/tools/publish.png", - "/assets/theme-v2/images/tools/ratings.png", -diff --git a/tests/dev-runtime/DevRuntimeBoundary.test.mjs b/tests/dev-runtime/DevRuntimeBoundary.test.mjs -index f2908a876..0b5b1660f 100644 ---- a/tests/dev-runtime/DevRuntimeBoundary.test.mjs -+++ b/tests/dev-runtime/DevRuntimeBoundary.test.mjs -@@ -32,7 +32,7 @@ const retiredToolboxDevRuntimeFiles = [ - "toolbox/game-configuration/game-configuration-mock-repository.js", - "toolbox/game-design/game-design-mock-repository.js", - "toolbox/game-journey/game-journey-mock-repository.js", -- "toolbox/game-workspace/game-workspace-mock-repository.js", -+ "toolbox/game-hub/game-workspace-mock-repository.js", - ]; - - const retiredBrowserCompatibilityModules = new Set([ -@@ -123,6 +123,6 @@ test("server seed loading and guest package loading use dev-runtime modules with - assert.doesNotMatch(router, /mock-db-state/); - assert.match(router, /parts\[1\] === "guest"/); - assert.match(router, /parts\[2\] === "seed"/); -- assert.doesNotMatch(router, /toolbox\/(?:assets|colors|game-configuration|game-design|game-journey|game-workspace)\/.*mock-repository/); -+ assert.doesNotMatch(router, /toolbox\/(?:assets|colors|game-configuration|game-design|game-journey|game-hub)\/.*mock-repository/); - assert.doesNotMatch(router, /toolbox\/colors\/palette-source-mock-db/); - }); -diff --git a/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs b/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -index f0a342677..15fd5ae10 100644 ---- a/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -+++ b/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -@@ -115,7 +115,7 @@ test("Active browser product-data entrypoints use API or service clients", () => - "toolbox/game-configuration/game-configuration-api-client.js", - "toolbox/game-design/game-design-api-client.js", - "toolbox/game-journey/game-journey-api-client.js", -- "toolbox/game-workspace/game-workspace-api-client.js", -+ "toolbox/game-hub/game-hub-api-client.js", - "toolbox/objects/objects-api-client.js", - "toolbox/tags/tags-api-client.js", - ]; -@@ -128,7 +128,7 @@ test("Active browser product-data entrypoints use API or service clients", () => - - assert.match(readRepoFile("admin/tool-votes.js"), /src\/engine\/api\/toolbox-votes-api-client\.js/); - assert.match(readRepoFile("toolbox/tools-page-accordions.js"), /src\/engine\/api\/toolbox-votes-api-client\.js/); -- assert.match(readRepoFile("assets/theme-v2/js/account-achievements.js"), /createServerRepositoryClient\("game-workspace"\)/); -+ assert.match(readRepoFile("assets/theme-v2/js/account-achievements.js"), /createServerRepositoryClient\("game-hub"\)/); - - [ - "admin/tool-votes.js", -diff --git a/tests/dev-runtime/SupabaseProductDataCutover.test.mjs b/tests/dev-runtime/SupabaseProductDataCutover.test.mjs -index e28122bb9..5a1e05d3d 100644 ---- a/tests/dev-runtime/SupabaseProductDataCutover.test.mjs -+++ b/tests/dev-runtime/SupabaseProductDataCutover.test.mjs -@@ -382,13 +382,13 @@ test("Supabase-selected toolbox repositories open and persist through product ta - }, async () => { - const server = await startApiServer(); - try { -- const repository = await apiJson(server.baseUrl, "/api/toolbox/game-workspace/repositories", { -+ const repository = await apiJson(server.baseUrl, "/api/toolbox/game-hub/repositories", { - body: { options: {} }, - method: "POST", - }); -- assert.match(repository.repositoryId, /^game-workspace-\d+$/); -+ assert.match(repository.repositoryId, /^game-hub-\d+$/); - -- const snapshot = await apiJson(server.baseUrl, `/api/toolbox/game-workspace/repositories/${repository.repositoryId}/methods/getSnapshot`, { -+ const snapshot = await apiJson(server.baseUrl, `/api/toolbox/game-hub/repositories/${repository.repositoryId}/methods/getSnapshot`, { - body: { args: [] }, - method: "POST", - }); -diff --git a/tests/playwright/account/AchievementsPage.spec.mjs b/tests/playwright/account/AchievementsPage.spec.mjs -index 4f6594aa2..9f27a0091 100644 ---- a/tests/playwright/account/AchievementsPage.spec.mjs -+++ b/tests/playwright/account/AchievementsPage.spec.mjs -@@ -97,7 +97,7 @@ test("account achievements page switches Build Play Share views", async ({ page - await expect(page.getByText("Games I shared")).toBeHidden(); - await expect(page.locator("[data-achievements-build-created-count]")).toHaveText("4"); - await expect(page.locator("[data-achievements-build-ready-count]")).toHaveText("0"); -- await expect(page.locator("[data-achievements-build-status]")).toContainText("Game Workspace game source"); -+ await expect(page.locator("[data-achievements-build-status]")).toContainText("Game Hub game source"); - const buildRows = await page.locator("[data-achievements-build-rows] tr").evaluateAll((rows) => rows.map((row) => { - const cells = Array.from(row.querySelectorAll("td")).map((cell) => cell.textContent.trim()); - return { -diff --git a/tests/playwright/tools/AdminDbViewer.spec.mjs b/tests/playwright/tools/AdminDbViewer.spec.mjs -index 216019fd9..d4d9eb713 100644 ---- a/tests/playwright/tools/AdminDbViewer.spec.mjs -+++ b/tests/playwright/tools/AdminDbViewer.spec.mjs -@@ -169,7 +169,7 @@ function configuredDbViewerGroups(tableNames) { - { id: "game-configuration", label: "Game Configuration", tableNames: include(["game_configuration_records", "game_configuration_validation_items"]), type: "tool" }, - { id: "game-design", label: "Game Design", tableNames: include(["game_design_documents", "game_design_validation_items"]), type: "tool" }, - { id: "game-journey", label: "Game Journey", tableNames: include(["game_journey_completion_metrics", "game_journey_note_types", "game_journey_notes", "game_journey_templates", "game_journey_items", "game_journey_activity"]), type: "tool" }, -- { id: "game-workspace", label: "Game Hub", tableNames: include(["game_workspace_games", "game_workspace_progress"]), type: "tool" }, -+ { id: "game-hub", label: "Game Hub", tableNames: include(["game_workspace_games", "game_workspace_progress"]), type: "tool" }, - { id: "objects", label: "Objects", tableNames: include(["object_definition_records"]), type: "tool" }, - { id: "palette", label: "Palette", tableNames: include(["palette_colors", "palette_source_swatches", "palette_swatch_usages", "project_workspace_palette_globals"]), type: "tool" }, - { id: "tags", label: "Tags", tableNames: include(["workspace_tag_records"]), type: "tool" }, -diff --git a/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs -similarity index 91% -rename from tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -rename to tests/playwright/tools/GameHubMockRepository.spec.mjs -index 1c1f742a9..2d583e1d7 100644 ---- a/tests/playwright/tools/GameWorkspaceMockRepository.spec.mjs -+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs -@@ -99,15 +99,15 @@ function startFakeSupabaseServer() { - test.beforeAll(async () => { - previousSupabaseEnv = Object.fromEntries(SUPABASE_ENV_KEYS.map((key) => [key, process.env[key]])); - fakeSupabaseServer = await startFakeSupabaseServer(); -- process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "game-workspace-anon-key"; -- process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://game-workspace:test@127.0.0.1:5432/game_workspace"; -- process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "game-workspace-service-role-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "game-hub-anon-key"; -+ delete process.env.GAMEFOUNDRY_DATABASE_URL; -+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "game-hub-service-role-key"; - process.env.GAMEFOUNDRY_SUPABASE_URL = fakeSupabaseServer.baseUrl; - }); +- return { message: `Ready to test ${target.label}.`, ok: true }; ++ return "Ready for Message Studio playback."; + } - test.beforeEach(async ({ page }) => { - await installPlaywrightStorageIsolation(page, { -- lane: "game-workspace", -+ lane: "game-hub", - surface: "game workspace mock repository" - }); - }); -@@ -183,7 +183,7 @@ async function openRepoPage(page, pathName, options = {}) { - }); +-function renderSpeechTestControls() { +- renderTtsServiceOptions(); +- renderTtsProfileOptions(); +- const target = selectedSpeechTarget(); +- const readiness = speechTestReadiness(); +- setText(elements.speechTestTarget, target?.label || "None"); +- setText(elements.previewStatus, readiness.message); +- if (elements.testSpeech) { +- elements.testSpeech.disabled = !readiness.ok; +- } ++function renderPlaybackControls() { ++ setText(elements.playbackStatus, playbackReadinessMessage()); + if (elements.stopSpeech) { + elements.stopSpeech.disabled = !selectedTtsService()?.available; } - -- if (pathName.includes("/toolbox/game-workspace/") || pathName.includes("/toolbox/project-workspace/")) { -+ if (pathName.includes("/toolbox/game-hub/") || pathName.includes("/toolbox/project-workspace/")) { - await page.route("**/api/platform-settings/banner", async (route) => { - await route.fulfill({ - contentType: "application/json", -@@ -235,7 +235,7 @@ test("Deprecated project workspace route points creators to Game Hub", async ({ - await expect(page.getByRole("heading", { name: "Game Hub" })).toBeVisible(); - await expect(page.locator("main")).toContainText("This route is kept for older links."); - await expect(page.locator("main")).not.toContainText("Project Workspace"); -- await expect(page.getByRole("link", { name: "Open Game Hub" })).toHaveAttribute("href", "toolbox/game-workspace/index.html"); -+ await expect(page.getByRole("link", { name: "Open Game Hub" })).toHaveAttribute("href", "toolbox/game-hub/index.html"); - - await expectNoPageFailures(failures); - } finally { -@@ -244,7 +244,7 @@ test("Deprecated project workspace route points creators to Game Hub", async ({ - }); - - test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html", { session: creatorSession() }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); - - try { - await expect(page.locator(".tool-workspace")).toBeVisible(); -@@ -275,7 +275,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { - await expect(page.locator("[data-game-list]")).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."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Created and opened Launch Test Game."); - - await page.getByLabel("Game Name").fill("Archive Game"); - await page.getByRole("button", { name: "Create Game" }).click(); -@@ -284,12 +284,12 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { - await page.getByRole("button", { name: "Open Launch Test Game" }).click(); - await expect(page.locator("[data-active-game-name]")).toHaveText("Launch Test Game"); - await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Open Launch Test Game (Active)" })).toHaveAttribute("data-game-active", "true"); -- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Opened Launch Test Game."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Opened Launch Test Game."); - - await page.getByRole("button", { name: "Delete Open Game" }).click(); - await expect(page.locator("[data-active-game-name]")).not.toHaveText("Launch Test Game"); - await expect(page.locator("[data-game-list]")).not.toContainText("Launch Test Game"); -- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Deleted Launch Test Game."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Deleted Launch Test Game."); - - await expectNoPageFailures(failures); - } finally { -@@ -298,7 +298,7 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { - }); - - test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - try { - await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); -@@ -314,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("Sign in to create or update Game Hub projects."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Sign in to create or update Game Hub projects."); - - await expectNoPageFailures(failures); - } finally { -@@ -323,7 +323,7 @@ test("Game Hub preserves guest browsing and blocks guest saves", 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 page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { - await route.fulfill({ - body: JSON.stringify({ - error: "Active game unavailable for validation.", -@@ -334,12 +334,12 @@ test("Game Hub shows active-game errors without throwing", async ({ page }) => { - status: 502, - }); - }); -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - 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 is temporarily unavailable."); -+ await expect(page.locator("[data-game-hub-log]")).toContainText("Active game is temporarily unavailable."); - expect(failures.pageErrors).toEqual([]); - expect(failures.consoleErrors.filter((message) => !message.includes("status of 502"))).toEqual([]); - } finally { -@@ -348,7 +348,7 @@ test("Game Hub shows active-game errors without throwing", async ({ page }) => { - }); - - test("Game Hub reports malformed active-game payloads without throwing", async ({ page }) => { -- await page.route("**/api/toolbox/game-workspace/repositories/*/methods/getActiveGame", async (route) => { -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { - await route.fulfill({ - body: JSON.stringify({ - data: { -@@ -364,12 +364,12 @@ test("Game Hub reports malformed active-game payloads without throwing", async ( - status: 200, - }); - }); -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - 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 is temporarily unavailable."); -+ await expect(page.locator("[data-game-hub-log]")).toContainText("Active game is temporarily unavailable."); - await expect(page.getByLabel("Game Purpose")).toBeDisabled(); - - await expectNoPageFailures(failures); -@@ -379,7 +379,7 @@ test("Game Hub reports malformed active-game payloads without throwing", async ( - }); - - test("Game Hub displays and edits game purpose and member role", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html", { session: creatorSession() }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); - - try { - await expect(page.locator("#gamePurposeInput option")).toHaveText([ -@@ -411,15 +411,15 @@ test("Game Hub displays and edits game purpose and member role", async ({ page } - - await page.getByLabel("Game Purpose").selectOption("Learning Game"); - await expect(page.locator("[data-active-game-purpose]")).toHaveText("Learning Game"); -- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Updated Demo Game purpose to Learning Game."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Updated Demo Game purpose to Learning Game."); - - await page.getByLabel("Game Status").selectOption("Ready for Testing"); - await expect(page.locator("[data-active-game-status]")).toHaveText("Ready for Testing"); -- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Updated Demo Game status to Ready for Testing."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Updated Demo Game status to Ready for Testing."); - - await page.getByLabel("Current User Role").selectOption("Designer"); - await expect(page.locator("[data-current-user-role]")).toHaveText("Designer"); -- await expect(page.locator("[data-game-workspace-log]")).toHaveText("Updated current user role to Designer."); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Updated current user role to Designer."); - - await page.getByLabel("Game Purpose").selectOption("Capability Demo"); - await page.getByLabel("Game Name").fill("Purpose Review Game"); -@@ -436,7 +436,7 @@ test("Game Hub displays and edits game purpose and member role", async ({ page } - }); - - test("Game Hub progress panels update from mock game state", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html", { session: creatorSession() }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); - - try { - await expect(page.locator("[data-game-status]")).toHaveText("Under Construction"); -@@ -452,7 +452,7 @@ test("Game Hub progress panels update from mock game state", async ({ page }) => - 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 staticOverlay = panel.querySelector("[data-game-hub-foundation]"); - const outputPanels = panel.querySelector("[data-game-output-panels]"); - return Boolean( - projectInformation && -@@ -484,7 +484,7 @@ test("Game Hub progress panels update from mock game state", async ({ page }) => - - test("Game Hub uses the wide Theme V2 tool layout at desktop widths", async ({ page }) => { - await page.setViewportSize({ width: 1440, height: 1100 }); -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - try { - await expect(page.locator(".container--tool-wide")).toBeVisible(); -@@ -609,7 +609,7 @@ test("Toolbox member-role filters focus tools without exposing admin-only contro - const failures = await openRepoPage(page, "/toolbox/index.html"); - - try { -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 11/39"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); - await expect(page.locator("[data-toolbox-role-focus]")).toHaveCount(0); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Hub$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Journey$/ }) })).toBeVisible(); -@@ -620,7 +620,7 @@ test("Toolbox member-role filters focus tools without exposing admin-only contro - - await page.goto(`${failures.server.baseUrl}/toolbox/index.html?memberRole=Designer`, { waitUntil: "networkidle" }); - await expect(page.locator("[data-toolbox-role-focus='Designer']")).toBeVisible(); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 7/39"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 8/43"); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Hub$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Journey$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Design$/ }) })).toBeVisible(); -@@ -632,15 +632,16 @@ test("Toolbox member-role filters focus tools without exposing admin-only contro - - await page.goto(`${failures.server.baseUrl}/toolbox/index.html?memberRole=Audio%20Creator`, { waitUntil: "networkidle" }); - await expect(page.locator("[data-toolbox-role-focus='Audio Creator']")).toBeVisible(); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 1/39"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 2/43"); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Assets$/ }) })).toBeVisible(); -+ await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Text To Speech$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Audio$/ }) })).toHaveCount(0); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^MIDI$/ }) })).toHaveCount(0); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Design$/ }) })).toHaveCount(0); - - await page.goto(`${failures.server.baseUrl}/toolbox/index.html?memberRole=Viewer`, { waitUntil: "networkidle" }); - await expect(page.locator("[data-toolbox-role-focus='Viewer']")).toBeVisible(); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 9/39"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 10/43"); - await expect(page.getByText("Viewer focus shows preview-safe read-only tiles only.")).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Hub$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Game Journey$/ }) })).toBeVisible(); -@@ -649,7 +650,7 @@ test("Toolbox member-role filters focus tools without exposing admin-only contro - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Assets$/ }) })).toBeVisible(); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Debug$/ }) })).toHaveCount(0); - await page.goto(`${failures.server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 11/39"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^Cloud$/ }) })).toHaveCount(0); - - await expectNoPageFailures(failures); -diff --git a/tests/playwright/tools/GameJourneyTool.spec.mjs b/tests/playwright/tools/GameJourneyTool.spec.mjs -index 2cc895fef..429f6f381 100644 ---- a/tests/playwright/tools/GameJourneyTool.spec.mjs -+++ b/tests/playwright/tools/GameJourneyTool.spec.mjs -@@ -1444,8 +1444,8 @@ test("Game Journey requires an active game before editing", async ({ page }) => - await expect(page.getByRole("button", { name: "Update Item" })).toBeDisabled(); - await expect(page.getByRole("button", { name: "Add Note", exact: true })).toBeDisabled(); - await expect(page.locator("[data-journey-title-input]")).toBeDisabled(); -- await expect(page.locator("[data-journey-note-type-select]")).toBeDisabled(); -- await expect(page.locator("[data-journey-new-note-type]")).toBeDisabled(); -+ await expect(page.locator("[data-journey-note-type-select]")).toHaveCount(0); -+ await expect(page.locator("[data-journey-new-note-type]")).toHaveCount(0); - - await expectNoPageFailures(failures); - } finally { -@@ -1454,7 +1454,7 @@ test("Game Journey requires an active game before editing", async ({ page }) => - }); - - test("Game Hub hands the active game route to Game Journey", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - try { - const journeyLink = page.getByRole("link", { name: "Open Game Journey" }); -diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -index a18205fa4..934faf46f 100644 ---- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -@@ -346,15 +346,22 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - 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(["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 page.waitForURL(/\/toolbox\/game-hub\/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.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0); - await expect(page.locator("main")).not.toContainText(/\bproject records\b|\bAPI\b|\bDB\b|\bmock\b|\bseed\b|\bdebug\b|\binternal\b/i); -+ await page.getByRole("link", { name: "Open Game Journey" }).click(); -+ await page.waitForURL(/\/toolbox\/game-journey\/index\.html\?game=lantern-reef-\d+$/); -+ await expect(page.locator("[data-journey-active-game]")).toHaveText("Active game: Lantern Reef."); -+ await expect(page.locator("[data-journey-summary-body]")).toContainText("Source Idea: Lantern Reef"); -+ await expect(page.locator("[data-journey-summary-body]")).toContainText("10000011"); -+ await expect(page.locator("[data-journey-recent-activity]")).toContainText("Created 1 Game Journey item from Source Idea."); - -- expect(mutatingApiRequests.some((request) => request.includes("/api/toolbox/game-workspace/repositories"))).toBe(true); -+ expect(mutatingApiRequests.some((request) => request.includes("/api/toolbox/game-hub/repositories"))).toBe(true); - expect(mutatingApiRequests.some((request) => request.includes("/methods/createGame"))).toBe(true); - expect(failedRequests).toEqual([]); - expect(pageErrors).toEqual([]); -diff --git a/tests/playwright/tools/RootToolsFutureState.spec.mjs b/tests/playwright/tools/RootToolsFutureState.spec.mjs -index ba97f5c6a..8110dbe04 100644 ---- a/tests/playwright/tools/RootToolsFutureState.spec.mjs -+++ b/tests/playwright/tools/RootToolsFutureState.spec.mjs -@@ -277,7 +277,7 @@ test("root tools surface links current tool pages without old_* routes", async ( - await expect(page.getByRole("button", { name: "Progress" })).toHaveCount(0); - await expect(page.getByRole("button", { name: "Build Path" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Build Path" })).not.toHaveAttribute("aria-disabled", "true"); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/44"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); - await expect(page.locator("[data-toolbox-admin-nav-group]")).toHaveCount(0); - await expect(page.locator("nav.nav-links > .nav-item > a[data-route='admin']")).toHaveCount(0); - await expect(page.locator("nav.nav-links > a[data-route='learn']")).toHaveCount(1); -@@ -311,15 +311,18 @@ test("root tools surface links current tool pages without old_* routes", async ( - await expect(page.getByText("Build Path Wireframe")).toHaveCount(0); - await expect(page.locator("[data-toolbox-wireframe]")).toHaveCount(0); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -- await expect(page.locator("main a.btn[data-toolbox-launch-link='Game Hub'][href='../toolbox/game-workspace/index.html']")).toBeVisible(); -+ const readyGameWorkspaceCard = page.locator("main .control-card").filter({ -+ has: page.locator("h3", { hasText: "Game Hub" }) -+ }); -+ await expect(readyGameWorkspaceCard.locator("a.btn")).toHaveAttribute("href", "../toolbox/game-hub/index.html"); - const defaultToolLabels = await page.locator("main [data-tools-accordion-list] .control-card h3").evaluateAll((labels) => labels.map((label) => label.textContent.trim())); -- expect(defaultToolLabels).toEqual(["Achievements", "Assets", "Colors", "Controls", "Game Configuration", "Game Design", "Game Hub", "Game Hub", "Game Journey", "Idea Board", "Languages", "Message Studio", "Objects", "Saved Data", "Tags", "Text To Speech"]); -+ expect(defaultToolLabels).toEqual(["Achievements", "Assets", "Colors", "Controls", "Game Configuration", "Game Design", "Game Hub", "Game Journey", "Idea Board", "Languages", "Message Studio", "Objects", "Saved Data", "Tags", "Text To Speech"]); - const textToSpeechCard = page.locator("main .control-card").filter({ - has: page.locator("h3", { hasText: /^Text To Speech$/ }) - }); - await expect(textToSpeechCard.locator("[data-toolbox-tool-name-link='Text To Speech']")).toHaveAttribute("href", "/toolbox/text-to-speech/index.html"); - await expect(textToSpeechCard.locator("[data-toolbox-tool-name-link='Text To Speech']")).toHaveAttribute("data-registered-tool-route", "toolbox/text-to-speech/index.html"); -- await expect(page.locator("[data-toolbox-readiness]")).toHaveText(["Wireframe", "Beta", "Complete", "Wireframe", "Beta", "Beta", "Beta", "Beta", "Beta", "Wireframe", "Wireframe", "Beta", "Beta", "Wireframe", "Beta", "Beta"]); -+ await expect(page.locator("[data-toolbox-readiness]")).toHaveText(["Wireframe", "Beta", "Complete", "Wireframe", "Beta", "Beta", "Beta", "Beta", "Wireframe", "Wireframe", "Beta", "Beta", "Wireframe", "Beta", "Beta"]); - await expect(page.locator("main .control-card").filter({ has: page.locator("h3", { hasText: /^AI Command Center$/ }) })).toHaveCount(0); - const oldStandaloneLabels = [ - ["Palette", "Manager"].join(" "), -@@ -389,7 +392,7 @@ test("root tools surface links current tool pages without old_* routes", async ( - const guestGroupLabels = await page.locator("[data-tools-accordion-list] details[data-tools-accordion]").evaluateAll((groups) => ( - groups.map((group) => group.dataset.toolsAccordion) +@@ -503,7 +399,7 @@ function createMessageEditRows(message = null) { + + const ttsCell = document.createElement("td"); + ttsCell.append(createTtsProfileSelect( +- message ? selectedTtsProfileForMessage(message.key).key : defaultTtsProfileKey(), ++ message ? selectedTtsProfileForMessage(message.key)?.key : defaultTtsProfileKey(), + "messageDefaultTtsProfile", + key, + )); +@@ -551,7 +447,7 @@ function createMessageSegmentTable() { + table.setAttribute("aria-label", "Message parts"); + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); +- ["Text", "Emotion", "TTS Profile", "Status", "Actions"].forEach((label) => { ++ ["Part Text", "Emotion", "TTS Profile", "Status", "Actions"].forEach((label) => { + const header = document.createElement("th"); + header.scope = "col"; + header.textContent = label; +@@ -587,7 +483,7 @@ function createMessageSegmentTable() { )); -- expect(guestGroupLabels).toEqual(["Idea", "Design", "Graphics", "Audio", "Objects", "Interface", "Controls", "Progression", "Publish"]); -+ expect(guestGroupLabels).toEqual(["Idea", "Design", "Graphics", "Audio", "Objects", "Interface", "Controls", "Progression"]); - await expect(page.locator("[data-tools-accordion='Admin']")).toHaveCount(0); - await expect(page.getByRole("button", { name: "Progress" })).toHaveCount(0); - await expect(page.locator("[data-tools-accordion-list] .control-card h3", { hasText: /^Progress$/ })).toHaveCount(0); -@@ -415,13 +418,13 @@ test("root tools surface links current tool pages without old_* routes", async ( - expect(failedRequests.filter((request) => request.includes("/toolbox/old_"))).toEqual([]); - - await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/44"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); - await expect(page.locator("main").getByText("Users", { exact: true })).toHaveCount(0); - await expect(page.locator("main").getByText("Creators", { exact: true })).toHaveCount(0); - await expect(page.locator("[data-toolbox-admin-nav-group]")).toHaveCount(0); - await setServerSession(server, MOCK_DB_KEYS.users.admin); - await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/47"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/46"); - await expect(page.locator("[data-toolbox-admin-nav-group]")).toHaveCount(0); - const adminLabels = await page.locator("main [data-tools-accordion-list] .control-card h3").evaluateAll((labels) => labels.map((label) => label.textContent.trim())); - expect(adminLabels).toEqual(defaultToolLabels); -@@ -432,7 +435,7 @@ test("root tools surface links current tool pages without old_* routes", async ( - "Platform Settings" - ])); - await expect(page.locator("main .control-card").filter({ -- has: page.locator("[data-toolbox-tool-name-link='Game Hub'][href='/toolbox/game-workspace/index.html']") -+ has: page.locator("[data-toolbox-tool-name-link='Game Hub'][href='/toolbox/game-hub/index.html']") - }).locator("[data-toolbox-readiness]")).toHaveText("Beta"); - await expect(page.locator("main .control-card").filter({ - has: page.locator("h3", { hasText: /^Game Configuration$/ }) -@@ -458,7 +461,7 @@ test("root tools surface links current tool pages without old_* routes", async ( - const adminGroupLabels = await page.locator("[data-tools-accordion-list] details[data-tools-accordion]").evaluateAll((groups) => ( - groups.map((group) => group.dataset.toolsAccordion) + const ttsCell = document.createElement("td"); + ttsCell.append(createTtsProfileSelect( +- selectedTtsProfileForSegment(segment.key, segment.messageKey).key, ++ selectedTtsProfileForSegment(segment.key, segment.messageKey)?.key || "", + "segmentTtsProfile", + segment.key, )); -- expect(adminGroupLabels).toEqual(["Idea", "Design", "Graphics", "Audio", "Objects", "Interface", "Controls", "Progression", "Publish"]); -+ expect(adminGroupLabels).toEqual(["Idea", "Design", "Graphics", "Audio", "Objects", "Interface", "Controls", "Progression"]); - await expect(page.getByRole("button", { name: "Progress" })).toHaveCount(0); - await page.getByRole("button", { name: "Build Path" }).click(); - await expect(page.locator("[data-build-path-table='workflow']")).toBeVisible(); -@@ -468,7 +471,7 @@ test("root tools surface links current tool pages without old_* routes", async ( - await expect(page.locator("[data-toolbox-admin-nav-group]")).toHaveCount(0); - await setServerSession(server, ""); - await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); -- await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/44"); -+ await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); - await expect(page.locator("main").getByText("Users", { exact: true })).toHaveCount(0); - await expect(page.locator("main").getByText("Creators", { exact: true })).toHaveCount(0); - expect(pageErrors).toEqual([]); -@@ -570,7 +573,7 @@ test("learn wireframe pages load with shared Theme V2 structure", async ({ page - headings: ["Desktop Target", "Ideal Width", "Minimum Comfortable Width", "Panel Layout", "Smaller Screens", "Wireframe Status"] - }, - { -- path: "/learn/game-workspace/index.html", -+ path: "/learn/game-hub/index.html", - headings: ["Overview", "Quick Start", "Common Tasks", "Related Documentation", "Related Videos", "Examples"] - }, - { -@@ -664,7 +667,7 @@ test("tool template future-state page loads from root Theme V2 paths", async ({ - test("representative active tool pages align center cleanup and registry group colors", async ({ page }) => { - const representativeToolIds = [ - "ai-assistant", -- "game-workspace", -+ "game-hub", - "game-design", - "colors", - "audio", -diff --git a/tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs b/tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs -index ace2d1a03..45de2a0c6 100644 ---- a/tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs -+++ b/tests/playwright/tools/ToolDisplayModeNavigation.spec.mjs -@@ -225,8 +225,8 @@ test("Game Design renders identity and navigation rows with registry anchor link - }); - - test("Game Hub and Game Configuration use registry order without page hardcoding", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -- const gameWorkspaceNavigation = getToolNavigationTargets("game-workspace"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); -+ const gameWorkspaceNavigation = getToolNavigationTargets("game-hub"); - const gameConfigurationNavigation = getToolNavigationTargets("game-configuration"); - - try { -diff --git a/tests/playwright/tools/ToolImageRegistry.spec.mjs b/tests/playwright/tools/ToolImageRegistry.spec.mjs -index d4d03175a..3827e38e8 100644 ---- a/tests/playwright/tools/ToolImageRegistry.spec.mjs -+++ b/tests/playwright/tools/ToolImageRegistry.spec.mjs -@@ -157,7 +157,7 @@ test("Toolbox cards consume registry image sources and expose visible image diag - - test("representative tool pages consume registry images in Tool Display Mode", async ({ page }) => { - const toolCases = [ -- { path: "/toolbox/game-workspace/index.html", toolId: "game-workspace" }, -+ { path: "/toolbox/game-hub/index.html", toolId: "game-hub" }, - { path: "/toolbox/game-design/index.html", toolId: "game-design" }, - { path: "/toolbox/game-configuration/index.html", toolId: "game-configuration" }, - { path: "/toolbox/build-game/index.html", toolId: "build-game" } -diff --git a/tests/playwright/tools/ToolNavigationPrevNext.spec.mjs b/tests/playwright/tools/ToolNavigationPrevNext.spec.mjs -index b2c71bdc9..d2aab4f10 100644 ---- a/tests/playwright/tools/ToolNavigationPrevNext.spec.mjs -+++ b/tests/playwright/tools/ToolNavigationPrevNext.spec.mjs -@@ -125,8 +125,8 @@ test("Tool Display Mode renders build-order previous and next controls", async ( - }); - - test("Game Hub Tool Display Mode follows registry route targets", async ({ page }) => { -- const failures = await openRepoPage(page, "/toolbox/game-workspace/index.html"); -- const navigation = getToolNavigationTargets("game-workspace"); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); -+ const navigation = getToolNavigationTargets("game-hub"); +@@ -627,7 +523,7 @@ function createSegmentEditRow(segment = null) { + + const ttsCell = document.createElement("td"); + ttsCell.append(createTtsProfileSelect( +- segment ? selectedTtsProfileForSegment(segment.key, segment.messageKey).key : selectedTtsProfileForMessage(state.selectedMessageKey).key, ++ segment ? selectedTtsProfileForSegment(segment.key, segment.messageKey)?.key : selectedTtsProfileForMessage(state.selectedMessageKey)?.key, + "segmentTtsProfile", + key, + )); +@@ -687,7 +583,7 @@ function renderMessageRows() { + nameCell.textContent = `${isExpanded ? "v" : ">"} ${message.name}`; + const ttsCell = document.createElement("td"); + ttsCell.append(createTtsProfileSelect( +- selectedTtsProfileForMessage(message.key).key, ++ selectedTtsProfileForMessage(message.key)?.key || "", + "messageDefaultTtsProfile", + message.key, + )); +@@ -721,7 +617,7 @@ function render(persistence = {}) { + renderSelectedMessage(); + renderCounts(); + renderPersistence(persistence); +- renderSpeechTestControls(); ++ renderPlaybackControls(); + } - try { - await expectNavigationTarget(page.locator("[data-tool-nav-previous]"), "Previous", navigation.previous); -diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs -index 0cf4d475c..12e463488 100644 ---- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs -+++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs -@@ -778,7 +778,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from - await expect(adminBuildVoteRow).toHaveAttribute("draggable", "true"); - await page.evaluate(() => { - const source = document.querySelector("[data-toolbox-votes-tool-id='build-game']"); -- const target = document.querySelector("[data-toolbox-votes-tool-id='game-workspace']"); -+ const target = document.querySelector("[data-toolbox-votes-tool-id='game-hub']"); - if (!source || !target) { - throw new Error("Toolbox vote drag/drop rows were not available."); - } -@@ -799,7 +799,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from - }); - await expect(page.locator("[data-toolbox-votes-status]")).toContainText("Rows were renumbered with whole-number order values."); - await expect(adminBuildVoteRow.locator("td").nth(1)).toHaveText("1"); -- await expect(page.locator("[data-toolbox-votes-tool-id='game-workspace'] td").nth(1)).toHaveText("2"); -+ await expect(page.locator("[data-toolbox-votes-tool-id='game-hub'] td").nth(1)).toHaveText("2"); - await expect(adminBuildVoteRow).toHaveAttribute("aria-selected", "true"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(5)).toHaveText("1"); - await expect(page.locator("[data-toolbox-votes-tool-id='publish'] td").nth(7)).toHaveText("1"); -diff --git a/toolbox/colors/index.html b/toolbox/colors/index.html -index 4c7e98f44..a0e9e8276 100644 ---- a/toolbox/colors/index.html -+++ b/toolbox/colors/index.html -@@ -34,7 +34,7 @@ -
-

No active project.

-

Active project required.

-- Open Game Hub -+ Open Game Hub -
-
-
-@@ -222,7 +222,7 @@ -

Active Project Required

- -

Open Game Hub before editing project swatches.

-- Open Game Hub -+ Open Game Hub - - -
-diff --git a/toolbox/controls/controls.js b/toolbox/controls/controls.js -index 901288790..c3355a91e 100644 ---- a/toolbox/controls/controls.js -+++ b/toolbox/controls/controls.js -@@ -642,7 +642,7 @@ function showWorkspaceReturnIfNeeded() { - const params = new URLSearchParams(window.location.search); - const returnTo = normalizeText(params.get("returnTo")); - const shouldShow = params.has("workspace") || params.has("project") || params.get("source") === "workspace" || params.has("workspaceLaunch") || returnTo; -- if (returnTo.startsWith("/toolbox/game-workspace/")) { -+ if (returnTo.startsWith("/toolbox/game-hub/")) { - elements.returnWorkspace.href = returnTo; + function editorValue(root, selector) { +@@ -912,36 +808,16 @@ async function disableSegment(key) { } - elements.returnWorkspace.hidden = !shouldShow; -diff --git a/toolbox/controls/index.html b/toolbox/controls/index.html -index dc3d404eb..5e23377e8 100644 ---- a/toolbox/controls/index.html -+++ b/toolbox/controls/index.html -@@ -19,7 +19,7 @@ -
Toolbox / Controls
-

Controls

-

Define game-owned controls with normalized actions and creator-facing usage labels.

--

-+

-
- -
-diff --git a/toolbox/game-design/index.html b/toolbox/game-design/index.html -index 14215637c..3335515ac 100644 ---- a/toolbox/game-design/index.html -+++ b/toolbox/game-design/index.html -@@ -46,7 +46,7 @@ - Actions - -@@ -142,7 +142,7 @@ -

Missing Game

- -

Open or seed a Game Hub game before saving Game Design.

-- Open Game Hub -+ Open Game Hub - - -
- - --
-+
-

Project Information

-

Review the open project and its source idea.

-
Project Information ready.
-@@ -118,7 +118,7 @@ - - - --
-+
-
-
-
Game Foundation
-@@ -134,7 +134,7 @@ -

Recommended Next Tool

Game Configuration

-

Checklist

  • Game identity: Complete
-
--
Game Hub ready.
-+
Game Hub ready.
-
-
-
-@@ -179,7 +179,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/game-journey/game-journey.js b/toolbox/game-journey/game-journey.js -index effb985da..c2b2f876c 100644 ---- a/toolbox/game-journey/game-journey.js -+++ b/toolbox/game-journey/game-journey.js -@@ -84,6 +84,7 @@ let completionMetricsDiagnostic = ""; - let addNoteRowOpen = false; - let editingNoteKey = ""; - let itemTree = null; -+let routeForcesNoActiveGame = false; - const recommendedTargetValues = new Map( - GAME_JOURNEY_RECOMMENDED_TARGETS.map((target) => [target.key, target.suggestedCount]), - ); -@@ -112,11 +113,12 @@ function refreshCompletionMetricsSnapshot() { - function applyInitialGameRoute() { - const gameId = params.get("game"); - if (gameId === "none") { -- repository.clearActiveGame(); -+ routeForcesNoActiveGame = true; - return; + function speakTarget(service, target, profile) { +@@ -1004,7 +880,7 @@ function playPart(key) { } + clearValidation(); + const message = `Play Part queued ${target.label} using ${target.profile.name}.`; +- setText(elements.previewStatus, message); ++ setText(elements.playbackStatus, message); + setText(elements.log, message); + } - if (gameId) { -+ routeForcesNoActiveGame = false; - repository.openGame(gameId); +@@ -1029,7 +905,7 @@ function playMessage(key) { } + clearValidation(); + const message = `Play Message queued ${parts.length} parts for ${messageRecord.name}.`; +- setText(elements.previewStatus, message); ++ setText(elements.playbackStatus, message); + setText(elements.log, message); + } -@@ -125,6 +127,14 @@ function applyInitialGameRoute() { +@@ -1041,7 +917,7 @@ function stopSpeech() { } + clearValidation(); + const message = `Message Studio playback stopped. Cleared ${result.stoppedCount} queued item${result.stoppedCount === 1 ? "" : "s"}.`; +- setText(elements.previewStatus, message); ++ setText(elements.playbackStatus, message); + setText(elements.log, message); } -+function routedActiveGame() { -+ return routeForcesNoActiveGame ? null : repository.getActiveGame(); -+} -+ -+function routedNotes(filterId) { -+ return routeForcesNoActiveGame ? [] : repository.listNotes(filterId); -+} -+ - function createElement(tagName, options = {}) { - const element = document.createElement(tagName); - if (options.className) { -@@ -1419,9 +1429,9 @@ function selectFirstVisibleNote(notes) { +@@ -1076,24 +952,12 @@ async function moveSegment(key, direction) { + } } - function render() { -- const activeGame = repository.getActiveGame(); -+ const activeGame = routedActiveGame(); - const searchQuery = currentSearchQuery(); -- const notes = applySearch(repository.listNotes(activeFilter), searchQuery); -+ const notes = applySearch(routedNotes(activeFilter), searchQuery); - const note = selectFirstVisibleNote(notes); - ensureSelectedItemMatchesFilter(note); - const displayNote = filterNoteItemsForDisplay(note); -@@ -1651,7 +1661,7 @@ summaryBody.addEventListener("input", (event) => { - repository.updateItem(selectedItem.key, { - userDetails: detailsInput.value, - }); -- renderDiagnostics(repository.getActiveGame(), repository.getSelectedNote(), applySearch(repository.listNotes(activeFilter), currentSearchQuery())); -+ renderDiagnostics(routedActiveGame(), routeForcesNoActiveGame ? null : repository.getSelectedNote(), applySearch(routedNotes(activeFilter), currentSearchQuery())); - }); - - addItemButton.addEventListener("click", () => { -diff --git a/toolbox/game-workspace/game-workspace-api-client.js b/toolbox/game-workspace/game-workspace-api-client.js -deleted file mode 100644 -index 191602e40..000000000 ---- a/toolbox/game-workspace/game-workspace-api-client.js -+++ /dev/null -@@ -1,24 +0,0 @@ --import { -- createServerRepositoryClient, -- requireServerApiData, -- readServerToolConstants, -- requireServerConstant, -- safeRequestServerApi, --} from "../../src/api/server-api-client.js"; +-elements.previewTtsProfile?.addEventListener("change", () => { +- renderSpeechTestControls(); +-}); - --const constants = readServerToolConstants("game-workspace"); +-elements.ttsService?.addEventListener("change", () => { +- renderSpeechTestControls(); +-}); - --export const GAME_WORKSPACE_MEMBER_ROLES = Object.freeze(requireServerConstant(constants, "GAME_WORKSPACE_MEMBER_ROLES", "game-workspace")); --export const GAME_WORKSPACE_GAME_PURPOSES = Object.freeze(requireServerConstant(constants, "GAME_WORKSPACE_GAME_PURPOSES", "game-workspace")); --export const GAME_WORKSPACE_GAME_STATUSES = Object.freeze(requireServerConstant(constants, "GAME_WORKSPACE_GAME_STATUSES", "game-workspace")); -- --export function createGameWorkspaceApiRepository(options = {}) { -- return createServerRepositoryClient("game-workspace", options); --} +-elements.testSpeech?.addEventListener("click", () => { +- testSelectedSpeech(); +-}); - --export function readProjectWorkspaceProjectRecords() { -- return requireServerApiData( -- safeRequestServerApi("/project-workspace/projects"), -- "Game Hub project records", -- ); --} -diff --git a/toolbox/idea-board/index.js b/toolbox/idea-board/index.js -index 74364a996..d1e339336 100644 ---- a/toolbox/idea-board/index.js -+++ b/toolbox/idea-board/index.js -@@ -3,7 +3,7 @@ import { createServerRepositoryClient } from "../../src/api/server-api-client.js - const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); - const defaultVisibleStatuses = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project"]); - const userId = "user-1"; --const gameHubRoute = "toolbox/game-workspace/index.html"; -+const gameHubRoute = "toolbox/game-hub/index.html"; - let gameHubRepository = null; + elements.stopSpeech?.addEventListener("click", () => { + stopSpeech(); + }); - const ideaTable = [ -@@ -142,7 +142,7 @@ function isRepositoryErrorResult(value) { + ttsServiceRegistry.onServicesChanged(() => { +- renderSpeechTestControls(); ++ renderPlaybackControls(); + }); - function gameHubProjectRepository() { - if (!gameHubRepository) { -- gameHubRepository = createServerRepositoryClient("game-workspace"); -+ gameHubRepository = createServerRepositoryClient("game-hub"); + elements.table?.addEventListener("change", (event) => { +@@ -1104,7 +968,7 @@ elements.table?.addEventListener("change", (event) => { + if (key && key !== NEW_ROW_KEY) { + state.messageTtsProfileKeys.set(key, messageSelect.value || defaultTtsProfileKey()); + } +- renderSpeechTestControls(); ++ renderPlaybackControls(); + setText(elements.log, "Default TTS profile selected for this message playback session."); } - return gameHubRepository; - } -diff --git a/toolbox/project-workspace/index.html b/toolbox/project-workspace/index.html -index 42c6f2bc5..a2e46640e 100644 ---- a/toolbox/project-workspace/index.html -+++ b/toolbox/project-workspace/index.html -@@ -19,7 +19,7 @@ -
Toolbox / Game Hub
-

Game Hub

-

This route is kept for older links. The active workspace is Game Hub.

--

Open Game Hub

-+

Open Game Hub

-
-
- -diff --git a/toolbox/tools-page-accordions.js b/toolbox/tools-page-accordions.js -index 8c5c6061b..f2b6641e1 100644 ---- a/toolbox/tools-page-accordions.js -+++ b/toolbox/tools-page-accordions.js -@@ -1,7 +1,7 @@ - import { -- GAME_WORKSPACE_MEMBER_ROLES, -- createGameWorkspaceApiRepository --} from "./game-workspace/game-workspace-api-client.js"; -+ GAME_HUB_MEMBER_ROLES, -+ createGameHubApiRepository -+} from "./game-hub/game-hub-api-client.js"; - import { - readGameJourneyCompletionMetrics - } from "../src/api/game-journey-completion-api-client.js"; -@@ -34,10 +34,10 @@ import { getSessionCurrent } from "../src/api/session-api-client.js"; - const statusFilterList = document.querySelector("[data-toolbox-status-filters]"); - const launchStatus = document.querySelector("[data-toolbox-launch-status]"); - const searchParams = new URLSearchParams(window.location.search); -- const gameWorkspaceRepository = createGameWorkspaceApiRepository(); -+ const gameHubRepository = createGameHubApiRepository(); - const urlMemberRole = searchParams.get("memberRole"); - const defaultGameMemberRole = "Owner"; -- const gameMemberRole = GAME_WORKSPACE_MEMBER_ROLES.includes(urlMemberRole) -+ const gameMemberRole = GAME_HUB_MEMBER_ROLES.includes(urlMemberRole) - ? urlMemberRole - : defaultGameMemberRole; - const session = getSessionCurrent(); -@@ -154,7 +154,7 @@ import { getSessionCurrent } from "../src/api/session-api-client.js"; - "game-journey": "Progression", - "game-migration": "Publish", - "game-testing": "Play Test", -- "game-workspace": "Design", -+ "game-hub": "Design", - "hitboxes": "Controls", - "idea-board": "Idea", - "input-mapping-v2": "Controls", -@@ -183,7 +183,7 @@ import { getSessionCurrent } from "../src/api/session-api-client.js"; - const toolboxGroupPositions = new Map(toolboxGroupOrder.map((group, index) => [group, index])); - const workflowToolOrderByGroup = Object.freeze({ - "Design": Object.freeze({ -- "game-workspace": 1, -+ "game-hub": 1, - "users": 2, - "game-crew": 2, - "game-configuration": 3, -@@ -244,8 +244,8 @@ import { getSessionCurrent } from "../src/api/session-api-client.js"; + if (segmentSelect) { +@@ -1112,7 +976,7 @@ elements.table?.addEventListener("change", (event) => { + if (key && key !== NEW_ROW_KEY) { + state.segmentTtsProfileKeys.set(key, segmentSelect.value || defaultTtsProfileKey()); } - - function getGameProgressSummary() { -- const activeGame = gameWorkspaceRepository.getActiveGame(); -- const progress = gameWorkspaceRepository.getGameProgress(); -+ const activeGame = gameHubRepository.getActiveGame(); -+ const progress = gameHubRepository.getGameProgress(); - return { - activeGameName: activeGame?.name || "No active game", - gameProgress: progress.gameProgress, +- renderSpeechTestControls(); ++ renderPlaybackControls(); + setText(elements.log, "TTS profile selected for this part playback session."); + } + }); +diff --git a/toolbox/text-to-speech/index.html b/toolbox/text-to-speech/index.html +index 7989b8309..9f509977a 100644 +--- a/toolbox/text-to-speech/index.html ++++ b/toolbox/text-to-speech/index.html +@@ -67,11 +67,11 @@ +
Message NameMessageTypeStatusPartsDefault TTS ProfileTTS ProfileActions
+ + +- ++ + + + +- ++ + + + +diff --git a/toolbox/text-to-speech/text2speech.js b/toolbox/text-to-speech/text2speech.js +index 5174c8d5e..b8b2afcce 100644 +--- a/toolbox/text-to-speech/text2speech.js ++++ b/toolbox/text-to-speech/text2speech.js +@@ -663,7 +663,7 @@ function initializeTextToSpeechTool(root = document, { engine = new TextToSpeech + table.setAttribute("aria-label", "Emotion Settings"); + const thead = document.createElement("thead"); + const headerRow = document.createElement("tr"); +- ["Emotion", "Pitch", "Rate", "Volume", "Preset", "Status", "Actions"].forEach((label) => { ++ ["Emotion", "Pitch", "Rate", "Volume", "Delivery Preset", "Status", "Actions"].forEach((label) => { + const header = document.createElement("th"); + header.scope = "col"; + header.textContent = label; diff --git a/tests/playwright/tools/MessagesTool.spec.mjs b/tests/playwright/tools/MessagesTool.spec.mjs index 6c00fa6f5..69068a2e2 100644 --- a/tests/playwright/tools/MessagesTool.spec.mjs +++ b/tests/playwright/tools/MessagesTool.spec.mjs @@ -167,11 +167,16 @@ test("Message Studio renders Messages with child Message Parts and plays ordered await expect(page.locator("[data-messages-tts-add-row]")).toHaveCount(0); await expect(page.locator("[data-messages-emotions]")).toHaveCount(0); await expect(page.locator("[data-messages-tts-profiles]")).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-preview-tts-profile]")).toHaveCount(0); + await expect(page.locator("[data-messages-tts-service]")).toHaveCount(0); + await expect(page.locator("[data-messages-test-speech]")).toHaveCount(0); + await expect(page.getByText("Speech Test", { exact: true })).toHaveCount(0); + await expect(page.getByRole("columnheader", { name: "Message", exact: true })).toBeVisible(); + await expect(page.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible(); await expect(page.locator("[data-messages-segment-count]")).toHaveText("0"); await expect(page.locator("[data-messages-add-control-row]")).toBeVisible(); - await expect(page.getByRole("button", { name: "Stop" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Stop Playback" })).toBeEnabled(); + await expect(page.locator("[data-messages-playback-status]")).toHaveText("Ready for Message Studio playback."); await page.getByRole("button", { name: "Add Message" }).click(); await expect(page.locator("[data-messages-row-editor='__new__']")).toBeVisible(); @@ -209,7 +214,7 @@ test("Message Studio renders Messages with child Message Parts and plays ordered await expect(page.getByRole("heading", { name: "Message Parts" })).toBeVisible(); const partsTable = page.getByLabel("Message parts"); await expect(partsTable.getByRole("columnheader", { name: "Order" })).toHaveCount(0); - await expect(partsTable.getByRole("columnheader", { name: "Text" })).toBeVisible(); + await expect(partsTable.getByRole("columnheader", { name: "Part Text" })).toBeVisible(); await expect(partsTable.getByRole("columnheader", { name: "Emotion" })).toBeVisible(); await expect(partsTable.getByRole("columnheader", { exact: true, name: "TTS Profile" })).toBeVisible(); await expect(partsTable.getByRole("columnheader", { name: "Status" })).toBeVisible(); @@ -251,6 +256,20 @@ test("Message Studio renders Messages with child Message Parts and plays ordered volume: 1, }), ])); + const urgentPartRow = page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }); + await urgentPartRow.locator("[data-segment-tts-profile]").evaluate((select) => { + const option = document.createElement("option"); + option.value = "missing-profile"; + option.textContent = "Missing Profile"; + select.append(option); + select.value = option.value; + select.dispatchEvent(new Event("change", { bubbles: true })); + }); + await urgentPartRow.getByRole("button", { name: "Play Part" }).click(); + await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); + await expect(page.locator("[data-messages-validation-errors]")).toContainText("Select a TTS profile before playback."); + await expect(page.locator("[data-messages-playback-status]")).toHaveText("Select a TTS profile before playback."); + await page.locator("[data-messages-segment-row]").filter({ hasText: "Keep your torch high." }).locator("[data-segment-tts-profile]").selectOption({ label: "Default Balanced TTS Profile" }); 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."); @@ -274,7 +293,7 @@ test("Message Studio renders Messages with child Message Parts and plays ordered type: "speak", voiceName: "Test Voice", })); - await page.getByRole("button", { name: "Stop" }).click(); + await page.getByRole("button", { name: "Stop Playback" }).click(); await expect(page.locator("[data-messages-log]")).toHaveText("Message Studio playback stopped. Cleared 2 queued items."); speechCalls = await page.evaluate(() => window.__messagesSpeechCalls); expect(speechCalls.at(-1)).toEqual({ type: "cancel" }); @@ -359,13 +378,72 @@ test("Message Studio shows actionable playback error when audio engine is unavai 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."); + await expect(page.locator("[data-messages-playback-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([]); + expect(failures.consoleErrors).toEqual([]); + } finally { + await closeMessagesRun(failures, page); + await fs.rm(sqlitePath, { force: true }); + } +}); + +test("Message Studio shows actionable playback error when selected TTS profile lacks the selected emotion", async ({ page }) => { + const sqlitePath = messagesDbPath(); + await fs.rm(sqlitePath, { force: true }); + await page.route("**/api/messages/tts-profiles", async (route) => { + if (route.request().method() !== "GET") { + await route.continue(); + return; + } + await route.fulfill({ + contentType: "application/json", + body: JSON.stringify({ + data: { + persistence: { owner: "messages" }, + ttsProfiles: [{ + active: true, + emotionSettings: [{ active: true, emotion: "calm", emotionLabel: "Calm", pitch: 1, rate: 1, ssmlLikePreset: "normal", volume: 1 }], + key: "calm-only-profile", + language: "en-US", + name: "Calm Only Profile", + providerKey: "browser-speech", + voiceName: "", + }], + }, + ok: true, + }), + }); + }); + const failures = await openMessagesPage(page, sqlitePath); + + try { + await addMessage(page, { + emotion: "Urgent", + name: "Urgent Encounter", + text: "Danger is close.", + }); + await openMessageParts(page, "Urgent Encounter"); + await addPart(page, { + emotion: "Urgent", + order: 1, + text: "Danger is close.", + }); + + await page.locator("[data-messages-segment-row]").filter({ hasText: "Danger is close." }).getByRole("button", { name: "Play Part" }).click(); + const expectedError = 'Selected TTS Profile "Calm Only Profile" does not include an Emotion Setting for "Urgent".'; + await expect(page.locator("[data-messages-validation-card]")).toBeVisible(); + await expect(page.locator("[data-messages-validation-errors]")).toContainText(expectedError); + await expect(page.locator("[data-messages-playback-status]")).toHaveText(expectedError); expect(await page.evaluate(() => window.__messagesSpeechCalls)).toEqual([]); expect(failures.failedRequests).toEqual([]); expect(failures.pageErrors).toEqual([]); expect(failures.consoleErrors).toEqual([]); } finally { + await page.unroute("**/api/messages/tts-profiles"); await closeMessagesRun(failures, page); await fs.rm(sqlitePath, { force: true }); } diff --git a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs index c9a659a4d..bf68c80f4 100644 --- a/tests/playwright/tools/TextToSpeechFunctional.spec.mjs +++ b/tests/playwright/tools/TextToSpeechFunctional.spec.mjs @@ -120,6 +120,8 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as await expect(page.locator("[data-tts-profile-table]")).toContainText("Default Balanced Profile"); await expect(page.locator("[data-tts-profile-table]")).toContainText("Man Profile 1"); await expect(page.locator("[data-tts-profile-table]")).toContainText("Woman Profile 2"); + await expect(page.getByRole("columnheader", { exact: true, name: "Profile" })).toBeVisible(); + await expect(page.getByRole("columnheader", { name: "Age Filter" })).toBeVisible(); const defaultProfileRow = page.locator("[data-tts-profile-row]").filter({ hasText: "Default Balanced Profile" }); await expect(defaultProfileRow.getByRole("button", { name: "Delete" })).toBeDisabled(); await expect(page.locator("[data-tts-emotion-host]")).toHaveCount(0); @@ -130,7 +132,7 @@ test("Text To Speech page loads and speaks through browser speech synthesis", as await expect(defaultProfileRow.locator("[data-tts-profile-name-cell]")).toHaveAttribute("aria-expanded", "true"); await expect(page.getByRole("heading", { name: "Emotion Settings" })).toBeVisible(); await expect(page.getByRole("columnheader", { name: "Emotion", exact: true })).toBeVisible(); - await expect(page.getByRole("columnheader", { name: "Preset" })).toBeVisible(); + await expect(page.getByRole("columnheader", { name: "Delivery Preset" })).toBeVisible(); await expect(page.locator("[data-tts-emotion-row]")).toHaveCount(4); await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Neutral" }).getByRole("button", { name: "Delete" })).toBeDisabled(); await expect(page.locator("[data-tts-emotion-row]").filter({ hasText: "Happy" })).toBeVisible(); diff --git a/toolbox/messages/index.html b/toolbox/messages/index.html index 55cbd6ccb..363fe4a34 100644 --- a/toolbox/messages/index.html +++ b/toolbox/messages/index.html @@ -64,11 +64,11 @@

Messages

Profile NameProfileVoiceLanguageGenderAgeAge FilterEmotion CountStatusActions
- + - + @@ -109,18 +109,12 @@

Inspector

- Speech Test + Playback
- - - - -

Selected item: None

- - +
-
Select a message row or segment row before testing speech.
+
Loading playback engine.
diff --git a/toolbox/messages/messages.js b/toolbox/messages/messages.js index 4af71adaa..5fdd0a47b 100644 --- a/toolbox/messages/messages.js +++ b/toolbox/messages/messages.js @@ -39,20 +39,16 @@ const elements = { persistenceEngine: document.querySelector("[data-messages-persistence-engine]"), persistenceOwner: document.querySelector("[data-messages-persistence-owner]"), persistenceSource: document.querySelector("[data-messages-persistence-source]"), - previewStatus: document.querySelector("[data-messages-preview-status]"), - previewTtsProfile: document.querySelector("[data-messages-preview-tts-profile]"), + playbackStatus: document.querySelector("[data-messages-playback-status]"), selectedEmotion: document.querySelector("[data-messages-selected-emotion]"), selectedName: document.querySelector("[data-messages-selected-name]"), selectedSegment: document.querySelector("[data-messages-selected-segment]"), selectedStatus: document.querySelector("[data-messages-selected-status]"), selectedText: document.querySelector("[data-messages-selected-text]"), segmentCount: document.querySelector("[data-messages-segment-count]"), - speechTestTarget: document.querySelector("[data-messages-speech-test-target]"), stopSpeech: document.querySelector("[data-messages-stop-speech]"), table: document.querySelector("[data-messages-table]"), - testSpeech: document.querySelector("[data-messages-test-speech]"), ttsCount: document.querySelector("[data-messages-tts-count]"), - ttsService: document.querySelector("[data-messages-tts-service]"), validationCard: document.querySelector("[data-messages-validation-card]"), validationErrors: document.querySelector("[data-messages-validation-errors]"), }; @@ -173,28 +169,6 @@ function createTtsProfileSelect(value, dataName, identityKey) { return select; } -function populateSelect(select, options, placeholder) { - if (!select) { - return; - } - const currentValue = select.value; - select.replaceChildren(); - const placeholderOption = document.createElement("option"); - placeholderOption.value = ""; - placeholderOption.textContent = placeholder; - select.append(placeholderOption); - options.forEach((optionValue) => { - const option = document.createElement("option"); - option.value = optionValue.key; - option.textContent = optionValue.name; - option.disabled = optionValue.disabled === true; - select.append(option); - }); - select.value = options.some((optionValue) => optionValue.key === currentValue && optionValue.disabled !== true) - ? currentValue - : ""; -} - function createField(labelText, field) { const label = document.createElement("label"); const span = document.createElement("span"); @@ -307,57 +281,32 @@ function defaultTtsProfileKey() { } function ttsProfileOptionByKey(profileKey) { - return activeTtsProfileOptions().find((profile) => profile.key === profileKey) - || activeTtsProfileOptions()[0] - || DEFAULT_TTS_PROFILE; + const activeProfiles = activeTtsProfileOptions(); + if (!profileKey) { + return activeProfiles[0] || DEFAULT_TTS_PROFILE; + } + return activeProfiles.find((profile) => profile.key === profileKey) || null; } function selectedTtsProfileForMessage(messageKey) { - return ttsProfileOptionByKey(state.messageTtsProfileKeys.get(messageKey) || defaultTtsProfileKey()); + const profileKey = state.messageTtsProfileKeys.get(messageKey); + return ttsProfileOptionByKey(profileKey || defaultTtsProfileKey()); } function selectedTtsProfileForSegment(segmentKey, messageKey = state.selectedMessageKey) { - return ttsProfileOptionByKey( - state.segmentTtsProfileKeys.get(segmentKey) - || state.messageTtsProfileKeys.get(messageKey) - || defaultTtsProfileKey(), - ); -} - -function selectedTtsProfile() { - return ttsProfileOptionByKey(elements.previewTtsProfile?.value || defaultTtsProfileKey()); + const segmentProfileKey = state.segmentTtsProfileKeys.get(segmentKey); + if (segmentProfileKey) { + return ttsProfileOptionByKey(segmentProfileKey); + } + const messageProfileKey = state.messageTtsProfileKeys.get(messageKey); + if (messageProfileKey) { + return ttsProfileOptionByKey(messageProfileKey); + } + return ttsProfileOptionByKey(defaultTtsProfileKey()); } function selectedTtsService() { - return state.ttsServices.find((service) => service.key === elements.ttsService?.value) || null; -} - -function selectedSpeechTarget() { - const segment = selectedSegment(); - if (segment) { - return { - emotionProfile: emotionProfileByKey(segment.emotionProfileKey), - id: segment.key, - label: `Part ${segment.displayOrder}`, - name: `${segment.messageName || "Message"} part ${segment.displayOrder}`, - profile: selectedTtsProfileForSegment(segment.key, segment.messageKey), - text: segment.segmentText, - type: "part", - }; - } - const message = selectedMessage(); - if (!message) { - return null; - } - return { - emotionProfile: emotionProfileByKey(message.emotionProfileKey), - id: message.key, - label: `Message: ${message.name}`, - name: message.name, - profile: selectedTtsProfileForMessage(message.key), - text: message.messageText, - type: "message", - }; + return state.ttsServices.find((service) => service.available) || null; } function messageSegments(messageKey) { @@ -409,78 +358,25 @@ function renderSelectedMessage() { setText(elements.selectedText, segment?.segmentText || selected?.messageText || "No message selected."); } -function renderTtsServiceOptions() { +function refreshTtsServices() { state.ttsServices = ttsServiceRegistry.listServices(); - const options = state.ttsServices.map((service) => ({ - disabled: !service.available, - key: service.key, - name: service.available ? service.name : `${service.name} unavailable`, - })); - populateSelect(elements.ttsService, options, "No TTS service available"); - const availableService = state.ttsServices.find((service) => service.available); - if (!elements.ttsService?.value && availableService) { - elements.ttsService.value = availableService.key; - } - if (elements.ttsService) { - elements.ttsService.disabled = !availableService; - } } -function renderTtsProfileOptions() { - const activeProfiles = activeTtsProfileOptions(); - populateSelect(elements.previewTtsProfile, activeProfiles, "Select TTS profile"); - const selected = selectedTtsProfile(); - if (!selected && activeProfiles[0]) { - elements.previewTtsProfile.value = activeProfiles[0].key; - } - if (elements.previewTtsProfile) { - elements.previewTtsProfile.disabled = activeProfiles.length === 0; - } -} - -function speechTestReadiness() { +function playbackReadinessMessage() { + refreshTtsServices(); const service = selectedTtsService(); - const profile = selectedTtsProfile(); - const target = selectedSpeechTarget(); if (!service) { const unavailableService = state.ttsServices.find((candidate) => !candidate.available); if (unavailableService) { - return { message: unavailableService.unavailableMessage || "No TTS service is available in this browser.", ok: false }; + return unavailableService.unavailableMessage || "No TTS service is available in this browser."; } - return { message: "No TTS service is selected.", ok: false }; - } - if (!service.available) { - return { message: service.unavailableMessage || "Selected TTS service is unavailable.", ok: false }; - } - if (!target) { - return { message: "Select a message row or segment row before testing speech.", ok: false }; - } - if (!profile) { - return { message: "Select an active TTS profile before testing speech.", ok: false }; - } - if (!target.emotionProfile) { - return { message: "Selected item needs an Emotion before testing speech.", ok: false }; - } - const emotionSetting = selectedEmotionSettingForProfile(profile, target.emotionProfile); - if (!emotionSetting.ok) { - return { message: emotionSetting.message, ok: false }; - } - if (!String(target.text || "").trim()) { - return { message: "Selected item needs message text before testing speech.", ok: false }; + return "No TTS service is available in this browser."; } - return { message: `Ready to test ${target.label}.`, ok: true }; + return "Ready for Message Studio playback."; } -function renderSpeechTestControls() { - renderTtsServiceOptions(); - renderTtsProfileOptions(); - const target = selectedSpeechTarget(); - const readiness = speechTestReadiness(); - setText(elements.speechTestTarget, target?.label || "None"); - setText(elements.previewStatus, readiness.message); - if (elements.testSpeech) { - elements.testSpeech.disabled = !readiness.ok; - } +function renderPlaybackControls() { + setText(elements.playbackStatus, playbackReadinessMessage()); if (elements.stopSpeech) { elements.stopSpeech.disabled = !selectedTtsService()?.available; } @@ -503,7 +399,7 @@ function createMessageEditRows(message = null) { const ttsCell = document.createElement("td"); ttsCell.append(createTtsProfileSelect( - message ? selectedTtsProfileForMessage(message.key).key : defaultTtsProfileKey(), + message ? selectedTtsProfileForMessage(message.key)?.key : defaultTtsProfileKey(), "messageDefaultTtsProfile", key, )); @@ -551,7 +447,7 @@ function createMessageSegmentTable() { table.setAttribute("aria-label", "Message parts"); const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); - ["Text", "Emotion", "TTS Profile", "Status", "Actions"].forEach((label) => { + ["Part Text", "Emotion", "TTS Profile", "Status", "Actions"].forEach((label) => { const header = document.createElement("th"); header.scope = "col"; header.textContent = label; @@ -587,7 +483,7 @@ function createMessageSegmentTable() { )); const ttsCell = document.createElement("td"); ttsCell.append(createTtsProfileSelect( - selectedTtsProfileForSegment(segment.key, segment.messageKey).key, + selectedTtsProfileForSegment(segment.key, segment.messageKey)?.key || "", "segmentTtsProfile", segment.key, )); @@ -627,7 +523,7 @@ function createSegmentEditRow(segment = null) { const ttsCell = document.createElement("td"); ttsCell.append(createTtsProfileSelect( - segment ? selectedTtsProfileForSegment(segment.key, segment.messageKey).key : selectedTtsProfileForMessage(state.selectedMessageKey).key, + segment ? selectedTtsProfileForSegment(segment.key, segment.messageKey)?.key : selectedTtsProfileForMessage(state.selectedMessageKey)?.key, "segmentTtsProfile", key, )); @@ -687,7 +583,7 @@ function renderMessageRows() { nameCell.textContent = `${isExpanded ? "v" : ">"} ${message.name}`; const ttsCell = document.createElement("td"); ttsCell.append(createTtsProfileSelect( - selectedTtsProfileForMessage(message.key).key, + selectedTtsProfileForMessage(message.key)?.key || "", "messageDefaultTtsProfile", message.key, )); @@ -721,7 +617,7 @@ function render(persistence = {}) { renderSelectedMessage(); renderCounts(); renderPersistence(persistence); - renderSpeechTestControls(); + renderPlaybackControls(); } function editorValue(root, selector) { @@ -912,36 +808,16 @@ async function disableSegment(key) { } } -function testSelectedSpeech() { - const readiness = speechTestReadiness(); - if (!readiness.ok) { - setText(elements.previewStatus, readiness.message); - setText(elements.log, readiness.message); - return; - } - const service = selectedTtsService(); - const target = selectedSpeechTarget(); - 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; - } - 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.playbackStatus, safeMessage); setText(elements.log, safeMessage); return { message: safeMessage, ok: false }; } function playbackService() { - return selectedTtsService() || state.ttsServices.find((service) => service.available) || null; + return selectedTtsService(); } function speakTarget(service, target, profile) { @@ -1004,7 +880,7 @@ function playPart(key) { } clearValidation(); const message = `Play Part queued ${target.label} using ${target.profile.name}.`; - setText(elements.previewStatus, message); + setText(elements.playbackStatus, message); setText(elements.log, message); } @@ -1029,7 +905,7 @@ function playMessage(key) { } clearValidation(); const message = `Play Message queued ${parts.length} parts for ${messageRecord.name}.`; - setText(elements.previewStatus, message); + setText(elements.playbackStatus, message); setText(elements.log, message); } @@ -1041,7 +917,7 @@ function stopSpeech() { } clearValidation(); const message = `Message Studio playback stopped. Cleared ${result.stoppedCount} queued item${result.stoppedCount === 1 ? "" : "s"}.`; - setText(elements.previewStatus, message); + setText(elements.playbackStatus, message); setText(elements.log, message); } @@ -1076,24 +952,12 @@ async function moveSegment(key, direction) { } } -elements.previewTtsProfile?.addEventListener("change", () => { - renderSpeechTestControls(); -}); - -elements.ttsService?.addEventListener("change", () => { - renderSpeechTestControls(); -}); - -elements.testSpeech?.addEventListener("click", () => { - testSelectedSpeech(); -}); - elements.stopSpeech?.addEventListener("click", () => { stopSpeech(); }); ttsServiceRegistry.onServicesChanged(() => { - renderSpeechTestControls(); + renderPlaybackControls(); }); elements.table?.addEventListener("change", (event) => { @@ -1104,7 +968,7 @@ elements.table?.addEventListener("change", (event) => { if (key && key !== NEW_ROW_KEY) { state.messageTtsProfileKeys.set(key, messageSelect.value || defaultTtsProfileKey()); } - renderSpeechTestControls(); + renderPlaybackControls(); setText(elements.log, "Default TTS profile selected for this message playback session."); } if (segmentSelect) { @@ -1112,7 +976,7 @@ elements.table?.addEventListener("change", (event) => { if (key && key !== NEW_ROW_KEY) { state.segmentTtsProfileKeys.set(key, segmentSelect.value || defaultTtsProfileKey()); } - renderSpeechTestControls(); + renderPlaybackControls(); setText(elements.log, "TTS profile selected for this part playback session."); } }); diff --git a/toolbox/text-to-speech/index.html b/toolbox/text-to-speech/index.html index 7989b8309..9f509977a 100644 --- a/toolbox/text-to-speech/index.html +++ b/toolbox/text-to-speech/index.html @@ -67,11 +67,11 @@

TTS Profiles

Message NameMessage Type Status PartsDefault TTS ProfileTTS Profile Actions
- + - + diff --git a/toolbox/text-to-speech/text2speech.js b/toolbox/text-to-speech/text2speech.js index 5174c8d5e..b8b2afcce 100644 --- a/toolbox/text-to-speech/text2speech.js +++ b/toolbox/text-to-speech/text2speech.js @@ -663,7 +663,7 @@ function initializeTextToSpeechTool(root = document, { engine = new TextToSpeech table.setAttribute("aria-label", "Emotion Settings"); const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); - ["Emotion", "Pitch", "Rate", "Volume", "Preset", "Status", "Actions"].forEach((label) => { + ["Emotion", "Pitch", "Rate", "Volume", "Delivery Preset", "Status", "Actions"].forEach((label) => { const header = document.createElement("th"); header.scope = "col"; header.textContent = label;
Profile NameProfile Voice Language GenderAgeAge Filter Emotion Count Status Actions