From ffc7e30633bd43d9233be42e4e8d9027daea3481 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 23 Jun 2026 21:03:51 -0400 Subject: [PATCH] PR_26175_ALFA_010: status bar replacement review --- ..._ALFA_010-status-bar-replacement-review.md | 163 + .../dev/reports/codex_changed_files.txt | 307 +- docs_build/dev/reports/codex_review.diff | 37130 +--------------- 3 files changed, 597 insertions(+), 37003 deletions(-) create mode 100644 docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md diff --git a/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md b/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md new file mode 100644 index 000000000..473e3a42b --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md @@ -0,0 +1,163 @@ +# PR_26175_ALFA_010-status-bar-replacement-review + +## Purpose + +Determine whether GitHub PR #133 fully supersedes GitHub PR #120 for the shared toolbox selected-game status bar. + +This PR is report-only. It does not merge PRs, close PRs, delete branches, or modify runtime code. + +## GitHub Authority Snapshot + +| PR | Title | State | Draft | Mergeable | Base | Head | +| --- | --- | --- | --- | --- | --- | --- | +| #120 | `[codex] PR_26175_ALFA_003 toolbox status bar single row polish` | open | yes | no | `main` at `6d94477bb0ae9f63dd1466dbb89e4a437b8749b0` | `codex/pr-26175-alfa-003-toolbox-status-bar-single-row-polish` at `8a4ab291b9b948e3fe93a4359376bab7f1886dea` | +| #133 | `PR_26175_ALFA_009-status-bar-single-row-rebuild` | open | yes | yes | `main` at `5415f6675d7a0f10931b83368948a83df98d8021` | `codex/pr-26175-alfa-009-status-bar-single-row-rebuild` at `025aac91acb67565ae92de8fad4def6135ce85b5` | + +## Executive Answer + +Recommendation: **Close #120 and merge #133**. + +#133 should replace #120. It carries the same intended creator-facing status bar behavior from #120, rebuilds it on current `main`, and is mergeable while #120 is stale and not mergeable. No creator-visible behavior appears lost. + +Nuance: #133 does not preserve #120's exact fullscreen reserve implementation or exact reserve-equality Playwright assertion. #120 used `margin-block-end: var(--toolbox-status-bar-height)` on `.tool-center-panel` and asserted that reserve equaled the status bar height. #133 instead reserves space through fullscreen workspace and column sizing, including the platform-banner top reserve, and validates the observable behavior that the center panel stops above the fixed status bar. This is an implementation and validation-shape change, not a visible behavior loss. + +## 1. Files Changed Comparison + +| File | PR #120 | PR #133 | Comparison | +| --- | --- | --- | --- | +| `assets/theme-v2/js/toolbox-status-bar.js` | Changed | Changed | Same behavior: removes visible labels, purpose text, context pill, and action link; leaves selected-game name and status message; keeps non-visible `data-toolbox-status-context-kind`. | +| `assets/theme-v2/css/status.css` | Changed | Changed | Same visible status bar behavior. Difference: #133 replaces #120's center-panel margin reserve with workspace/column height reserves and includes top-reserve handling on columns. | +| `assets/theme-v2/css/layout.css` | Changed | Changed | Same change: shared footer top padding becomes `0px` while bottom padding remains. | +| `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` | Changed | Changed | Same core coverage for removed labels, no purpose/action/context pill, same-row layout, footer spacing, fullscreen anchoring, Game Hub ownership, missing-game prompt, and Idea Board exclusion. Difference: #120 asserts exact center-panel reserve equals status bar height; #133 asserts the panel bottom is above the status bar. | + +Full changed-file set: + +| Category | PR #120 | PR #133 | +| --- | --- | --- | +| Runtime/shared UI files | `assets/theme-v2/css/layout.css`, `assets/theme-v2/css/status.css`, `assets/theme-v2/js/toolbox-status-bar.js` | Same three files | +| Test files | `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` | Same file | +| Build/report files | `docs_build/dev/BUILD_PR.md`, ALFA_003 reports, `codex_changed_files.txt`, `codex_review.diff` | `docs_build/dev/BUILD_PR.md`, ALFA_009 reports, `codex_changed_files.txt`, `codex_review.diff` | + +## 2. Feature Comparison + +| Feature | PR #120 | PR #133 | Superseded by #133? | +| --- | --- | --- | --- | +| Single visible status bar row | Yes | Yes | PASS | +| Selected game name on left | Yes | Yes | PASS | +| Status message centered | Yes | Yes | PASS | +| Remove visible selected-game labels | Yes | Yes | PASS | +| Remove selected-game purpose from visible bar | Yes | Yes | PASS | +| Remove visible status category pill labels | Yes | Yes | PASS | +| Remove status action link | Yes | Yes | PASS | +| Preserve non-visible context classification data | Yes | Yes | PASS | +| Preserve Game Hub selected-game ownership | Yes | Yes | PASS | +| Preserve Idea Board selected-game filtering exclusion | Yes | Yes | PASS | +| Remove footer/status extra spacing | Yes | Yes | PASS | +| Preserve fullscreen bottom anchoring | Yes | Yes | PASS | +| Prevent center content from being hidden behind fixed status bar | Yes | Yes | PASS | +| Account for platform banner in fullscreen sizing | Partial: workspace top reserve exists, but column sizing does not subtract top reserve | Yes: workspace and column sizing subtract top reserve | PASS, #133 improves this area | + +## 3. Validation Comparison + +| Validation | PR #120 | PR #133 | Comparison | +| --- | --- | --- | --- | +| Targeted Playwright | PASS: `npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1`, 6 passed | PASS: same command, 6 passed | Equivalent pass result | +| Inline style/style block scan | PASS: `rg -n "/dev/projects/

-

/ist/projects/

-

/uat/projects/

--

/prd/projects/

-+

/prod/projects/

- - - -@@ -81,7 +81,7 @@ - DEV/dev/projects/Loading - IST/ist/projects/Loading - UAT/uat/projects/Loading -- PRD/prd/projects/Loading -+ PRD/prod/projects/Loading - - - -diff --git a/admin/system-health.html b/admin/system-health.html -index f5330bcdd..65ff3c12e 100644 ---- a/admin/system-health.html -+++ b/admin/system-health.html -@@ -39,6 +39,7 @@ - Health Sections -
-

Environment Summary

-+

Local API Startup

-

Database Health

-

Storage Health

-

Runtime Environment

-@@ -74,6 +75,21 @@ - - -
-+
-+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+
Local API Startup Diagnostics
FieldSafe ValueStatus
Startup diagnosticsWaiting for safe API statusPENDING
-+
-
- - -diff --git a/toolbox/assets/assets-api-client.js b/assets/js/shared/assets-api-client.js -similarity index 95% -rename from toolbox/assets/assets-api-client.js -rename to assets/js/shared/assets-api-client.js -index 9e504899c..1232aac7c 100644 ---- a/toolbox/assets/assets-api-client.js -+++ b/assets/js/shared/assets-api-client.js -@@ -3,7 +3,7 @@ import { - createServerRepositoryClient, - readServerToolConstants, - requireServerConstant, --} from "../../src/api/server-api-client.js"; -+} from "../../../src/api/server-api-client.js"; - - const constants = readServerToolConstants("assets"); - -diff --git a/toolbox/controls/controls-api-client.js b/assets/js/shared/controls-api-client.js -similarity index 96% -rename from toolbox/controls/controls-api-client.js -rename to assets/js/shared/controls-api-client.js -index 89eb688be..3b382022b 100644 ---- a/toolbox/controls/controls-api-client.js -+++ b/assets/js/shared/controls-api-client.js -@@ -2,7 +2,7 @@ import { - createServerRepositoryClient, - readServerToolConstants, - requireServerConstant, --} from "../../src/api/server-api-client.js"; -+} from "../../../src/api/server-api-client.js"; - - const constants = readServerToolConstants("controls"); - -diff --git a/toolbox/game-journey/game-journey-api-client.js b/assets/js/shared/game-journey-api-client.js -similarity index 91% -rename from toolbox/game-journey/game-journey-api-client.js -rename to assets/js/shared/game-journey-api-client.js -index 4d9647bb2..fba67b725 100644 ---- a/toolbox/game-journey/game-journey-api-client.js -+++ b/assets/js/shared/game-journey-api-client.js -@@ -2,11 +2,11 @@ import { - createServerRepositoryClient, - readServerToolConstants, - requireServerConstant, --} from "../../src/api/server-api-client.js"; -+} from "../../../src/api/server-api-client.js"; - export { - readGameJourneyCompletionMetrics, - updateGameJourneyCompletionMetric, --} from "../../src/api/game-journey-completion-api-client.js"; -+} from "../../../src/api/game-journey-completion-api-client.js"; - - const constants = readServerToolConstants("game-journey"); - -diff --git a/assets/js/shared/status.js b/assets/js/shared/status.js +diff --git a/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md b/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md new file mode 100644 -index 000000000..26bb6926f +index 000000000..473e3a42b --- /dev/null -+++ b/assets/js/shared/status.js -@@ -0,0 +1,42 @@ -+export const STATUS_VALUES = Object.freeze(["PASS", "WARN", "FAIL", "PENDING", "INFO", "SKIP"]); -+ -+export function statusText(value, fallback = "not available") { -+ const text = String(value ?? "").trim(); -+ return text || fallback; -+} -+ -+export function normalizeStatusValue(value, fallback = "PENDING") { -+ const normalized = String(value || "").trim().toUpperCase(); -+ return STATUS_VALUES.includes(normalized) ? normalized : fallback; -+} -+ -+export function formatStatusMessage(status, message, options = {}) { -+ const normalized = normalizeStatusValue(status, options.fallbackStatus || "PENDING"); -+ const resolvedMessage = Object.hasOwn(options, "fallbackMessage") -+ ? statusText(message, options.fallbackMessage) -+ : String(message); -+ return `${normalized}: ${resolvedMessage}`; -+} -+ -+export function formatStatusReason(status, reason, options = {}) { -+ const normalized = normalizeStatusValue(status, options.fallbackStatus || "PENDING"); -+ return `${normalized}: ${statusText(reason, options.fallbackReason || "Safe server diagnostics did not provide a reason.")}`; -+} -+ -+export function applyStatusNode(node, status, options = {}) { -+ if (!node) { -+ return ""; -+ } -+ const normalized = normalizeStatusValue(status, options.fallbackStatus || "PENDING"); -+ node.textContent = normalized; -+ node.dataset.healthStatus = normalized; -+ if (normalized === "PASS" && !options.reason) { -+ node.removeAttribute("title"); -+ node.removeAttribute("aria-label"); -+ return normalized; -+ } -+ const reason = statusText(options.reason, options.titleFallback || "Safe server diagnostics returned this non-PASS status."); -+ node.setAttribute("title", `${options.titlePrefix || "Reason: "}${reason}`); -+ node.setAttribute("aria-label", formatStatusReason(normalized, options.reason, options)); -+ return normalized; -+} -diff --git a/toolbox/text-to-speech/tts-profile-store.js b/assets/js/shared/tts-profile-store.js -similarity index 100% -rename from toolbox/text-to-speech/tts-profile-store.js -rename to assets/js/shared/tts-profile-store.js -diff --git a/assets/theme-v2/css/status.css b/assets/theme-v2/css/status.css -index 5c521c45d..88e0d5e64 100644 ---- a/assets/theme-v2/css/status.css -+++ b/assets/theme-v2/css/status.css -@@ -35,6 +35,119 @@ - line-height: var(--line-height-copy) - } - -+.toolbox-status-bar { -+ width: 100%; -+ border-block: var(--border-standard); -+ background: var(--panel-overlay-strong); -+ color: var(--text) -+} -+ -+.toolbox-status-bar__inner { -+ width: var(--container-width); -+ margin: var(--space-0) auto; -+ padding: var(--space-10) var(--space-0); -+ display: grid; -+ grid-template-columns: minmax(var(--space-0), 1fr) minmax(var(--space-0), 2fr); -+ gap: var(--space-16); -+ align-items: center -+} -+ -+.toolbox-status-bar__game { -+ min-width: var(--space-0); -+ display: flex; -+ align-items: center; -+ flex-wrap: wrap; -+ gap: var(--space-14); -+ text-align: left -+} -+ -+.toolbox-status-bar__field { -+ min-width: var(--space-0); -+ display: grid; -+ gap: var(--space-3) -+} -+ -+.toolbox-status-bar__label { -+ color: var(--muted); -+ font-size: var(--font-size-xs); -+ font-weight: var(--font-weight-heavy); -+ letter-spacing: var(--letter-spacing-kicker); -+ text-transform: uppercase -+} -+ -+.toolbox-status-bar__game-name { -+ color: var(--text); -+ font-size: var(--font-size-base); -+ overflow-wrap: anywhere -+} -+ -+.toolbox-status-bar__purpose { -+ color: var(--muted); -+ font-size: var(--font-size-sm); -+ overflow-wrap: anywhere -+} -+ -+.toolbox-status-bar__center { -+ min-width: var(--space-0); -+ display: flex; -+ align-items: center; -+ justify-content: center; -+ flex-wrap: wrap; -+ gap: var(--space-10); -+ text-align: center -+} -+ -+.toolbox-status-bar__context-type { -+ flex: 0 0 auto -+} -+ -+.toolbox-status-bar__message { -+ margin: var(--space-0); -+ max-width: var(--measure-lg); -+ overflow-wrap: anywhere -+} -+ -+.toolbox-status-bar__action { -+ flex: 0 0 auto -+} -+ -+.toolbox-status-bar[data-selected-game-state="active"] { -+ border-color: color-mix(in srgb, var(--green) 52%, var(--line)) -+} -+ -+.toolbox-status-bar[data-selected-game-state="missing"] { -+ border-color: var(--gold-border-muted) -+} -+ -+.toolbox-status-bar[data-selected-game-state="error"] { -+ border-color: color-mix(in srgb, var(--red) 52%, var(--line)) -+} -+ -+.toolbox-status-bar[data-toolbox-status-context-kind="error"] .toolbox-status-bar__context-type { -+ border-color: color-mix(in srgb, var(--red) 62%, var(--line)); -+ color: var(--red) -+} -+ -+.toolbox-status-bar[data-toolbox-status-context-kind="warning"] .toolbox-status-bar__context-type, -+.toolbox-status-bar[data-toolbox-status-context-kind="validation"] .toolbox-status-bar__context-type { -+ border-color: var(--gold-border-muted); -+ color: var(--gold) -+} -+ -+.toolbox-status-bar[data-toolbox-status-context-kind="save"] .toolbox-status-bar__context-type { -+ border-color: color-mix(in srgb, var(--green) 62%, var(--line)); -+ color: var(--green) -+} -+ -+body.tool-focus-mode .toolbox-status-bar { -+ position: fixed; -+ inset-block-end: var(--space-0); -+ inset-inline: var(--space-0); -+ z-index: var(--z-index-lg); -+ border-block-end: var(--space-0); -+ box-shadow: var(--shadow-md) -+} -+ - .platform-banner { - width: 100%; - border-bottom: var(--border-standard); -@@ -144,3 +257,16 @@ - color: var(--bg); - text-shadow: none - } -+ -+@media (max-width: 720px) { -+ .toolbox-status-bar__inner { -+ width: var(--container-width); -+ grid-template-columns: 1fr; -+ text-align: center -+ } -+ -+ .toolbox-status-bar__game { -+ justify-content: center; -+ text-align: center -+ } -+} -diff --git a/assets/theme-v2/css/tables.css b/assets/theme-v2/css/tables.css -index c39ef19f0..5575ff684 100644 ---- a/assets/theme-v2/css/tables.css -+++ b/assets/theme-v2/css/tables.css -@@ -116,26 +116,26 @@ td { - } - - .idea-board-idea-label { -- display: inline-flex; -- align-items: center; -- gap: .35em; -+ display: inline; - color: inherit; - font: inherit; - line-height: var(--line-height-single); - vertical-align: baseline; -- white-space: nowrap -+ white-space: normal - } - - .idea-board-idea-label__text { -- line-height: var(--line-height-single) -+ overflow-wrap: anywhere; -+ line-height: inherit - } - - .idea-board-idea-chevron { - display: inline-block; - width: 1em; - height: 1em; -- flex: 0 0 1em; -+ margin-right: .35em; - background: currentColor; -+ vertical-align: -0.125em; - -webkit-mask-position: center; - mask-position: center; - -webkit-mask-repeat: no-repeat; -diff --git a/assets/theme-v2/js/admin-infrastructure.js b/assets/theme-v2/js/admin-infrastructure.js -index 0251c32d6..b470c84df 100644 ---- a/assets/theme-v2/js/admin-infrastructure.js -+++ b/assets/theme-v2/js/admin-infrastructure.js -@@ -6,7 +6,7 @@ const STORAGE_PATH_LANES = Object.freeze([ - Object.freeze({ lane: "DEV", path: "/dev/projects/" }), - Object.freeze({ lane: "IST", path: "/ist/projects/" }), - Object.freeze({ lane: "UAT", path: "/uat/projects/" }), -- Object.freeze({ lane: "PRD", path: "/prd/projects/" }), -+ Object.freeze({ lane: "PRD", path: "/prod/projects/" }), - ]); - - class AdminInfrastructureStoragePathStatus { -diff --git a/assets/theme-v2/js/admin-invitations.js b/assets/theme-v2/js/admin-invitations.js -index 5299093f3..3e3c9e829 100644 ---- a/assets/theme-v2/js/admin-invitations.js -+++ b/assets/theme-v2/js/admin-invitations.js -@@ -3,6 +3,7 @@ import { - readAdminInvites, - revokeAdminBetaInvite - } from "../../../src/api/admin-invitations-api-client.js"; -+import { formatStatusMessage } from "../../js/shared/status.js"; - - function text(value) { - if (value === undefined || value === null || value === "") { -@@ -53,7 +54,7 @@ class AdminInvitesController { - } - - setStatus(status, message) { -- this.status.textContent = `${status}: ${message}`; -+ this.status.textContent = formatStatusMessage(status, message); - } - - setSummary(payload = {}) { -diff --git a/assets/theme-v2/js/admin-operations.js b/assets/theme-v2/js/admin-operations.js -index 306faca9b..fd669562f 100644 ---- a/assets/theme-v2/js/admin-operations.js -+++ b/assets/theme-v2/js/admin-operations.js -@@ -2,6 +2,7 @@ import { - readAdminOperationsStatus, - runAdminOperationAction - } from "../../../src/api/admin-operations-api-client.js"; -+import { formatStatusMessage } from "../../js/shared/status.js"; - - class AdminOperationsController { - constructor(root) { -@@ -25,7 +26,7 @@ class AdminOperationsController { - } - - setStatus(status, message) { -- this.status.textContent = `${status}: ${message}`; -+ this.status.textContent = formatStatusMessage(status, message); - } - - createLabeledControl(labelText, control) { -diff --git a/assets/theme-v2/js/admin-setup-actions.js b/assets/theme-v2/js/admin-setup-actions.js -index 27ddbd1aa..84f39fcd9 100644 ---- a/assets/theme-v2/js/admin-setup-actions.js -+++ b/assets/theme-v2/js/admin-setup-actions.js -@@ -1,11 +1,12 @@ - import { readAdminSetupStatus } from "../../../src/api/admin-setup-api-client.js"; -+import { formatStatusMessage } from "../../js/shared/status.js"; - - const refreshButtons = Array.from(document.querySelectorAll("[data-admin-setup-refresh]")); - const statusFields = Array.from(document.querySelectorAll("[data-admin-setup-status]")); - const statusRows = Array.from(document.querySelectorAll("[data-admin-setup-status-rows]")); - - function setStatus(message, status = "PASS") { -- const text = `${status}: ${message}`; -+ const text = formatStatusMessage(status, message); - statusFields.forEach((field) => { - field.textContent = text; - }); -diff --git a/assets/theme-v2/js/admin-system-health.js b/assets/theme-v2/js/admin-system-health.js -index dfe67df79..72ddd7ea8 100644 ---- a/assets/theme-v2/js/admin-system-health.js -+++ b/assets/theme-v2/js/admin-system-health.js -@@ -2,8 +2,12 @@ import { - readAdminSystemHealthStatus, - runAdminSystemHealthStorageConnectivityAction, - } from "../../../src/api/admin-system-health-api-client.js"; -+import { -+ applyStatusNode, -+ normalizeStatusValue, -+ statusText, -+} from "../../js/shared/status.js"; - --const STATUS_VALUES = Object.freeze(["PASS", "WARN", "FAIL", "PENDING", "INFO", "SKIP"]); - const STORAGE_DIAGNOSTIC_ACTIONS = Object.freeze([ - Object.freeze({ actionId: "storage-list", key: "list" }), - Object.freeze({ actionId: "storage-write-test-object", key: "write" }), -@@ -12,18 +16,7 @@ const STORAGE_DIAGNOSTIC_ACTIONS = Object.freeze([ - ]); - - function asText(value, fallback = "not available") { -- const text = String(value ?? "").trim(); -- return text || fallback; --} -- --function statusValue(value, fallback = "PENDING") { -- const normalized = String(value || "").trim().toUpperCase(); -- return STATUS_VALUES.includes(normalized) ? normalized : fallback; --} -- --function reasonText(status, reason) { -- const message = asText(reason, "Safe server diagnostics did not provide a reason."); -- return `${status}: ${message}`; -+ return statusText(value, fallback); - } - - class AdminSystemHealthController { -@@ -45,6 +38,7 @@ class AdminSystemHealthController { - node.dataset.adminSystemHealthStorageStatus, - node, - ])); -+ this.startupRows = root.querySelector("[data-admin-system-health-startup-rows]"); - this.runtimeRows = root.querySelector("[data-admin-system-health-runtime-rows]"); - } - -@@ -80,26 +74,14 @@ class AdminSystemHealthController { - } - - setStatusNode(node, status, reason = "") { -- if (!node) { -- return; -- } -- const normalized = statusValue(status); -- node.textContent = normalized; -- node.dataset.healthStatus = normalized; -- if (normalized === "PASS" && !reason) { -- node.removeAttribute("title"); -- node.removeAttribute("aria-label"); -- return; -- } -- const resolvedReason = reasonText(normalized, reason); -- node.setAttribute("title", `Reason: ${asText(reason, "Safe server diagnostics returned this non-PASS status.")}`); -- node.setAttribute("aria-label", resolvedReason); -+ applyStatusNode(node, status, { reason }); - } - - renderPending(reason) { - ["host", "database", "migration", "connection"].forEach((key) => { - this.setStatus(key, "PENDING", reason); - }); -+ this.renderStartupPending(reason); - this.renderStoragePending(reason); - } - -@@ -138,6 +120,45 @@ class AdminSystemHealthController { - this.setStorageStatus("bucket", storageStatus.bucketStatus || storageStatus.status, reason); - } - -+ renderStartupPending(reason) { -+ if (!this.startupRows) { -+ return; -+ } -+ const row = document.createElement("tr"); -+ row.append( -+ this.createCell("Local API startup diagnostics"), -+ this.createCell("not available"), -+ this.createStatusCell("PENDING", reason), -+ ); -+ this.startupRows.replaceChildren(row); -+ } -+ -+ renderStartupDiagnostics(localApiStartup = {}) { -+ if (!this.startupRows) { -+ return; -+ } -+ if (localApiStartup?.secretsExposed === true || localApiStartup?.secretEditingAllowed === true) { -+ this.renderStartupPending("Safe Local API startup diagnostics were blocked because the response exposed secret controls."); -+ return; -+ } -+ const rows = Array.isArray(localApiStartup.rows) ? localApiStartup.rows : []; -+ if (!rows.length) { -+ this.renderStartupPending("Safe Local API startup diagnostics returned no rows."); -+ return; -+ } -+ const fragment = document.createDocumentFragment(); -+ rows.forEach((startupRow) => { -+ const row = document.createElement("tr"); -+ row.append( -+ this.createCell(startupRow.field), -+ this.createCell(startupRow.value), -+ this.createStatusCell(startupRow.status, startupRow.reason || localApiStartup.message), -+ ); -+ fragment.append(row); -+ }); -+ this.startupRows.replaceChildren(fragment); -+ } -+ - storageResultTarget(result = {}) { - if (typeof result.keysListed === "number" && result.actionId === "storage-list") { - return `${result.keysListed} object(s) under ${asText(result.projectsPrefix, "configured prefix")}`; -@@ -176,7 +197,7 @@ class AdminSystemHealthController { - - createStatusCell(status, reason) { - const cell = document.createElement("td"); -- cell.dataset.healthStatus = statusValue(status); -+ cell.dataset.healthStatus = normalizeStatusValue(status); - this.setStatusNode(cell, status, reason); - return cell; - } -@@ -230,6 +251,7 @@ class AdminSystemHealthController { - return; - } - this.renderPostgresStatus(data?.databaseStatus || {}); -+ this.renderStartupDiagnostics(data?.localApiStartup || {}); - this.renderStorageStatus(data?.storageStatus || {}); - this.runStorageDiagnostics(); - this.renderRuntimeEnvironment(data?.runtimeEnvironment || {}); -diff --git a/assets/theme-v2/js/gamefoundry-partials.js b/assets/theme-v2/js/gamefoundry-partials.js -index 44b301916..f7f220880 100644 ---- a/assets/theme-v2/js/gamefoundry-partials.js -+++ b/assets/theme-v2/js/gamefoundry-partials.js -@@ -1048,6 +1048,32 @@ - } - } - -+ async function renderToolboxStatusBar() { -+ const pagePath = currentPagePath() || ""; -+ if (pagePath.indexOf("toolbox/") !== 0) { -+ return; -+ } -+ try { -+ const module = await import(assetUrl("js/toolbox-status-bar.js")); -+ if (typeof module.mountToolboxStatusBar === "function") { -+ module.mountToolboxStatusBar({ -+ gameHubHref: routeHref("game-hub"), -+ pagePath -+ }); -+ } -+ } catch (error) { -+ console.warn("[toolbox/status] Shared status bar could not be loaded.", error); -+ } -+ } -+ -+ function refreshSharedSurfaces() { -+ renderPlatformBanner() -+ .then(renderToolboxStatusBar) -+ .catch(function (error) { -+ console.error(error); -+ }); -+ } -+ - enforcePageProtection(); - document.addEventListener("DOMContentLoaded", function () { - enforcePageProtection(); -@@ -1056,11 +1082,11 @@ - replaceExisting("header-nav", "header.site-header"), - replaceExisting("footer", "footer.footer") - ]; -- Promise.all(tasks).then(renderPlatformBanner).catch(function (error) { -+ Promise.all(tasks).then(refreshSharedSurfaces).catch(function (error) { - console.error(error); - }); - }); - window.addEventListener("gamefoundry:session-user-changed", refreshHeaderLoginState); - window.addEventListener("gamefoundry:data-changed", refreshHeaderOnly); -- window.addEventListener("gamefoundry:platform-settings-changed", renderPlatformBanner); -+ window.addEventListener("gamefoundry:platform-settings-changed", refreshSharedSurfaces); - }()); -diff --git a/assets/theme-v2/js/toolbox-status-bar.js b/assets/theme-v2/js/toolbox-status-bar.js -new file mode 100644 -index 000000000..fdb367328 ---- /dev/null -+++ b/assets/theme-v2/js/toolbox-status-bar.js -@@ -0,0 +1,348 @@ -+import { createServerRepositoryClient } from "/src/api/server-api-client.js"; -+ -+const EXCLUDED_SELECTED_GAME_TOOLS = new Set(["idea-board"]); -+const STATUS_BAR_SELECTOR = "[data-toolbox-status-bar]"; -+ -+let repository = null; -+let messageObserver = null; -+let listenersInstalled = false; -+let latestToolMessage = ""; -+let pendingToolMessageRefresh = 0; -+let mountOptions = { -+ gameHubHref: "toolbox/game-hub/index.html", -+ pagePath: "", -+}; -+ -+function getRepository() { -+ if (!repository) { -+ repository = createServerRepositoryClient("game-hub"); -+ } -+ return repository; -+} -+ -+function toolSlugFromPath(pagePath) { -+ const parts = String(pagePath || window.location.pathname || "") -+ .replace(/^\/+/, "") -+ .split("/") -+ .filter(Boolean); -+ if (parts[0] !== "toolbox") { -+ return ""; -+ } -+ const slug = (parts[1] || "index").replace(/\.html$/i, ""); -+ return slug || "index"; -+} -+ -+function pageRequiresSelectedGame() { -+ const slug = toolSlugFromPath(mountOptions.pagePath); -+ return !EXCLUDED_SELECTED_GAME_TOOLS.has(slug); -+} -+ -+function isRepositoryError(value) { -+ return Boolean(value && typeof value === "object" && value.error === true); -+} -+ -+function normalizeSelectedGame(value) { -+ if (!value) { -+ return null; -+ } -+ if (isRepositoryError(value)) { -+ throw new Error(value.message || "Game Hub selected game is unavailable."); -+ } -+ const id = String(value.id || "").trim(); -+ const name = String(value.name || "").trim(); -+ if (!id || !name || !Array.isArray(value.members)) { -+ throw new Error("Game Hub selected game payload is malformed."); -+ } -+ return Object.freeze({ -+ id, -+ name, -+ ownerKey: String(value.ownerKey || "").trim(), -+ purpose: String(value.purpose || "Game").trim() || "Game", -+ status: String(value.status || "").trim(), -+ }); -+} -+ -+function readSelectedGame() { -+ return normalizeSelectedGame(getRepository().getActiveGame()); -+} -+ -+function createText(tagName, className, datasetName) { -+ const element = document.createElement(tagName); -+ if (className) { -+ element.className = className; -+ } -+ if (datasetName) { -+ element.dataset[datasetName] = ""; -+ } -+ return element; -+} -+ -+function createStatusBar() { -+ const bar = document.createElement("section"); -+ bar.className = "toolbox-status-bar"; -+ bar.dataset.toolboxStatusBar = ""; -+ bar.setAttribute("aria-label", "Toolbox selected game status"); -+ -+ const inner = document.createElement("div"); -+ inner.className = "toolbox-status-bar__inner"; -+ -+ const game = document.createElement("div"); -+ game.className = "toolbox-status-bar__game"; -+ game.dataset.toolboxSelectedGame = ""; -+ -+ const nameField = document.createElement("div"); -+ nameField.className = "toolbox-status-bar__field"; -+ nameField.dataset.toolboxSelectedGameNameField = ""; -+ const nameLabel = createText("span", "toolbox-status-bar__label", "toolboxSelectedGameNameLabel"); -+ nameLabel.textContent = "Selected Game Name"; -+ const name = createText("strong", "toolbox-status-bar__game-name", "toolboxSelectedGameName"); -+ nameField.append(nameLabel, name); -+ -+ const purposeField = document.createElement("div"); -+ purposeField.className = "toolbox-status-bar__field"; -+ purposeField.dataset.toolboxSelectedGamePurposeField = ""; -+ const purposeLabel = createText("span", "toolbox-status-bar__label", "toolboxSelectedGamePurposeLabel"); -+ purposeLabel.textContent = "Selected Game Purpose"; -+ const purpose = createText("span", "toolbox-status-bar__purpose", "toolboxSelectedGamePurpose"); -+ purpose.dataset.toolboxSelectedGameMeta = ""; -+ purposeField.append(purposeLabel, purpose); -+ game.append(nameField, purposeField); -+ -+ const center = document.createElement("div"); -+ center.className = "toolbox-status-bar__center"; -+ center.dataset.toolboxStatusCenter = ""; -+ -+ const contextType = createText("span", "pill toolbox-status-bar__context-type", "toolboxStatusContextType"); -+ const message = createText("p", "toolbox-status-bar__message status", "toolboxStatusMessage"); -+ message.setAttribute("role", "status"); -+ const action = document.createElement("a"); -+ action.className = "btn btn--compact toolbox-status-bar__action"; -+ action.dataset.toolboxStatusAction = ""; -+ action.href = mountOptions.gameHubHref; -+ action.textContent = "Open Game Hub"; -+ center.append(contextType, message, action); -+ -+ inner.append(game, center); -+ bar.append(inner); -+ return bar; -+} -+ -+function ensureStatusBar() { -+ let bar = document.querySelector(STATUS_BAR_SELECTOR); -+ if (!bar) { -+ bar = createStatusBar(); -+ } -+ placeStatusBar(bar); -+ return bar; -+} -+ -+function placeStatusBar(bar) { -+ const footer = document.querySelector("footer.footer"); -+ if (footer?.parentNode) { -+ if (bar.nextElementSibling !== footer) { -+ footer.before(bar); -+ } -+ return; -+ } -+ -+ const main = document.querySelector("main"); -+ if (main?.parentNode) { -+ main.after(bar); -+ return; -+ } -+ -+ document.body.append(bar); -+} -+ -+function visibleStatusText(element) { -+ if (!element || element.closest(STATUS_BAR_SELECTOR) || element.hidden) { -+ return ""; -+ } -+ if (element.closest("[hidden]")) { -+ return ""; -+ } -+ return String(element.textContent || "").replace(/\s+/g, " ").trim(); -+} -+ -+function readToolMessage() { -+ const messages = Array.from(document.querySelectorAll("main [role='status'], main .status")) -+ .map((element, index) => ({ -+ index, -+ priority: Object.keys(element.dataset || {}).length > 0 ? 1 : 0, -+ text: visibleStatusText(element), -+ })) -+ .filter((entry) => entry.text); -+ const prioritized = messages -+ .filter((entry) => entry.priority > 0) -+ .pop(); -+ return prioritized?.text || messages[messages.length - 1]?.text || ""; -+} -+ -+function updateLatestToolMessage() { -+ const nextMessage = readToolMessage(); -+ if (nextMessage && nextMessage !== latestToolMessage) { -+ latestToolMessage = nextMessage; -+ refreshToolboxStatusBar(); -+ } -+} -+ -+function scheduleToolMessageRefresh() { -+ window.clearTimeout(pendingToolMessageRefresh); -+ pendingToolMessageRefresh = window.setTimeout(updateLatestToolMessage, 0); -+ window.setTimeout(updateLatestToolMessage, 120); -+} -+ -+function observeToolMessages() { -+ messageObserver?.disconnect(); -+ const main = document.querySelector("main"); -+ if (!main) { -+ return; -+ } -+ latestToolMessage = readToolMessage(); -+ messageObserver = new MutationObserver(updateLatestToolMessage); -+ messageObserver.observe(main, { -+ characterData: true, -+ childList: true, -+ subtree: true, -+ }); -+} -+ -+function publishSelectedGameContext(selectedGame, state) { -+ const required = pageRequiresSelectedGame(); -+ const context = Object.freeze({ -+ required, -+ selectedGame, -+ source: "game-hub", -+ state, -+ }); -+ window.GameFoundryToolboxSelectedGame = context; -+ document.body.dataset.toolboxSelectedGameFilter = required ? state : "optional"; -+ document.body.dataset.toolboxSelectedGameSource = "game-hub"; -+ if (selectedGame) { -+ document.body.dataset.toolboxSelectedGameId = selectedGame.id; -+ } else { -+ delete document.body.dataset.toolboxSelectedGameId; -+ } -+ document.body.classList.toggle("toolbox-selected-game-missing", required && state === "missing"); -+ document.body.classList.toggle("toolbox-selected-game-unavailable", required && state === "error"); -+ document.body.classList.toggle("toolbox-selected-game-optional", !required); -+ window.dispatchEvent(new CustomEvent("gamefoundry:toolbox-selected-game-context", { -+ detail: context, -+ })); -+} -+ -+function classifyToolContext(messageText, state, required) { -+ const text = String(messageText || "").trim(); -+ if (state === "error") { -+ return { kind: "error", label: "Error" }; -+ } -+ if (required && state === "missing") { -+ return { kind: "action", label: "Tool Action" }; -+ } -+ if (/\b(error|failed|malformed|unavailable|could not)\b/i.test(text)) { -+ return { kind: "error", label: "Error" }; -+ } -+ if (/\b(sign in|refresh|try again|temporarily|blocked)\b/i.test(text)) { -+ return { kind: "warning", label: "Warning" }; -+ } -+ if (/\b(validation|requirement|requirements|missing|required|open or seed)\b/i.test(text)) { -+ return { kind: "validation", label: "Validation" }; -+ } -+ if (/\b(saved|created|deleted|updated|loaded|save changes)\b/i.test(text)) { -+ return { kind: "save", label: "Save State" }; -+ } -+ return { kind: "action", label: "Tool Action" }; -+} -+ -+function renderSelectedGame(bar, selectedGame, state, messageText) { -+ const required = pageRequiresSelectedGame(); -+ const name = bar.querySelector("[data-toolbox-selected-game-name]"); -+ const purpose = bar.querySelector("[data-toolbox-selected-game-purpose]"); -+ const contextType = bar.querySelector("[data-toolbox-status-context-type]"); -+ const message = bar.querySelector("[data-toolbox-status-message]"); -+ const action = bar.querySelector("[data-toolbox-status-action]"); -+ const nextMessage = messageText || latestToolMessage || (selectedGame -+ ? `Tool context is filtered to ${selectedGame.name}.` -+ : required -+ ? "Select or create a game in Game Hub before using this toolbox page." -+ : "Idea Board can capture ideas before a Game Hub game exists."); -+ const context = classifyToolContext(nextMessage, state, required); -+ -+ bar.dataset.selectedGameState = state; -+ bar.dataset.selectedGameRequired = String(required); -+ bar.dataset.toolboxStatusContextKind = context.kind; -+ contextType.textContent = context.label; -+ action.hidden = false; -+ action.href = mountOptions.gameHubHref; -+ -+ if (selectedGame) { -+ name.textContent = selectedGame.name; -+ purpose.textContent = selectedGame.purpose || "Game"; -+ message.textContent = nextMessage; -+ action.textContent = "Open Game Hub"; -+ return; -+ } -+ -+ if (!required) { -+ name.textContent = "No game selected"; -+ purpose.textContent = "Idea Board optional"; -+ message.textContent = nextMessage; -+ action.textContent = "Open Game Hub"; -+ return; -+ } -+ -+ if (state === "error") { -+ name.textContent = "Unavailable"; -+ purpose.textContent = "Game Hub selected game could not be read"; -+ message.textContent = nextMessage; -+ action.textContent = "Open Game Hub"; -+ return; -+ } -+ -+ name.textContent = "No game selected"; -+ purpose.textContent = "Game Hub owns game selection"; -+ message.textContent = "Select or create a game in Game Hub before using this toolbox page."; -+ action.textContent = "Select or Create in Game Hub"; -+} -+ -+export function refreshToolboxStatusBar() { -+ const bar = ensureStatusBar(); -+ let selectedGame = null; -+ let state = "missing"; -+ let message = ""; -+ -+ try { -+ selectedGame = readSelectedGame(); -+ state = selectedGame ? "active" : "missing"; -+ } catch (error) { -+ state = "error"; -+ message = error instanceof Error ? error.message : String(error || "Game Hub selected game is unavailable."); -+ } -+ -+ publishSelectedGameContext(selectedGame, state); -+ renderSelectedGame(bar, selectedGame, state, message); -+ placeStatusBar(bar); -+} -+ -+function installEventListeners() { -+ if (listenersInstalled) { -+ return; -+ } -+ listenersInstalled = true; -+ document.addEventListener("click", scheduleToolMessageRefresh, true); -+ document.addEventListener("submit", scheduleToolMessageRefresh, true); -+ document.addEventListener("change", scheduleToolMessageRefresh, true); -+ window.addEventListener("gamefoundry:toolbox-selected-game-changed", refreshToolboxStatusBar); -+ window.addEventListener("gamefoundry:data-changed", refreshToolboxStatusBar); -+} -+ -+export function mountToolboxStatusBar(options = {}) { -+ mountOptions = { -+ ...mountOptions, -+ ...options, -+ }; -+ ensureStatusBar(); -+ observeToolMessages(); -+ installEventListeners(); -+ refreshToolboxStatusBar(); -+} -diff --git a/toolbox/assets/assets-upload-worker.js b/assets/toolbox/assets/js/assets-upload-worker.js -similarity index 100% -rename from toolbox/assets/assets-upload-worker.js -rename to assets/toolbox/assets/js/assets-upload-worker.js -diff --git a/toolbox/assets/assets.js b/assets/toolbox/assets/js/index.js -similarity index 99% -rename from toolbox/assets/assets.js -rename to assets/toolbox/assets/js/index.js -index 712917f48..d09e88a81 100644 ---- a/toolbox/assets/assets.js -+++ b/assets/toolbox/assets/js/index.js -@@ -2,8 +2,8 @@ import { - ASSET_CATALOG_TYPES, - ASSET_USAGE_OPTIONS, - createAssetToolApiRepository --} from "./assets-api-client.js"; --import { getSessionCurrent } from "../../src/api/session-api-client.js"; -+} from "../../../js/shared/assets-api-client.js"; -+import { getSessionCurrent } from "../../../../src/api/session-api-client.js"; - - const repository = createAssetToolApiRepository(); - const params = new URLSearchParams(window.location.search); -diff --git a/toolbox/colors/colors.js b/assets/toolbox/colors/js/index.js -similarity index 97% -rename from toolbox/colors/colors.js -rename to assets/toolbox/colors/js/index.js -index 99efed4f5..dcc0631e5 100644 ---- a/toolbox/colors/colors.js -+++ b/assets/toolbox/colors/js/index.js -@@ -1,20 +1,36 @@ - import { -- CURATED_PALETTE_COLLECTIONS, -- NUMERIC_VARIANT_COUNTS, -- PALETTE_SOURCE_USER, -- PALETTE_GENERATOR_DEFAULTS, -- PALETTE_TOOL_KEY, -- PALETTE_VARIANTS, -- PALETTE_WORKSPACE_PATH, -- PICKER_PREVIEW_DEFAULTS, -- PICKER_PREVIEW_SORT_OPTIONS, -- SIZE_OPTIONS, -- SORT_OPTIONS, -- SUGGESTED_TAGS, -- createGameWorkspacePaletteApiRepository, -- normalizePaletteSwatchInput, -- validatePaletteSwatchInput --} from "./palette-api-client.js"; -+ callServerToolFunction, -+ createServerRepositoryClient, -+ readServerToolConstants, -+ requireServerConstant, -+} from "../../../../src/api/server-api-client.js"; -+ -+const constants = readServerToolConstants("palette"); -+ -+export const PALETTE_SOURCE_USER = requireServerConstant(constants, "PALETTE_SOURCE_USER", "palette"); -+export const PALETTE_TOOL_KEY = requireServerConstant(constants, "PALETTE_TOOL_KEY", "palette"); -+export const PALETTE_WORKSPACE_PATH = requireServerConstant(constants, "PALETTE_WORKSPACE_PATH", "palette"); -+export const CURATED_PALETTE_COLLECTIONS = Object.freeze(requireServerConstant(constants, "CURATED_PALETTE_COLLECTIONS", "palette")); -+export const NUMERIC_VARIANT_COUNTS = Object.freeze(requireServerConstant(constants, "NUMERIC_VARIANT_COUNTS", "palette")); -+export const PALETTE_GENERATOR_DEFAULTS = Object.freeze(requireServerConstant(constants, "PALETTE_GENERATOR_DEFAULTS", "palette")); -+export const PALETTE_VARIANTS = Object.freeze(requireServerConstant(constants, "PALETTE_VARIANTS", "palette")); -+export const PICKER_PREVIEW_DEFAULTS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_DEFAULTS", "palette")); -+export const PICKER_PREVIEW_SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_SORT_OPTIONS", "palette")); -+export const SIZE_OPTIONS = Object.freeze(requireServerConstant(constants, "SIZE_OPTIONS", "palette")); -+export const SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "SORT_OPTIONS", "palette")); -+export const SUGGESTED_TAGS = Object.freeze(requireServerConstant(constants, "SUGGESTED_TAGS", "palette")); -+ -+export function createGameWorkspacePaletteApiRepository(options = {}) { -+ return createServerRepositoryClient("palette", options); -+} -+ -+export function normalizePaletteSwatchInput(input) { -+ return callServerToolFunction("palette", "normalizePaletteSwatchInput", input); -+} -+ -+export function validatePaletteSwatchInput(input, existingSwatches, options) { -+ return callServerToolFunction("palette", "validatePaletteSwatchInput", input, existingSwatches, options); -+} - - const params = new URLSearchParams(window.location.search); - -diff --git a/toolbox/controls/controls.js b/assets/toolbox/controls/js/index.js -similarity index 99% -rename from toolbox/controls/controls.js -rename to assets/toolbox/controls/js/index.js -index c3355a91e..6a648b878 100644 ---- a/toolbox/controls/controls.js -+++ b/assets/toolbox/controls/js/index.js -@@ -5,11 +5,11 @@ import { - GAME_CONTROL_NORMALIZED_INPUTS, - NORMALIZED_USAGE_LABELS, - createControlsToolApiRepository, --} from "./controls-api-client.js"; -+} from "../../../js/shared/controls-api-client.js"; - import { - normalizeNormalizedInput, - normalizedInputOptions, --} from "../../src/engine/input/NormalizedInputRegistry.js"; -+} from "../../../../src/engine/input/NormalizedInputRegistry.js"; - - const GAME_CONTROL_NORMALIZED_INPUT_IDS = new Set(GAME_CONTROL_NORMALIZED_INPUTS); - -diff --git a/toolbox/events/events.js b/assets/toolbox/events/js/index.js -similarity index 99% -rename from toolbox/events/events.js -rename to assets/toolbox/events/js/index.js -index 80df22928..837198395 100644 ---- a/toolbox/events/events.js -+++ b/assets/toolbox/events/js/index.js -@@ -1,7 +1,7 @@ - import { - requireServerApiData, - safeRequestServerApi, --} from "../../src/api/server-api-client.js"; -+} from "../../../../src/api/server-api-client.js"; - - const NEW_ROW_KEY = "__new__"; - const TABLE_COLSPAN = 5; -diff --git a/toolbox/game-configuration/game-configuration.js b/assets/toolbox/game-configuration/js/index.js -similarity index 92% -rename from toolbox/game-configuration/game-configuration.js -rename to assets/toolbox/game-configuration/js/index.js -index b907ca893..d94f3ced2 100644 ---- a/toolbox/game-configuration/game-configuration.js -+++ b/assets/toolbox/game-configuration/js/index.js -@@ -1,7 +1,17 @@ - import { -- GAME_CONFIGURATION_SECTIONS, -- createGameConfigurationApiRepository --} from "./game-configuration-api-client.js"; -+ createServerRepositoryClient, -+ readServerToolConstants, -+ requireServerConstant, -+} from "../../../../src/api/server-api-client.js"; -+ -+const constants = readServerToolConstants("game-configuration"); -+ -+export const GAME_CONFIGURATION_SECTIONS = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_SECTIONS", "game-configuration")); -+export const GAME_CONFIGURATION_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_PLAYER_MODES", "game-configuration")); -+ -+export function createGameConfigurationApiRepository(options = {}) { -+ return createServerRepositoryClient("game-configuration", options); -+} - - const repository = createGameConfigurationApiRepository(); - const params = new URLSearchParams(window.location.search); -diff --git a/toolbox/game-design/game-design.js b/assets/toolbox/game-design/js/index.js -similarity index 92% -rename from toolbox/game-design/game-design.js -rename to assets/toolbox/game-design/js/index.js -index 51466ecff..af61960cd 100644 ---- a/toolbox/game-design/game-design.js -+++ b/assets/toolbox/game-design/js/index.js -@@ -1,10 +1,19 @@ - import { -- GAME_DESIGN_GAME_TYPES, -- GAME_DESIGN_GENRES, -- GAME_DESIGN_PLAYER_MODES, -- GAME_DESIGN_PLAY_STYLES, -- createGameDesignApiRepository --} from "./game-design-api-client.js"; -+ createServerRepositoryClient, -+ readServerToolConstants, -+ requireServerConstant, -+} from "../../../../src/api/server-api-client.js"; -+ -+const constants = readServerToolConstants("game-design"); -+ -+export const GAME_DESIGN_GAME_TYPES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GAME_TYPES", "game-design")); -+export const GAME_DESIGN_GENRES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GENRES", "game-design")); -+export const GAME_DESIGN_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAYER_MODES", "game-design")); -+export const GAME_DESIGN_PLAY_STYLES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAY_STYLES", "game-design")); -+ -+export function createGameDesignApiRepository(options = {}) { -+ return createServerRepositoryClient("game-design", options); -+} - - const repository = createGameDesignApiRepository(); - const params = new URLSearchParams(window.location.search); -diff --git a/toolbox/game-journey/game-journey.js b/assets/toolbox/game-journey/js/index.js -similarity index 98% -rename from toolbox/game-journey/game-journey.js -rename to assets/toolbox/game-journey/js/index.js -index c2b2f876c..085df839c 100644 ---- a/toolbox/game-journey/game-journey.js -+++ b/assets/toolbox/game-journey/js/index.js -@@ -6,11 +6,11 @@ import { - GAME_JOURNEY_SUGGESTED_TOOLS, - createGameJourneyApiRepository, - readGameJourneyCompletionMetrics, --} from "./game-journey-api-client.js"; -+} from "../../../js/shared/game-journey-api-client.js"; - import { - getActiveToolRegistry, - getToolRegistryApiDiagnostic, --} from "../tool-registry-api-client.js"; -+} from "../../../../toolbox/tool-registry-api-client.js"; - - const repository = createGameJourneyApiRepository(); - const registryDiagnostic = getToolRegistryApiDiagnostic(); -@@ -1161,7 +1161,7 @@ function renderRecommendedTargets() { - table.setAttribute("aria-label", "Game Journey recommended planning targets"); - const head = createElement("thead"); - const headRow = createElement("tr"); -- ["Target", "Section", "Suggested"].forEach((heading) => { -+ ["Target", "Section", "Count"].forEach((heading) => { - const cell = createElement("th", { text: heading }); - cell.scope = "col"; - headRow.append(cell); -@@ -1169,16 +1169,22 @@ function renderRecommendedTargets() { - head.append(headRow); - const body = createElement("tbody"); - targets.forEach((target) => { -+ const targetCount = recommendedTargetValues.get(target.key) ?? target.suggestedCount; - const row = createElement("tr"); - row.dataset.journeyRecommendedTarget = target.key; -- const labelCell = createElement("td", { text: target.label }); -+ const labelCell = createElement("td"); -+ const label = createElement("span", { text: target.label }); -+ const countPreview = createElement("span", { text: ` [${targetCount}]` }); -+ countPreview.dataset.journeyTargetCountPreview = target.key; -+ labelCell.append(label, countPreview); - const sectionCell = createElement("td", { text: target.sectionName }); - const input = document.createElement("input"); - input.type = "number"; -+ input.inputMode = "numeric"; - input.min = "0"; - input.step = "1"; -- input.value = String(recommendedTargetValues.get(target.key) ?? target.suggestedCount); -- input.setAttribute("aria-label", `${target.label} suggested target`); -+ input.value = String(targetCount); -+ input.setAttribute("aria-label", `${target.label} count`); - input.dataset.journeyTargetInput = target.key; - const inputCell = createElement("td"); - inputCell.append(input); -@@ -1773,6 +1779,10 @@ recommendedTargets?.addEventListener("input", (event) => { - const savedValue = normalizeTargetCount(updated.suggestedCount); - recommendedTargetValues.set(target.key, savedValue); - input.value = String(savedValue); -+ const preview = recommendedTargets.querySelector(`[data-journey-target-count-preview='${target.key}']`); -+ if (preview) { -+ preview.textContent = ` [${savedValue}]`; -+ } - if (recommendedTargetStatus) { - recommendedTargetStatus.textContent = `Saved ${target.label} target at ${savedValue}.`; - } -diff --git a/toolbox/idea-board/index.js b/assets/toolbox/idea-board/js/index.js -similarity index 93% -rename from toolbox/idea-board/index.js -rename to assets/toolbox/idea-board/js/index.js -index d1e339336..47a5ed91e 100644 ---- a/toolbox/idea-board/index.js -+++ b/assets/toolbox/idea-board/js/index.js -@@ -1,9 +1,12 @@ --import { createServerRepositoryClient } from "../../src/api/server-api-client.js"; -+import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js"; -+import { getSessionCurrent } from "../../../../src/api/session-api-client.js"; - --const statusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); -+const editableStatusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready"]); -+const filterStatusOptions = 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-hub/index.html"; -+const signInRoute = "account/sign-in.html"; - let gameHubRepository = null; - - const ideaTable = [ -@@ -121,7 +124,7 @@ function visibleIdeas() { - } - - function previousStatusForRestore(record) { -- return statusOptions.includes(record.previousStatus) && record.previousStatus !== "Archived" -+ return filterStatusOptions.includes(record.previousStatus) && record.previousStatus !== "Archived" - ? record.previousStatus - : "Refining"; - } -@@ -166,7 +169,7 @@ function renderStatusFilter(root) { - const options = root.querySelector("[data-idea-board-status-options]"); - if (!options) return; - options.replaceChildren(); -- for (const status of statusOptions) { -+ for (const status of filterStatusOptions) { - const label = document.createElement("label"); - label.className = "idea-board-show-filter__option"; - const input = document.createElement("input"); -@@ -193,7 +196,7 @@ function statusSelect(value) { - const select = document.createElement("select"); - select.setAttribute("aria-label", "Idea status"); - select.dataset.ideaBoardIdeaStatusInput = "true"; -- for (const optionValue of statusOptions) { -+ for (const optionValue of editableStatusOptions) { - const option = document.createElement("option"); - option.value = optionValue; - option.textContent = optionValue; -@@ -229,7 +232,6 @@ function renderIdeaInputRow(tbody, record = null) { - const statusCell = document.createElement("td"); - statusCell.append(statusSelect(record?.status || "New")); - row.append(statusCell); -- row.append(cell(record?.updated || today())); - row.append(cell(record ? noteCountLabel(record.ideaId) : "0 Notes")); - - const actions = document.createElement("td"); -@@ -272,7 +274,6 @@ function renderIdeaRow(tbody, record) { - row.append(idea); - row.append(cell(record.pitch)); - row.append(cell(record.status)); -- row.append(cell(record.updated)); - - const notes = document.createElement("td"); - const notesCount = document.createElement("span"); -@@ -351,7 +352,7 @@ function renderExpandedNotesRow(tbody, record) { - row.dataset.ideaBoardExpandedRow = record.ideaId; - - const content = document.createElement("td"); -- content.colSpan = 6; -+ content.colSpan = 5; - - const childSurface = document.createElement("div"); - childSurface.className = "idea-board-notes-child-surface"; -@@ -398,7 +399,7 @@ function renderAddIdeaRow(tbody) { - const row = document.createElement("tr"); - row.dataset.ideaBoardAddIdeaRow = "true"; - const actions = document.createElement("td"); -- actions.colSpan = 6; -+ actions.colSpan = 5; - const addIdea = actionButton("Add Idea", "add", "ideaBoardIdeaAction", "primary"); - addIdea.dataset.ideaBoardAddIdea = "true"; - actions.append(addIdea); -@@ -561,6 +562,28 @@ function gameHubUrl(record) { - return `${gameHubRoute}${suffix}`; - } - -+function signInUrl() { -+ return new URL(signInRoute, document.baseURI || window.location.href).href; -+} -+ -+function currentSessionState() { -+ try { -+ const session = getSessionCurrent(); -+ return { -+ apiAvailable: true, -+ authenticated: Boolean(session?.authenticated && session.userKey), -+ session, -+ }; -+ } catch (error) { -+ console.warn("Idea Board could not verify the current session.", error instanceof Error ? error.message : String(error || "")); -+ return { -+ apiAvailable: false, -+ authenticated: false, -+ session: null, -+ }; -+ } -+} -+ - function createProject(root, ideaId) { - const record = ideaRecord(ideaId); - if (!record) { -@@ -571,6 +594,16 @@ function createProject(root, ideaId) { - updateStatus(root, "Set this idea to Ready before creating a project."); - return; - } -+ const sessionState = currentSessionState(); -+ if (!sessionState.apiAvailable) { -+ updateStatus(root, "Sign-in status could not be verified. Try again shortly."); -+ return; -+ } -+ if (!sessionState.authenticated) { -+ updateStatus(root, "Sign in to create a Game Hub project."); -+ window.location.href = signInUrl(); -+ return; -+ } - const repository = gameHubProjectRepository(); - const project = repository.createGame({ - name: record.idea, -@@ -723,7 +756,7 @@ function handleNoteAction(root, actionControl) { - - function handleFilterAction(root, actionControl) { - if (actionControl.matches("[data-idea-board-filter-select-all]")) { -- state.visibleStatuses = new Set(statusOptions); -+ state.visibleStatuses = new Set(filterStatusOptions); - updateStatus(root, "Showing all statuses."); - } else if (actionControl.matches("[data-idea-board-filter-clear-all]")) { - state.visibleStatuses = new Set(); -diff --git a/toolbox/objects/objects.js b/assets/toolbox/objects/js/index.js -similarity index 96% -rename from toolbox/objects/objects.js -rename to assets/toolbox/objects/js/index.js -index bd600c533..0f332df58 100644 ---- a/toolbox/objects/objects.js -+++ b/assets/toolbox/objects/js/index.js -@@ -1,16 +1,49 @@ -+import { -+ createServerRepositoryClient, -+ readServerToolConstants, -+ requireServerConstant, -+} from "../../../../src/api/server-api-client.js"; - import { - OBJECT_MODEL_TRAIT_LIST, - getObjectModelTrait, - getObjectModelType, - validateObjectDefinition, --} from "../../src/engine/object-model/index.js"; --import { createAssetToolApiRepository } from "../assets/assets-api-client.js"; --import { -- CAPABILITY_LABELS, -- OBJECT_TYPE_TEMPLATES, -- STARTER_OBJECTS, -- createObjectsToolApiRepository, --} from "./objects-api-client.js"; -+} from "../../../../src/engine/object-model/index.js"; -+import { createAssetToolApiRepository } from "../../../js/shared/assets-api-client.js"; -+ -+const constants = readServerToolConstants("objects"); -+ -+function freezeTemplate(template = {}) { -+ return Object.freeze({ -+ ...template, -+ capabilities: Object.freeze(Array.isArray(template.capabilities) ? [...template.capabilities] : []), -+ }); -+} -+ -+function freezeStarterObject(object = {}) { -+ return Object.freeze({ -+ ...object, -+ render: Object.freeze({ ...(object.render || {}) }), -+ }); -+} -+ -+export const CAPABILITY_LABELS = Object.freeze( -+ { ...requireServerConstant(constants, "CAPABILITY_LABELS", "objects") }, -+); -+ -+export const OBJECT_TYPE_TEMPLATES = Object.freeze( -+ requireServerConstant(constants, "OBJECT_TYPE_TEMPLATES", "objects").map(freezeTemplate), -+); -+ -+export const OBJECTS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "OBJECTS_TOOL_TABLES", "objects")); -+ -+export const STARTER_OBJECTS = Object.freeze( -+ requireServerConstant(constants, "STARTER_OBJECTS", "objects").map(freezeStarterObject), -+); -+ -+export function createObjectsToolApiRepository(options = {}) { -+ return createServerRepositoryClient("objects", options); -+} - - let assetRepository = createAssetToolApiRepository(); - let objectsRepository = createObjectsToolApiRepository(); -diff --git a/toolbox/tags/tags.js b/assets/toolbox/tags/js/index.js -similarity index 95% -rename from toolbox/tags/tags.js -rename to assets/toolbox/tags/js/index.js -index c43c6fc89..118923da9 100644 ---- a/toolbox/tags/tags.js -+++ b/assets/toolbox/tags/js/index.js -@@ -1,4 +1,16 @@ --import { createTagsToolApiRepository } from "./tags-api-client.js"; -+import { -+ createServerRepositoryClient, -+ readServerToolConstants, -+ requireServerConstant, -+} from "../../../../src/api/server-api-client.js"; -+ -+const constants = readServerToolConstants("tags"); -+ -+export const TAGS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "TAGS_TOOL_TABLES", "tags")); ++++ b/docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md +@@ -0,0 +1,163 @@ ++# PR_26175_ALFA_010-status-bar-replacement-review + -+export function createTagsToolApiRepository(options = {}) { -+ return createServerRepositoryClient("tags", options); -+} - - const repository = createTagsToolApiRepository(); - -diff --git a/toolbox/text-to-speech/text2speech.js b/assets/toolbox/text-to-speech/js/index.js -similarity index 99% -rename from toolbox/text-to-speech/text2speech.js -rename to assets/toolbox/text-to-speech/js/index.js -index 081fa25b2..77e090f4e 100644 ---- a/toolbox/text-to-speech/text2speech.js -+++ b/assets/toolbox/text-to-speech/js/index.js -@@ -1,7 +1,7 @@ - import { - textToSpeechLanguageOptionsFromVoices, - TextToSpeechEngine, --} from "../../src/engine/audio/TextToSpeechEngine.js"; -+} from "../../../../src/engine/audio/TextToSpeechEngine.js"; - import { - TEXT_TO_SPEECH_AGE_FILTER_OPTIONS, - TEXT_TO_SPEECH_DEFAULTS, -@@ -9,11 +9,11 @@ import { - TEXT_TO_SPEECH_LANGUAGE_OPTIONS, - TEXT_TO_SPEECH_RANGE_DEFAULTS, - TEXT_TO_SPEECH_SSML_LIKE_PRESET_OPTIONS --} from "../../src/engine/audio/TextToSpeechDefaults.js"; -+} from "../../../../src/engine/audio/TextToSpeechDefaults.js"; - import { - readSavedTextToSpeechProfiles, - writeSavedTextToSpeechProfiles, --} from "./tts-profile-store.js"; -+} from "../../../js/shared/tts-profile-store.js"; - - const TTS_OWNERSHIP = Object.freeze({ - DESIGN: "Design", -diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -index 20d0cb9f0..dd5b4d28f 100644 ---- a/docs_build/dev/BUILD_PR.md -+++ b/docs_build/dev/BUILD_PR.md -@@ -1,27 +1,63 @@ --# BUILD_PR: Schema location correction -+# PR_26175_ALFA_008-game-hub-feature-matrix - --## Codex task --Move the schema contract plan from root-level schema files to `src/shared/schemas/`. +## Purpose -+Audit the current Game Hub workflow and publish a feature matrix that maps implemented creator-facing behavior to code and Playwright evidence. - --## Required changes --1. Remove any planned or newly-added root-level `*.schema.json` files. --2. Add schema contracts under `src/shared/schemas/` only. --3. Put reusable manifest schemas directly under `src/shared/schemas/`. --4. Put individual tool payload schemas under `src/shared/schemas/tools/`. --5. Update any docs or references to point to the new schema paths. -+## Source Of Truth -+This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_008-game-hub-feature-matrix`. - --## Do not --- Do not write broad validation utilities as the primary deliverable. --- Do not change sample payloads. --- Do not unlock or mutate samples. --- Do not create schema files at repository root. --- Do not modify start_of_day folders. -+## Exact Scope -+- Produce a Game Hub feature matrix only. -+- Audit Game Hub table workflow, selected/open game behavior, create/edit/delete actions, child tables, guest save gating, empty/error states, Theme V2 layout, and targeted Game Hub coverage. -+- Use current `main` behavior as evidence. -+- Preserve Game Hub UI/product behavior. -+- Preserve API/service/repository contracts. -+- Preserve previous ALFA Game Hub cleanup and create-validation behavior. -+- Do not implement product/UI changes unless validation exposes a requirement-critical defect. - --## Validation command --Search for misplaced schemas: -+## Exact Targets -+- `docs_build/dev/BUILD_PR.md` -+- `docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_report.md` -+- `docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_validation-lane.md` -+- `docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_requirements-checklist.md` -+- `docs_build/dev/reports/codex_review.diff` -+- `docs_build/dev/reports/codex_changed_files.txt` -+ -+## Evidence Sources -+- `toolbox/game-hub/index.html` -+- `toolbox/project-workspace/index.html` -+- `toolbox/game-hub/game-hub.js` -+- `toolbox/game-hub/game-hub-api-client.js` -+- `src/dev-runtime/persistence/tool-repositories/game-workspace-mock-repository.js` -+- `tests/playwright/tools/GameHubMockRepository.spec.mjs` -+ -+## Out Of Scope -+- No Game Hub product or UI changes. -+- No Game Journey changes. -+- No shared toolbox status bar changes. -+- No browser-owned product data as source of truth. -+- No API/service/repository contract changes. -+- No inline styles, style blocks, or page-local CSS. -+- No engine core changes. -+- No `start_of_day` folder changes. -+- No ALFA_007 work. -+ -+## Validation -+Run targeted Game Hub validation: - - ```powershell --Get-ChildItem -Path . -Filter *.schema.json -Recurse | Select-Object FullName -+npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs --workers=1 - ``` - --Expected result: all schema files are under `src\shared\schemas\`. -+Also verify changed docs/reports do not introduce inline styles or style blocks: -+ -+```powershell -+rg -n "<[s]tyle|[s]tyle=" docs_build/dev/BUILD_PR.md docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_report.md docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_validation-lane.md docs_build/dev/reports/PR_26175_ALFA_008-game-hub-feature-matrix_requirements-checklist.md -+``` -+ -+## Artifact -+Create repo-structured delta ZIP: -+ -+```text -+tmp/PR_26175_ALFA_008-game-hub-feature-matrix_delta.zip -+``` -diff --git a/docs_build/dev/ProjectInstructions/PROJECT_INSTRUCTIONS.md b/docs_build/dev/ProjectInstructions/PROJECT_INSTRUCTIONS.md -index 40db33c44..3314b3dea 100644 ---- a/docs_build/dev/ProjectInstructions/PROJECT_INSTRUCTIONS.md -+++ b/docs_build/dev/ProjectInstructions/PROJECT_INSTRUCTIONS.md -@@ -44,3 +44,16 @@ OWNER follows the same safety rules: - - One active OWNER assignment at a time. - - OWNER may override team locks, but may not silently delete, rewrite, or remove protected instructions. - - OWNER override must be explicitly documented. -+ -+## Four-Team Ownership Alignment -+ -+The single authoritative four-team ownership definition is: -+ -+`docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md` -+ -+Use the `Current Four-Team Ownership Model` section there for team ownership, assignment routing, and cross-team scope checks. -+ -+Rules: -+- Teams pull backlog items only from their ownership area unless OWNER explicitly reassigns or splits the work. -+- Cross-team work requires OWNER approval and must identify the owning team for each PR. -+- Team start commands must remain aligned with this ownership model. -diff --git a/docs_build/dev/ProjectInstructions/TEAM_START_COMMANDS.md b/docs_build/dev/ProjectInstructions/TEAM_START_COMMANDS.md -index 35279e8f1..12f74cb1f 100644 ---- a/docs_build/dev/ProjectInstructions/TEAM_START_COMMANDS.md -+++ b/docs_build/dev/ProjectInstructions/TEAM_START_COMMANDS.md -@@ -66,6 +66,30 @@ Create one Team Charlie branch for the selected assignment. - Work only that assignment. - ``` - -+## Start Team Delta -+ -+Ready-to-copy command: -+ -+```text -+OWNER override approved: Start Team Delta from the ProjectInstructions release gate. -+ -+Read docs_build/dev/ProjectInstructions/README.txt first. -+Read docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md. -+Read docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md. -+Read docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md. -+ -+Pull one [ ] item for Team Delta from BACKLOG_MASTER.md. -+Stop if Team Delta already has an active branch. -+Stop if Team Delta already has an active assignment. -+Stop if no [ ] Team Delta backlog item is available. -+Stop if the selected item is outside Team Delta ownership. -+ -+Change the selected backlog item from [ ] to [.]. -+Add the selected assignment under Team Delta in TEAM_ASSIGNMENTS.md. -+Create one Team Delta branch for the selected assignment. -+Work only that assignment. -+``` -+ - ## Day Work / EOD Merge Reminder - - Ready-to-copy reminder: -diff --git a/docs_build/dev/ProjectInstructions/addendums/multi_team.md b/docs_build/dev/ProjectInstructions/addendums/multi_team.md -index 4b45257b0..3d603aff9 100644 ---- a/docs_build/dev/ProjectInstructions/addendums/multi_team.md -+++ b/docs_build/dev/ProjectInstructions/addendums/multi_team.md -@@ -1,5 +1,18 @@ - # Multi-Team Codex Execution Governance - -+## Four Active Delivery Teams -+ -+The single authoritative four-team ownership definition is: -+ -+`docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md` -+ -+Use the `Current Four-Team Ownership Model` section there for active delivery team ownership. -+ -+Rules: -+- Team work must stay inside the owning team's area. -+- Cross-team work requires OWNER approval and must identify the correct owning team for each PR. -+- Team start commands must use the current ownership model before pulling a backlog item. -+ - ## All-Team Preferred Codex Execution Method - - Preferred execution model: -diff --git a/docs_build/dev/ProjectInstructions/addendums/table_first_ui.md b/docs_build/dev/ProjectInstructions/addendums/table_first_ui.md -index 23ceb1622..5645ebcbb 100644 ---- a/docs_build/dev/ProjectInstructions/addendums/table_first_ui.md -+++ b/docs_build/dev/ProjectInstructions/addendums/table_first_ui.md -@@ -15,3 +15,69 @@ Avoid: - - Reference implementation: - Idea Board is the reference implementation. -+ -+ -+DB base -+Creator Table 1 -+Parent Table *-1 user -+Child Table -- *-1 Parent -+ -+ -+No selected Items -+┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐ -+│ Idea Board │ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Idea │ Pitch │ Status │ Updated │ Notes │ Actions │ -+├───────────────┼─────────────────────────────────────────┼───────────┼────────────┼─────────┼─────────┤ -+│ Top Thougts │ Smartest person wins... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Sky Orchard │ Grow floating islands... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ Clockwork... │ Deliver messages through looping city...│ New │ 2026-06-20 │ 0 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ [ Add Idea ] │ -+└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ -+ -+ -+Clicking {Sky Orchard {chevron down arrow}] Expands/Acording the Note(s) for that Idea -+┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ -+│ Idea Board │ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Idea │ Pitch │ Status │ Updated │ Notes │ Actions │ -+├───────────────┼─────────────────────────────────────────┼───────────┼────────────┼─────────┼─────────┤ -+│ Top Thougts │ Smartest person wins... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Sky Orchard[^}│ Grow floating islands... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ Notes │ -+│ --------------------------------------------------------------------------------------------------- │ -+│ note 1 [Edit] [Delete] │ -+│ System seed note: compare early ideas before project creation. [Edit] │ -+│ Ask whether the core loop is planning, defense, or both. [Edit] [Delete] │ -+│ │ -+│ [ Add Note ] │ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Clockwork... │ Deliver messages through looping city...│ New │ 2026-06-20 │ 0 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ [ Add Idea ] │ -+└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ -+ -+Clickin Add Idea -+┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ -+│ Idea Board │ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Idea │ Pitch │ Status │ Updated │ Notes │ Actions │ -+├───────────────┼─────────────────────────────────────────┼───────────┼────────────┼─────────┼─────────┤ -+│ Top Thougts │ Smartest person wins... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┬─────────────────────────────────────────┬───────────┬────────────┬─────────┬─────────┤ -+│ Sky Orchard │ Grow floating islands... │ Exploring │ 2026-06-20 │ 3 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ Clockwork... │ Deliver messages through looping city...│ New │ 2026-06-20 │ 0 Notes │ Edit Del│ -+├───────────────┴─────────────────────────────────────────┴───────────┴────────────┴─────────┴─────────┤ -+│ [input.....] │ [input.....] │ [Dropdown]│ [autofile] │ 0 Notes │ Save Can│ -+└──────────────────────────────────────────────────────────────────────────────────────────────────────┘ -+ + ++Determine whether GitHub PR #133 fully supersedes GitHub PR #120 for the shared toolbox selected-game status bar. + ++This PR is report-only. It does not merge PRs, close PRs, delete branches, or modify runtime code. + ++## GitHub Authority Snapshot + -diff --git a/docs_build/dev/ProjectInstructions/addendums/team_start_and_release.md b/docs_build/dev/ProjectInstructions/addendums/team_start_and_release.md -index 3f85bbf25..e1018c155 100644 ---- a/docs_build/dev/ProjectInstructions/addendums/team_start_and_release.md -+++ b/docs_build/dev/ProjectInstructions/addendums/team_start_and_release.md -@@ -20,6 +20,17 @@ Before a team starts, validate: - - assigned team uses NATO phonetic naming - - work remains with the assigned team until complete or OWNER reassignment - -+## Current Four-Team Start Set -+ -+The current active delivery teams for backlog start commands are: -+ -+- Team Alfa -+- Team Bravo -+- Team Charlie -+- Team Delta -+ -+Each team start must confirm the selected backlog item is inside that team's ownership area. -+ - ## Assignment Flow - - For backlog-driven work: -diff --git a/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md b/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -index 09cc34579..0b8942261 100644 ---- a/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -+++ b/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -@@ -4,17 +4,17 @@ - - ### Idea - --0% Complete — Dream, brainstorm, and explore early game concepts -+33% Complete — Dream, brainstorm, and explore early game concepts - --- [ ] Alfa - Idea Board -+- [x] Alfa - Idea Board - - [ ] Alfa - Game Concept Notes - - [ ] Alfa - Creator Learning - - ### Design - --0% Complete — Shape the game's story, structure, and systems -+17% Complete — Shape the game's story, structure, and systems - --- [ ] Alfa - Game Hub -+- [x] Alfa - Game Hub - - [ ] Alfa - Game Design - - [ ] Alfa - Game Configuration - - [ ] Alfa - Game Crew -@@ -159,3 +159,45 @@ - - Open PR, draft PR, local branch, and remote branch review requirements verified. - - Recommendation-only hygiene audit process verified. - - Branch deletion and PR closure remain prohibited without explicit owner approval. -+ -+## Four-Team Backlog Alignment -+ -+### Team Alfa -+ -+- [ ] Alfa - Game Hub polish -+- [ ] Alfa - Game Journey completion tracking -+- [ ] Alfa - Journey progress calculations -+- [ ] Alfa - Creator onboarding flow -+- [ ] Alfa - Game Hub image integration -+ -+### Team Bravo -+ -+- [ ] Bravo - Audio tool improvements -+- [ ] Bravo - Audio Effects tool -+- [ ] Bravo - Messages tool -+- [ ] Bravo - Emotion Profiles -+- [ ] Bravo - TTS Profiles -+- [ ] Bravo - Asset Browser enhancements -+- [ ] Bravo - Vector Art improvements -+- [ ] Bravo - MIDI Studio improvements -+ -+### Team Charlie -+ -+- [ ] Charlie - Guardrail hardening -+- [ ] Charlie - Browser validation hardening -+- [ ] Charlie - Remaining test relocation audit -+- [ ] Charlie - Compliance baseline freeze -+- [ ] Charlie - System Health improvements -+- [ ] Charlie - Infrastructure dashboard -+- [ ] Charlie - Environment validation -+ -+### Team Delta -+ -+- [ ] Delta - Shared JS consolidation -+- [ ] Delta - API client consolidation -+- [ ] Delta - Runtime performance audit -+- [ ] Delta - Engine test coverage improvements -+- [ ] Delta - Event system audit -+- [ ] Delta - Controls runtime framework audit -+- [ ] Delta - Object runtime framework audit -+- [ ] Delta - World runtime framework audit -diff --git a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md -index 9a3c515f7..acefbba8e 100644 ---- a/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md -+++ b/docs_build/dev/ProjectInstructions/team_assignments/TEAM_ASSIGNMENTS.md -@@ -57,6 +57,14 @@ Active assignment: none. - - Active branch: none. - -+## Team Delta -+ -+Status: Available -+ -+Active assignment: none. -+ -+Active branch: none. -+ - ## Team OWNER - - Status: Available -diff --git a/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md b/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -index baa1598b6..865cca551 100644 ---- a/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -+++ b/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -@@ -1,43 +1,56 @@ - # Team Ownership Governance - --## OWNER -+## Current Four-Team Ownership Model - --- Architecture --- Product Direction --- Cross-Team Governance --- Final Approval -+This section is the current OWNER-approved active ownership alignment. - - ## Team Alfa - --- Idea Board - - Game Hub - - Game Journey --- Design --- Objects --- Worlds --- Interface --- Controls --- Rules --- Progression -+- Idea Board -+- Creator workflow -+- Creator onboarding -+- UX flow - - ## Team Bravo - --- Graphics - - Audio -+- Audio Effects - - Messages --- TTS --- MIDI --- Publishing --- Community --- Marketplace -+- Emotion Profiles -+- TTS Profiles -+- Asset Browser -+- Vector Art -+- MIDI Studio -+- Creator content tools - - ## Team Charlie - --- Governance -+- Repository compliance -+- Validation - - Infrastructure --- Operations --- Diagnostics -+- Storage -+- Environment management - - System Health -+- Operations -+ -+## Team Delta -+ -+- Engine -+- Runtime -+- Shared JS -+- API clients -+- Event systems -+- Performance -+- Technical debt remediation -+- Runtime test coverage -+ -+## Current Four-Team Rule -+ -+Alfa, Bravo, Charlie, and Delta are the four active delivery teams for backlog ownership and team start routing. -+ -+Each team may pull only from its ownership area unless OWNER explicitly reassigns, splits, or approves cross-team work. - - ## Rule - -diff --git a/docs_build/dev/reports/PR_26172_CHARLIE_001-repository-compliance-audit.md b/docs_build/dev/reports/PR_26172_CHARLIE_001-repository-compliance-audit.md -new file mode 100644 -index 000000000..ff09afb85 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26172_CHARLIE_001-repository-compliance-audit.md -@@ -0,0 +1,236 @@ -+# PR_26172_CHARLIE_001 Repository Compliance Audit -+ -+## Scope -+ -+Audit repository compliance against: -+ -+- PR_034 Canonical Repository Structure -+- PR_035 Test Structure Standardization -+- PR_036 Legacy Migration Policy -+ -+Reviewed areas: -+ -+- `toolbox/` -+- `assets/` -+- `tests/` -+- `api/` -+- `serverside/` -+- `src/engine/` -+ -+This PR is audit-only. No executable implementation changes were made. -+ -+## Team Ownership -+ -+- TEAM token: CHARLIE -+- Ownership classification: governance / repository compliance / diagnostics -+- TEAM ownership result: PASS -+ -+## Branch Validation -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Started from latest main | PASS | `main` was pulled before branch creation; source commit `c4a495f0aa8e32d499ca64555c4a3547e6fcb298`. | -+| Worktree clean before branch | PASS | `git status --short` returned no output before branch creation. | -+| Local/origin sync before branch | PASS | `git rev-list --left-right --count HEAD...origin/main` returned `0 0`. | -+| PR branch created from main | PASS | Branch `pr/26172-CHARLIE-001-repository-compliance-audit` was created from latest `main`. | -+| Runtime/source edits avoided | PASS | Planned changed files are reports only under `docs_build/dev/reports/`. | -+ -+## Repository Area Results -+ -+| Area | Status | Findings | -+| --- | --- | --- | -+| `toolbox/` | FAIL | Active tool JavaScript remains beside tool HTML entries instead of canonical `assets/toolbox/{tool-name}/js/index.js` or shared asset roots. | -+| `assets/` | PASS | No non-compliant JS or CSS files were found in `assets/`; scanned JS/CSS lives under `assets/theme-v2/`. | -+| `tests/` | FAIL | 494 files are in non-canonical top-level test locations outside `tests/toolbox/`, `tests/engine/`, `tests/api/`, `tests/server/`, `tests/js/shared/`, or `tests/regression/`. | -+| `api/` | FAIL | Canonical root is absent. No API files were found to classify, but the expected top-level API area is not physically present. | -+| `serverside/` | FAIL | Canonical root is absent. No serverside files were found to classify, but the expected top-level serverside area is not physically present. | -+| `src/engine/` | FAIL | Most engine JS is under feature folders, but `src/engine/paletteList.js` is a root-level JS file and `src/engine/ui/*.css` contains CSS outside canonical asset/theme roots. | -+ -+## Non-Compliant JS Locations -+ -+### `toolbox/` -+ -+These active JavaScript files are outside the canonical tool asset structure: -+ -+- `toolbox/assets/assets-api-client.js` -+- `toolbox/assets/assets-upload-worker.js` -+- `toolbox/assets/assets.js` -+- `toolbox/colors/colors.js` -+- `toolbox/colors/palette-api-client.js` -+- `toolbox/controls/controls-api-client.js` -+- `toolbox/controls/controls.js` -+- `toolbox/game-configuration/game-configuration-api-client.js` -+- `toolbox/game-configuration/game-configuration.js` -+- `toolbox/game-design/game-design-api-client.js` -+- `toolbox/game-design/game-design.js` -+- `toolbox/game-hub/game-hub-api-client.js` -+- `toolbox/game-hub/game-hub.js` -+- `toolbox/game-journey/game-journey-api-client.js` -+- `toolbox/game-journey/game-journey.js` -+- `toolbox/idea-board/index.js` -+- `toolbox/messages/message-tts-service-registry.js` -+- `toolbox/messages/messages-api-client.js` -+- `toolbox/messages/messages.js` -+- `toolbox/objects/objects-api-client.js` -+- `toolbox/objects/objects.js` -+- `toolbox/tags/tags-api-client.js` -+- `toolbox/tags/tags.js` -+- `toolbox/text-to-speech/text2speech.js` -+- `toolbox/tool-registry-api-client.js` -+- `toolbox/toolRegistry.js` -+- `toolbox/tools-page-accordions.js` -+ -+Recommended target pattern: -+ -+- Tool-specific JS: `assets/toolbox/{tool-name}/js/index.js` -+- Shared toolbox JS: `assets/js/shared/` -+ -+### `src/engine/` -+ -+- `src/engine/paletteList.js` -+ -+Recommended target pattern: -+ -+- Move under a feature folder such as `src/engine/palette/` after import-impact review. -+ -+## Non-Compliant CSS Locations -+ -+### `toolbox/` -+ -+- None found. -+ -+### `assets/` -+ -+- None found. -+ -+### `src/engine/` -+ -+These CSS files are active style assets under engine source: -+ -+- `src/engine/ui/baseLayout.css` -+- `src/engine/ui/hubCommon.css` -+- `src/engine/ui/spriteEditor.css` -+ -+Recommended target pattern: -+ -+- Move shared UI/theme styling into `assets/theme-v2/css/` or create an approved engine UI style policy before relocation. -+ -+## Non-Compliant Test Locations -+ -+Canonical test roots from PR_035: ++| PR | Title | State | Draft | Mergeable | Base | Head | ++| --- | --- | --- | --- | --- | --- | --- | ++| #120 | `[codex] PR_26175_ALFA_003 toolbox status bar single row polish` | open | yes | no | `main` at `6d94477bb0ae9f63dd1466dbb89e4a437b8749b0` | `codex/pr-26175-alfa-003-toolbox-status-bar-single-row-polish` at `8a4ab291b9b948e3fe93a4359376bab7f1886dea` | ++| #133 | `PR_26175_ALFA_009-status-bar-single-row-rebuild` | open | yes | yes | `main` at `5415f6675d7a0f10931b83368948a83df98d8021` | `codex/pr-26175-alfa-009-status-bar-single-row-rebuild` at `025aac91acb67565ae92de8fad4def6135ce85b5` | + -+- `tests/toolbox/{tool-name}/` -+- `tests/engine/{feature-name}/` -+- `tests/api/{feature-name}/` -+- `tests/server/{feature-name}/` -+- `tests/js/shared/` -+- `tests/regression/` ++## Executive Answer + -+The audit found 494 files in non-canonical test locations: ++Recommendation: **Close #120 and merge #133**. + -+| Path | File Count | Examples | -+| --- | ---: | --- | -+| `tests/ai/` | 1 | `tests/ai/AIBehaviors.test.mjs` | -+| `tests/assets/` | 1 | `tests/assets/AssetLoaderSystem.test.mjs` | -+| `tests/audio/` | 1 | `tests/audio/AudioService.test.mjs` | -+| `tests/combat/` | 1 | `tests/combat/Combat.test.mjs` | -+| `tests/config/` | 1 | `tests/config/ConfigStore.test.mjs` | -+| `tests/core/` | 11 | `tests/core/EngineCoreBoundaryBaseline.test.mjs` | -+| `tests/dev-runtime/` | 31 | `tests/dev-runtime/AdminHealthOperations.test.mjs` | -+| `tests/entity/` | 1 | `tests/entity/Entity.test.mjs` | -+| `tests/events/` | 2 | `tests/events/EventBus.test.mjs` | -+| `tests/final/` | 11 | `tests/final/ReleaseReadinessSystems.test.mjs` | -+| `tests/fixtures/` | 52 | `tests/fixtures/assets/asset-scenarios.json` | -+| `tests/fx/` | 1 | `tests/fx/ParticleSystem.test.mjs` | -+| `tests/games/` | 35 | `tests/games/AsteroidsValidation.test.mjs` | -+| `tests/helpers/` | 11 | `tests/helpers/playwrightRepoServer.mjs` | -+| `tests/index.html` | 1 | `tests/index.html` | -+| `tests/input/` | 8 | `tests/input/InputService.test.mjs` | -+| `tests/persistence/` | 1 | `tests/persistence/StorageService.test.mjs` | -+| `tests/playwright/` | 44 | `tests/playwright/tools/GameJourneyTool.spec.mjs` | -+| `tests/playwright_installation.txt` | 1 | `tests/playwright_installation.txt` | -+| `tests/production/` | 3 | `tests/production/ProductionReadiness.test.mjs` | -+| `tests/README.md` | 1 | `tests/README.md` | -+| `tests/render/` | 1 | `tests/render/Renderer.test.mjs` | -+| `tests/replay/` | 2 | `tests/replay/ReplaySystem.test.mjs` | -+| `tests/results/` | 26 | `tests/results/playwright-results.json` | -+| `tests/run-tests.mjs` | 1 | `tests/run-tests.mjs` | -+| `tests/runtime/` | 81 | `tests/runtime/V2SessionPersistence.test.mjs` | -+| `tests/samples/` | 1 | `tests/samples/FullscreenRuleEnforcement.test.mjs` | -+| `tests/scenes/` | 3 | `tests/scenes/SceneManager.test.mjs` | -+| `tests/schemas/` | 1 | `tests/schemas/tool.manifest.schema.json` | -+| `tests/shared/` | 92 | `tests/shared/ProjectContract.test.mjs` | -+| `tests/testRunner.html` | 1 | `tests/testRunner.html` | -+| `tests/testRunner.js` | 1 | `tests/testRunner.js` | -+| `tests/tools/` | 57 | `tests/tools/ToolBoundaryEnforcement.test.mjs` | -+| `tests/validation/` | 3 | `tests/validation/samples.runtime.validation.report.json` | -+| `tests/vector/` | 1 | `tests/vector/VectorMath.test.mjs` | -+| `tests/world/` | 4 | `tests/world/WorldSystems.test.mjs` | ++#133 should replace #120. It carries the same intended creator-facing status bar behavior from #120, rebuilds it on current `main`, and is mergeable while #120 is stale and not mergeable. No creator-visible behavior appears lost. + -+Generated Playwright result artifacts under `tests/results/` should be treated as cleanup/archive candidates rather than active test source. ++Nuance: #133 does not preserve #120's exact fullscreen reserve implementation or exact reserve-equality Playwright assertion. #120 used `margin-block-end: var(--toolbox-status-bar-height)` on `.tool-center-panel` and asserted that reserve equaled the status bar height. #133 instead reserves space through fullscreen workspace and column sizing, including the platform-banner top reserve, and validates the observable behavior that the center panel stops above the fixed status bar. This is an implementation and validation-shape change, not a visible behavior loss. + -+## Legacy Migration Candidates ++## 1. Files Changed Comparison + -+| Priority | Candidate | Reason | Recommended Handling | ++| File | PR #120 | PR #133 | Comparison | +| --- | --- | --- | --- | -+| P0 | `tests/results/` | Generated result artifacts are tracked under active tests. | Move to ignored output or archive/report storage after owner approval. | -+| P1 | `toolbox/*/*.js` and shared `toolbox/*.js` | Active JS is colocated with HTML entries instead of canonical asset roots. | Migrate tool JS to `assets/toolbox/{tool-name}/js/index.js`; migrate shared JS to `assets/js/shared/`. | -+| P1 | `tests/dev-runtime/`, `tests/playwright/`, `tests/runtime/`, `tests/shared/`, `tests/tools/` | Large active test buckets conflict with PR_035 canonical test roots. | Split by ownership into `tests/toolbox/`, `tests/engine/`, `tests/api/`, `tests/server/`, `tests/js/shared/`, and `tests/regression/`. | -+| P2 | `src/engine/paletteList.js` | Root-level engine JS file is outside `src/engine/{feature-name}/`. | Move to an approved feature folder with import compatibility reviewed. | -+| P2 | `src/engine/ui/*.css` | CSS lives under engine source instead of theme/tool asset roots. | Move to `assets/theme-v2/css/` or define an explicit engine UI CSS policy. | -+| P3 | Missing `api/` and `serverside/` roots | Canonical target roots are absent. | Create roots when the first API/server migration needs them, with placeholder README only if governance permits. | -+ -+## Prioritized Remediation List -+ -+1. Remove or archive tracked generated artifacts under `tests/results/`. -+2. Create a staged migration plan for `toolbox/` JavaScript sidecars, starting with shared files (`toolRegistry.js`, `tool-registry-api-client.js`, `tools-page-accordions.js`). -+3. Standardize high-volume test buckets in phases: `tests/dev-runtime/`, `tests/playwright/`, `tests/runtime/`, `tests/shared/`, then `tests/tools/`. -+4. Move `src/engine/paletteList.js` into an engine feature folder after import-impact review. -+5. Resolve engine CSS placement for `src/engine/ui/*.css`. -+6. Add or defer canonical `api/` and `serverside/` roots with explicit owner-approved scope. -+ -+## Recommended Next Charlie PRs ++| `assets/theme-v2/js/toolbox-status-bar.js` | Changed | Changed | Same behavior: removes visible labels, purpose text, context pill, and action link; leaves selected-game name and status message; keeps non-visible `data-toolbox-status-context-kind`. | ++| `assets/theme-v2/css/status.css` | Changed | Changed | Same visible status bar behavior. Difference: #133 replaces #120's center-panel margin reserve with workspace/column height reserves and includes top-reserve handling on columns. | ++| `assets/theme-v2/css/layout.css` | Changed | Changed | Same change: shared footer top padding becomes `0px` while bottom padding remains. | ++| `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` | Changed | Changed | Same core coverage for removed labels, no purpose/action/context pill, same-row layout, footer spacing, fullscreen anchoring, Game Hub ownership, missing-game prompt, and Idea Board exclusion. Difference: #120 asserts exact center-panel reserve equals status bar height; #133 asserts the panel bottom is above the status bar. | + -+- `PR_26172_CHARLIE_002-test-results-artifact-cleanup-plan` -+- `PR_26172_CHARLIE_003-toolbox-js-canonical-asset-migration-plan` -+- `PR_26172_CHARLIE_004-test-structure-standardization-plan` -+- `PR_26172_CHARLIE_005-engine-root-js-and-css-migration-plan` -+- `PR_26172_CHARLIE_006-api-serverside-root-readiness-plan` ++Full changed-file set: + -+## Requirement Checklist -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Follow ProjectInstructions | PASS | Read active `docs_build/dev/ProjectInstructions/` governance before work. | -+| Start from latest main | PASS | Main pulled and branch created from `c4a495f0aa8e32d499ca64555c4a3547e6fcb298`. | -+| Worktree clean before work | PASS | `git status --short` returned no output before edits. | -+| Audit PR_034 structure | PASS | Compared scoped paths to canonical toolbox, assets, engine, API, and serverside roots. | -+| Audit PR_035 tests | PASS | Compared `tests/` contents to canonical test roots. | -+| Audit PR_036 legacy migration policy | PASS | Classified migration candidates without moving or deleting files. | -+| Review `toolbox/` | PASS | Identified 27 non-compliant JS files. | -+| Review `assets/` | PASS | Found no non-compliant active JS/CSS in `assets/`. | -+| Review `tests/` | PASS | Identified 494 files in non-canonical test locations. | -+| Review `api/` | PASS | Directory absent; recorded as structural failure. | -+| Review `serverside/` | PASS | Directory absent; recorded as structural failure. | -+| Review `src/engine/` | PASS | Identified root JS and engine CSS placement issues. | -+| No executable implementation changes | PASS | Audit/report-only scope. | -+| Create audit report | PASS | This file. | -+| Create standard Codex reports | PASS | `docs_build/dev/reports/codex_review.diff` and `docs_build/dev/reports/codex_changed_files.txt` exist. | -+| Create ZIP artifact | PASS | `tmp/PR_26172_CHARLIE_001-repository-compliance-audit_delta.zip` exists. | -+ -+## Validation Lane Report -+ -+- `git diff --check`: PASS. -+- Report exists: PASS. -+- Required Codex reports exist: PASS. -+- ZIP artifact exists: PASS. -+- Runtime source files changed: PASS, changed files are limited to `docs_build/dev/reports/`. -+- Playwright: SKIP, audit-only documentation/report PR. -+- Samples: SKIP, audit-only documentation/report PR. -+ -+## Manual Validation Notes -+ -+- The audit intentionally did not move files, delete generated artifacts, create canonical roots, or modify executable source. -+- `api/` and `serverside/` were missing from the working tree; this was recorded as a structural finding, not remediated in this PR. -+- Non-canonical test locations were counted by top-level path under `tests/` to keep the report actionable without rewriting the test tree. -+- Tracked generated artifacts under `tests/results/` are listed as a high-priority cleanup candidate because they are active repository files but not active test source. -diff --git a/docs_build/dev/reports/PR_26172_CHARLIE_002-test-results-artifact-cleanup.md b/docs_build/dev/reports/PR_26172_CHARLIE_002-test-results-artifact-cleanup.md -new file mode 100644 -index 000000000..17898495f ---- /dev/null -+++ b/docs_build/dev/reports/PR_26172_CHARLIE_002-test-results-artifact-cleanup.md -@@ -0,0 +1,129 @@ -+# PR_26172_CHARLIE_002 Test Results Artifact Cleanup -+ -+## Scope -+ -+Clean up generated test result artifacts under `tests/results/` after the Charlie repository compliance audit identified that path as a high-priority cleanup candidate. -+ -+Source audit: -+ -+- `docs_build/dev/reports/PR_26172_CHARLIE_001-repository-compliance-audit.md` -+ -+This PR does not modify runtime source and does not move unrelated tests. -+ -+## Team Ownership -+ -+- TEAM token: CHARLIE -+- Ownership classification: governance / repository hygiene / diagnostics -+- TEAM ownership result: PASS -+ -+## Branch Validation -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Started from latest main | PASS | `main` was pulled before branch creation; source commit `f2b50ac9d79256df3a7716ac4eff21f3a4303bb3`. | -+| Worktree clean before branch | PASS | `git status --short` returned no output before branch creation. | -+| Local/origin sync before branch | PASS | `git rev-list --left-right --count HEAD...origin/main` returned `0 0`. | -+| PR branch created from main | PASS | Branch `pr/26172-CHARLIE-002-test-results-artifact-cleanup` was created from latest `main`. | -+ -+## Files Reviewed -+ -+`git ls-files tests/results` returned no tracked files. -+ -+The local ignored `tests/results/` folder contained generated Playwright/report output: -+ -+- `tests/results/artifacts/.last-run.json` -+- `tests/results/artifacts/tools-MidiStudioV2-MIDI-St-3c5a9-multi-song-manifest-payload-playwright/trace.zip` -+- `tests/results/artifacts/tools-MidiStudioV2-MIDI-St-752e4-on-and-timeline-scroll-sync-playwright/trace.zip` -+- `tests/results/artifacts/tools-MidiStudioV2-MIDI-St-c50c5-m-Tool-Mode-standalone-save-playwright/trace.zip` -+- `tests/results/playwright-results.json` -+- `tests/results/report/data/09daf0cfe8750af5e9e5bb22161367f97296f4fd.zip` -+- `tests/results/report/data/a9ba8bc1c6a629055b981a6f385fa4de3e42a79d.zip` -+- `tests/results/report/data/b1dc1da730cbd5e9adc334a6f385fa4de3e42a79d.zip` -+- `tests/results/report/data/c150573559f5367f4ec5724abb7a55798abcdff9.zip` -+- `tests/results/report/index.html` -+- `tests/results/report/trace/assets/codeMirrorModule-DS0FLvoc.js` -+- `tests/results/report/trace/assets/defaultSettingsView-GTWI-W_B.js` -+- `tests/results/report/trace/codeMirrorModule.DYBRYzYX.css` -+- `tests/results/report/trace/codicon.DCmgc-ay.ttf` -+- `tests/results/report/trace/defaultSettingsView.B4dS75f0.css` -+- `tests/results/report/trace/index.C5466mMT.js` -+- `tests/results/report/trace/index.CzXZzn5A.css` -+- `tests/results/report/trace/index.html` -+- `tests/results/report/trace/manifest.webmanifest` -+- `tests/results/report/trace/playwright-logo.svg` -+- `tests/results/report/trace/snapshot.html` -+- `tests/results/report/trace/sw.bundle.js` -+- `tests/results/report/trace/uiMode.Btcz36p_.css` -+- `tests/results/report/trace/uiMode.Vipi55dB.js` -+- `tests/results/report/trace/uiMode.html` -+- `tests/results/report/trace/xtermModule.DYP7pi_n.css` -+ -+## Files Removed Or Retained -+ -+| Category | Status | Notes | -+| --- | --- | --- | -+| Tracked files under `tests/results/` | None removed | No tracked files existed under `tests/results/`. | -+| Local ignored generated artifacts under `tests/results/` | Removed from workspace | Removed only after verifying the resolved target path was inside the repository. | -+| Active test source | Retained | No active test source was found under `tests/results/`. | -+| Fixture or baseline data | Retained | No committed fixture or baseline dependency was found under `tests/results/`. | -+ -+## Reference And Dependency Check -+ -+| Check | Status | Evidence | -+| --- | --- | --- | -+| Active tracked files under `tests/results/` | PASS | `git ls-files tests/results` returned no output. | -+| Tracked ignored files under `tests/results/` | PASS | `git ls-files -c -i --exclude-standard tests/results` returned no output. | -+| Ignored local generated files under `tests/results/` | PASS | `git ls-files -o -i --exclude-standard tests/results` listed only Playwright/report artifacts. | -+| Active config uses `tmp/test-results/` | PASS | `playwright.config.cjs` writes output, artifacts, HTML report, and JSON report under `tmp/test-results/`. | -+| Active references to `tests/results/` | PASS | Active config/test/docs search returned no required source or fixture dependency. | -+| Historical references retained | PASS | References in `archive/` and historical `docs_build/dev/reports/` were not modified. | -+ -+## Ignore Rule Changes -+ -+Updated `.gitignore` to make generated test-output protection explicit: -+ -+- Kept `tests/results/`. -+- Added `tests/results/**`. -+- Kept `tmp/test-results/`. -+- Added `tmp/test-results/**`. -+- Confirmed `tmp/` remains ignored. -+ -+Ignore probe: -+ -+- `git check-ignore -v tests/results/probe.txt` resolves to `.gitignore`. -+- `git check-ignore -v tmp/test-results/probe.txt` resolves to `.gitignore`. -+ -+## Requirement Checklist -+ -+| Requirement | Status | Evidence | ++| Category | PR #120 | PR #133 | +| --- | --- | --- | -+| Confirm Project Instructions were reviewed | PASS | Read `docs_build/dev/ProjectInstructions/README.txt`, `PROJECT_INSTRUCTIONS.md`, branch/workflow governance, team ownership, and artifact/reporting rules. | -+| Use PR_26172_CHARLIE_001 findings | PASS | This cleanup is based on the P0 `tests/results/` finding. | -+| Review `tests/results/` | PASS | Reviewed tracked, ignored, and local generated contents. | -+| Confirm generated artifacts, not active source | PASS | Files were Playwright JSON, HTML report, trace assets, and zipped trace/report data. | -+| Search references to `tests/results/` files | PASS | No active source/fixture dependency found; historical references retained. | -+| Remove tracked generated artifacts if safe | PASS | No tracked generated artifacts existed to remove. | -+| Add/update ignore rules | PASS | `.gitignore` now explicitly includes `tests/results/**` and `tmp/test-results/**`. | -+| Do not remove active test source | PASS | No active test source removed. | -+| Do not modify runtime source | PASS | No runtime source changed. | -+| Do not move unrelated tests | PASS | No test files were moved. | -+| Stop gate not triggered | PASS | No `tests/results/` file was required as active source, fixture data, or committed baseline data. | -+| Create required reports | PASS | `docs_build/dev/reports/codex_review.diff` and `docs_build/dev/reports/codex_changed_files.txt` exist. | -+| Create ZIP artifact | PASS | `tmp/PR_26172_CHARLIE_002-test-results-artifact-cleanup_delta.zip` exists. | -+ -+## Validation Lane Report -+ -+- `git diff --check`: PASS. -+- Cleanup limited to generated artifacts under `tests/results/`: PASS. -+- Ignore rule prevents recommit: PASS. -+- Runtime source files changed: PASS, no runtime source files changed. -+- Required reports exist: PASS. -+- ZIP artifact exists: PASS. -+- Playwright: SKIP, ignore/report-only cleanup with no active test or runtime source changes. -+- Samples: SKIP, no sample files changed. -+ -+## Manual Validation Notes -+ -+- The local ignored `tests/results/` directory was deleted from the workspace only after path verification showed it was inside the repository root. -+- Repository history already contains `docs_build/dev/reports/docs_archive_test_output_cleanup_report.md`, which documents the prior migration of generated test output from `tests/results/` to `tmp/test-results/`. -+- This PR preserves historical report/archive references and only hardens the active ignore rule. -diff --git a/docs_build/dev/reports/PR_26172_CHARLIE_003-src-dev-runtime-test-migration-audit.md b/docs_build/dev/reports/PR_26172_CHARLIE_003-src-dev-runtime-test-migration-audit.md -new file mode 100644 -index 000000000..3a9529821 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26172_CHARLIE_003-src-dev-runtime-test-migration-audit.md -@@ -0,0 +1,94 @@ -+# PR_26172_CHARLIE_003 Src Dev-Runtime Test Migration Audit -+ -+## Scope ++| Runtime/shared UI files | `assets/theme-v2/css/layout.css`, `assets/theme-v2/css/status.css`, `assets/theme-v2/js/toolbox-status-bar.js` | Same three files | ++| Test files | `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` | Same file | ++| Build/report files | `docs_build/dev/BUILD_PR.md`, ALFA_003 reports, `codex_changed_files.txt`, `codex_review.diff` | `docs_build/dev/BUILD_PR.md`, ALFA_009 reports, `codex_changed_files.txt`, `codex_review.diff` | + -+Audit `src/dev-runtime/` for test files that should move into canonical test paths. ++## 2. Feature Comparison + -+Canonical test targets reviewed: -+ -+- `tests/server/` -+- `tests/api/` -+- `tests/engine/` -+- `tests/toolbox/` -+ -+This PR is audit-only. No files were moved and no runtime source was modified. -+ -+## Branch Validation -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Started from latest main | PASS | Workstream branch was created from `main` at `b97893c78dcfed05d5b0de0c7d03127ec5575292`. | -+| Worktree clean before branch | PASS | `git status --short` returned no output before branch creation. | -+| Local/origin sync before branch | PASS | `git rev-list --left-right --count HEAD...origin/main` returned `0 0`. | -+| Active branch | PASS | `PR_26172_CHARLIE_repository-compliance-stack`. | -+| Team ownership | PASS | Team Charlie owns governance, infrastructure, operations, diagnostics, and system health. | ++| Feature | PR #120 | PR #133 | Superseded by #133? | ++| --- | --- | --- | --- | ++| Single visible status bar row | Yes | Yes | PASS | ++| Selected game name on left | Yes | Yes | PASS | ++| Status message centered | Yes | Yes | PASS | ++| Remove visible selected-game labels | Yes | Yes | PASS | ++| Remove selected-game purpose from visible bar | Yes | Yes | PASS | ++| Remove visible status category pill labels | Yes | Yes | PASS | ++| Remove status action link | Yes | Yes | PASS | ++| Preserve non-visible context classification data | Yes | Yes | PASS | ++| Preserve Game Hub selected-game ownership | Yes | Yes | PASS | ++| Preserve Idea Board selected-game filtering exclusion | Yes | Yes | PASS | ++| Remove footer/status extra spacing | Yes | Yes | PASS | ++| Preserve fullscreen bottom anchoring | Yes | Yes | PASS | ++| Prevent center content from being hidden behind fixed status bar | Yes | Yes | PASS | ++| Account for platform banner in fullscreen sizing | Partial: workspace top reserve exists, but column sizing does not subtract top reserve | Yes: workspace and column sizing subtract top reserve | PASS, #133 improves this area | ++ ++## 3. Validation Comparison ++ ++| Validation | PR #120 | PR #133 | Comparison | ++| --- | --- | --- | --- | ++| Targeted Playwright | PASS: `npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1`, 6 passed | PASS: same command, 6 passed | Equivalent pass result | ++| Inline style/style block scan | PASS: `rg -n "`, inline ` -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/colors/index.html b/toolbox/colors/index.html -+index a0e9e8276..c3f03f4de 100644 -+--- a/toolbox/colors/index.html -++++ b/toolbox/colors/index.html -+@@ -376,7 +376,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/colors/palette-api-client.js b/toolbox/colors/palette-api-client.js -+deleted file mode 100644 -+index 127b5a70b..000000000 -+--- a/toolbox/colors/palette-api-client.js -++++ /dev/null -+@@ -1,33 +0,0 @@ -+-import { -+- callServerToolFunction, -+- createServerRepositoryClient, -+- readServerToolConstants, -+- requireServerConstant, -+-} from "../../src/api/server-api-client.js"; -+- -+-const constants = readServerToolConstants("palette"); -+- -+-export const PALETTE_SOURCE_USER = requireServerConstant(constants, "PALETTE_SOURCE_USER", "palette"); -+-export const PALETTE_TOOL_KEY = requireServerConstant(constants, "PALETTE_TOOL_KEY", "palette"); -+-export const PALETTE_WORKSPACE_PATH = requireServerConstant(constants, "PALETTE_WORKSPACE_PATH", "palette"); -+-export const CURATED_PALETTE_COLLECTIONS = Object.freeze(requireServerConstant(constants, "CURATED_PALETTE_COLLECTIONS", "palette")); -+-export const NUMERIC_VARIANT_COUNTS = Object.freeze(requireServerConstant(constants, "NUMERIC_VARIANT_COUNTS", "palette")); -+-export const PALETTE_GENERATOR_DEFAULTS = Object.freeze(requireServerConstant(constants, "PALETTE_GENERATOR_DEFAULTS", "palette")); -+-export const PALETTE_VARIANTS = Object.freeze(requireServerConstant(constants, "PALETTE_VARIANTS", "palette")); -+-export const PICKER_PREVIEW_DEFAULTS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_DEFAULTS", "palette")); -+-export const PICKER_PREVIEW_SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_SORT_OPTIONS", "palette")); -+-export const SIZE_OPTIONS = Object.freeze(requireServerConstant(constants, "SIZE_OPTIONS", "palette")); -+-export const SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "SORT_OPTIONS", "palette")); -+-export const SUGGESTED_TAGS = Object.freeze(requireServerConstant(constants, "SUGGESTED_TAGS", "palette")); -+- -+-export function createGameWorkspacePaletteApiRepository(options = {}) { -+- return createServerRepositoryClient("palette", options); -+-} -+- -+-export function normalizePaletteSwatchInput(input) { -+- return callServerToolFunction("palette", "normalizePaletteSwatchInput", input); -+-} -+- -+-export function validatePaletteSwatchInput(input, existingSwatches, options) { -+- return callServerToolFunction("palette", "validatePaletteSwatchInput", input, existingSwatches, options); -+-} -+diff --git a/toolbox/controls/index.html b/toolbox/controls/index.html -+index 5e23377e8..99a8e8427 100644 -+--- a/toolbox/controls/index.html -++++ b/toolbox/controls/index.html -+@@ -118,7 +118,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/game-configuration/game-configuration-api-client.js b/toolbox/game-configuration/game-configuration-api-client.js -+deleted file mode 100644 -+index 649591dd4..000000000 -+--- a/toolbox/game-configuration/game-configuration-api-client.js -++++ /dev/null -+@@ -1,14 +0,0 @@ -+-import { -+- createServerRepositoryClient, -+- readServerToolConstants, -+- requireServerConstant, -+-} from "../../src/api/server-api-client.js"; -+- -+-const constants = readServerToolConstants("game-configuration"); -+- -+-export const GAME_CONFIGURATION_SECTIONS = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_SECTIONS", "game-configuration")); -+-export const GAME_CONFIGURATION_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_PLAYER_MODES", "game-configuration")); -+- -+-export function createGameConfigurationApiRepository(options = {}) { -+- return createServerRepositoryClient("game-configuration", options); -+-} -+diff --git a/toolbox/game-configuration/index.html b/toolbox/game-configuration/index.html -+index c62f1afc0..ae41be477 100644 -+--- a/toolbox/game-configuration/index.html -++++ b/toolbox/game-configuration/index.html -+@@ -168,7 +168,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/game-design/game-design-api-client.js b/toolbox/game-design/game-design-api-client.js -+deleted file mode 100644 -+index 561906791..000000000 -+--- a/toolbox/game-design/game-design-api-client.js -++++ /dev/null -+@@ -1,16 +0,0 @@ -+-import { -+- createServerRepositoryClient, -+- readServerToolConstants, -+- requireServerConstant, -+-} from "../../src/api/server-api-client.js"; -+- -+-const constants = readServerToolConstants("game-design"); -+- -+-export const GAME_DESIGN_GAME_TYPES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GAME_TYPES", "game-design")); -+-export const GAME_DESIGN_GENRES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GENRES", "game-design")); -+-export const GAME_DESIGN_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAYER_MODES", "game-design")); -+-export const GAME_DESIGN_PLAY_STYLES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAY_STYLES", "game-design")); -+- -+-export function createGameDesignApiRepository(options = {}) { -+- return createServerRepositoryClient("game-design", options); -+-} -+diff --git a/toolbox/game-design/index.html b/toolbox/game-design/index.html -+index 3335515ac..e8fda5433 100644 -+--- a/toolbox/game-design/index.html -++++ b/toolbox/game-design/index.html -+@@ -195,7 +195,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js -+index d2260fe04..50bbe46f1 100644 -+--- a/toolbox/game-hub/game-hub.js -++++ b/toolbox/game-hub/game-hub.js -+@@ -9,36 +9,22 @@ import { getSessionCurrent } from "../../src/api/session-api-client.js"; -+ const repository = createGameHubApiRepository(); -+ -+ const elements = { -+- activeGameName: document.querySelector("[data-active-game-name]"), -+- activeGameOwner: document.querySelector("[data-active-game-owner]"), -+- activeGamePurpose: document.querySelector("[data-active-game-purpose]"), -+- activeGameStatus: document.querySelector("[data-active-game-status]"), -+- currentFocus: document.querySelector("[data-current-focus]"), -+- currentUserRole: document.querySelector("[data-current-user-role]"), -+ currentUserRoleInput: document.querySelector("[data-current-user-role-input]"), -+ deleteOpenGame: document.querySelector("[data-game-delete-active]"), -+- form: document.querySelector("[data-game-form]"), -+ membersTable: document.querySelector("[data-game-members-table]"), -+- nameInput: document.querySelector("[data-game-name-input]"), -+ progressChecklist: document.querySelector("[data-game-progress-checklist]"), -+ gameList: document.querySelector("[data-game-list]"), -+- gameProgress: document.querySelector("[data-game-progress]"), -+- gameJourneyLink: document.querySelector("[data-game-journey-link]"), -+ projectRecordStatus: document.querySelector("[data-project-record-status]"), -+- projectRecordsTable: document.querySelector("[data-project-records-table]"), -+- purposeInput: document.querySelector("[data-game-purpose-input]"), -+- sourceIdeaDisplay: document.querySelector("[data-source-idea-display]"), -+- sourceIdeaName: document.querySelector("[data-source-idea-name]"), -+- sourceIdeaNotes: document.querySelector("[data-source-idea-notes]"), -+- sourceIdeaPitch: document.querySelector("[data-source-idea-pitch]"), -+- gameStatus: document.querySelector("[data-game-status]"), -+- gameStatusInput: document.querySelector("[data-game-status-input]"), -+- publishingProgress: document.querySelector("[data-publishing-progress]"), -+- recommendedNextTool: document.querySelectorAll("[data-recommended-next-tool]"), -+ statusLog: document.querySelector("[data-game-hub-log]"), -+ tableCounts: document.querySelector("[data-game-table-counts]"), -+ }; -+ -++const state = { -++ addingGame: false, -++ editingGameId: "", -++ expandedGameId: "", -++}; -++ -+ function setText(element, value) { -+ if (element && typeof element.forEach === "function" && !element.nodeType) { -+ element.forEach((item) => { -+@@ -56,6 +42,15 @@ function setStatusLog(message) { -+ setText(elements.statusLog, message); -+ } -+ -++function notifySelectedGameChanged(activeGame) { -++ window.dispatchEvent(new CustomEvent("gamefoundry:toolbox-selected-game-changed", { -++ detail: { -++ selectedGameId: activeGame?.id || "", -++ source: "game-hub", -++ }, -++ })); -++} -++ -+ function isRecord(value) { -+ return Boolean(value && typeof value === "object"); -+ } -+@@ -143,15 +138,11 @@ function setProjectRecordStatus(message) { -+ -+ function refreshSaveControls(activeGame = null) { -+ const saveAllowed = projectRecordsSaveAllowed(); -+- [elements.nameInput, elements.purposeInput, elements.gameStatusInput, elements.currentUserRoleInput].forEach((control) => { -++ [elements.currentUserRoleInput].forEach((control) => { -+ if (control) { -+ control.disabled = !saveAllowed; -+ } -+ }); -+- const submitButton = elements.form?.querySelector("button[type='submit']"); -+- if (submitButton) { -+- submitButton.disabled = !saveAllowed; -+- } -+ if (elements.deleteOpenGame) { -+ const sourceLinked = isSourceLinkedGame(activeGame); -+ elements.deleteOpenGame.disabled = !saveAllowed || sourceLinked; -+@@ -176,6 +167,18 @@ function ensureProjectRecordsSaveAllowed(action) { -+ return false; -+ } -+ -++function redirectGuestToSignIn() { -++ window.location.href = "account/sign-in.html"; -++} -++ -++function ensureProjectRecordsSaveAllowedForSave() { -++ if (projectRecordsSaveAllowed()) { -++ return true; -++ } -++ redirectGuestToSignIn(); -++ return false; -++} -++ -+ function populateSelect(select, options) { -+ if (!select) { -+ return; -+@@ -204,47 +207,335 @@ function currentGameMember(activeGame) { -+ return activeGameMembers(activeGame).find((member) => member.userKey === userKey) || null; -+ } -+ -+-function createGameButton(game, isActive) { -++function createActionButton(label, action, options = {}) { -+ const button = document.createElement("button"); -+- button.className = isActive ? "btn primary" : "btn"; -++ button.className = options.primary ? "btn btn--compact primary" : "btn btn--compact"; -+ button.type = "button"; -+- button.dataset.gameOpen = game.id; -+- if (isActive) { -++ button.dataset.gameAction = action; -++ if (options.gameId) { -++ button.dataset.gameId = options.gameId; -++ } -++ if (options.disabled) { -++ button.disabled = true; -++ } -++ if (options.ariaLabel) { -++ button.setAttribute("aria-label", options.ariaLabel); -++ } -++ button.textContent = label; -++ return button; -++} -++ -++function createGameButton(game) { -++ const button = createActionButton("Edit", "edit-game", { -++ ariaLabel: `Edit ${game.name}`, -++ gameId: game.id, -++ }); -++ return button; -++} -++ -++function createGameListStatus(message, state) { -++ const emptyState = document.createElement("p"); -++ emptyState.className = "status"; -++ emptyState.dataset.gameListStatus = state; -++ emptyState.textContent = message; -++ return emptyState; -++} -++ -++function createCell(value, tagName = "td") { -++ const cell = document.createElement(tagName); -++ cell.textContent = value; -++ return cell; -++} -++ -++function createSelect(options, selectedValue, datasetName, ariaLabel) { -++ const select = document.createElement("select"); -++ select.dataset[datasetName] = "true"; -++ select.setAttribute("aria-label", ariaLabel); -++ options.forEach((option) => { -++ const item = document.createElement("option"); -++ item.value = option; -++ item.textContent = option; -++ select.append(item); -++ }); -++ select.value = options.includes(selectedValue) ? selectedValue : options[0] || ""; -++ return select; -++} -++ -++function createInput(value, datasetName, ariaLabel, options = {}) { -++ const input = document.createElement("input"); -++ input.dataset[datasetName] = "true"; -++ input.type = "text"; -++ input.value = value || ""; -++ input.placeholder = options.placeholder || ""; -++ input.setAttribute("aria-label", ariaLabel); -++ if (options.required) { -++ input.required = true; -++ } -++ if (options.readOnly) { -++ input.readOnly = true; -++ } -++ return input; -++} -++ -++function createGameToggleButton(game, expanded, active) { -++ const button = document.createElement("button"); -++ button.className = active ? "btn btn--compact primary" : "btn btn--compact"; -++ button.type = "button"; -++ button.dataset.gameToggle = game.id; -++ if (active) { -+ button.dataset.gameActive = "true"; -+ button.setAttribute("aria-current", "true"); -+ } -+- button.textContent = isActive ? `Open ${game.name} (Active)` : `Open ${game.name}`; -++ button.setAttribute("aria-expanded", String(expanded)); -++ const controlledRows = []; -++ if (hasSourceIdeaDetails(game)) { -++ controlledRows.push(`game-child-source-idea-${game.id}`); -++ } -++ controlledRows.push(`game-child-readiness-output-${game.id}`); -++ button.setAttribute("aria-controls", controlledRows.join(" ")); -++ button.textContent = game.name; -+ return button; -+ } -+ -+-function renderProjectInformation(activeGame, currentMember, progress) { -+- if (!elements.projectRecordsTable) { -+- return; -++function gameSourceIdeaDetails(game) { -++ const sourceIdea = isRecord(game?.sourceIdea) ? game.sourceIdea : null; -++ const name = String(sourceIdea?.idea || "").trim(); -++ const pitch = String(sourceIdea?.pitch || "").trim(); -++ const notes = Array.isArray(sourceIdea?.notes) -++ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) -++ : []; -++ return { -++ name, -++ notes, -++ pitch, -++ }; -++} -++ -++function hasSourceIdeaDetails(game) { -++ const sourceIdea = gameSourceIdeaDetails(game); -++ return Boolean(sourceIdea.name || sourceIdea.pitch || sourceIdea.notes.length); -++} -++ -++function renderSourceIdeaChildTable(parent, game) { -++ const sourceIdea = gameSourceIdeaDetails(game); -++ const wrapper = document.createElement("div"); -++ wrapper.className = "table-wrapper"; -++ const table = document.createElement("table"); -++ table.className = "data-table data-table--fixed"; -++ table.dataset.gameChildTable = "source-idea"; -++ table.setAttribute("aria-label", `${game.name} source idea`); -++ table.innerHTML = "
"; -++ const body = document.createElement("tbody"); -++ [ -++ ["Idea", sourceIdea.name || "No source idea yet"], -++ ["Pitch", sourceIdea.pitch || "Create a project from Idea Board to see source details."], -++ ].forEach(([label, value]) => { -++ const row = document.createElement("tr"); -++ row.append(createCell(label, "th"), createCell(value)); -++ row.firstElementChild.scope = "row"; -++ body.append(row); -++ }); -++ -++ const notes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."]; -++ notes.forEach((note, index) => { -++ const row = document.createElement("tr"); -++ row.dataset.sourceIdeaNoteRow = String(index + 1); -++ row.append(createCell(`Note ${index + 1}`, "th"), createCell(note)); -++ row.firstElementChild.scope = "row"; -++ body.append(row); -++ }); -++ -++ table.append(body); -++ wrapper.append(table); -++ parent.append(wrapper); -++} -++ -++function renderReadinessOutputChildTable(parent, game, progress, active) { -++ const readiness = active && isRecord(progress) -++ ? progress -++ : { -++ currentFocus: "Open this game to review readiness", -++ gameProgress: `${game.name} identity ready`, -++ gameStatus: game.status || "No status", -++ publishingProgress: "Open this game to review launch progress", -++ recommendedNextTool: "Game Hub", -++ progressChecklist: [], -++ }; -++ const wrapper = document.createElement("div"); -++ wrapper.className = "table-wrapper"; -++ const table = document.createElement("table"); -++ table.className = "data-table data-table--fixed"; -++ table.dataset.gameChildTable = "readiness-output"; -++ table.setAttribute("aria-label", `${game.name} readiness output`); -++ table.innerHTML = ""; -++ const body = document.createElement("tbody"); -++ [ -++ ["Game Status", readiness.gameStatus], -++ ["Game Progress", readiness.gameProgress], -++ ["Launch Progress", readiness.publishingProgress], -++ ["Current Focus", readiness.currentFocus], -++ ["Recommended Next Tool", readiness.recommendedNextTool], -++ ].forEach(([label, value]) => { -++ const row = document.createElement("tr"); -++ row.append(createCell(label, "th"), createCell(value || "Not available")); -++ row.firstElementChild.scope = "row"; -++ body.append(row); -++ }); -++ -++ const checklist = Array.isArray(readiness.progressChecklist) ? readiness.progressChecklist : []; -++ checklist.forEach((item) => { -++ const row = document.createElement("tr"); -++ row.dataset.readinessChecklistRow = item.label || "Checklist"; -++ row.append(createCell(item.label || "Checklist", "th"), createCell(item.status || "Not available")); -++ row.firstElementChild.scope = "row"; -++ body.append(row); -++ }); -++ -++ table.append(body); -++ wrapper.append(table); -++ parent.append(wrapper); -++} -++ -++function renderExpandedGameRow(tbody, game, progress, active) { -++ const childRows = []; -++ if (hasSourceIdeaDetails(game)) { -++ childRows.push({ -++ id: `game-child-source-idea-${game.id}`, -++ render: (parent) => renderSourceIdeaChildTable(parent, game), -++ type: "source-idea", -++ }); -+ } -++ childRows.push( -++ { -++ id: `game-child-readiness-output-${game.id}`, -++ render: (parent) => renderReadinessOutputChildTable(parent, game, progress, active), -++ type: "readiness-output", -++ }, -++ ); -++ childRows.forEach(({ id, render, type }) => { -++ const row = document.createElement("tr"); -++ row.dataset.gameExpandedRow = game.id; -++ row.dataset.gameChildRow = type; -++ row.id = id; -++ const content = document.createElement("td"); -++ content.colSpan = 4; -++ render(content); -++ row.append(content); -++ tbody.append(row); -++ }); -++} -+ -+- elements.projectRecordsTable.replaceChildren(); -++function renderAddGameRow(tbody) { -+ const row = document.createElement("tr"); -+- [ -+- { datasetName: "activeGameName", value: activeGame?.name || "No game open" }, -+- { datasetName: "activeGameStatus", value: activeGame?.status || progress?.gameStatus || "No Game" }, -+- { datasetName: "activeGamePurpose", value: activeGame?.purpose || "No purpose" }, -+- { datasetName: "activeGameOwner", value: activeGame?.ownerDisplayName || "No owner" }, -+- { datasetName: "currentUserRole", value: currentMember?.role || "Viewer" }, -+- { datasetName: "recommendedNextTool", value: progress?.recommendedNextTool || "Game Hub" }, -+- ].forEach(({ datasetName, value }) => { -++ row.dataset.gameAddRow = state.addingGame ? "input" : "button"; -++ -++ if (!state.addingGame) { -+ const cell = document.createElement("td"); -+- cell.dataset[datasetName] = "true"; -+- cell.textContent = value; -++ cell.colSpan = 4; -++ cell.append(createActionButton("Add Game", "start-add-game")); -+ row.append(cell); -+- }); -+- elements.projectRecordsTable.append(row); -++ tbody.append(row); -++ return; -++ } -++ -++ const nameCell = document.createElement("th"); -++ nameCell.scope = "row"; -++ nameCell.append(createInput("", "gameNameInput", "Game", { -++ placeholder: "Untitled game", -++ required: true, -++ })); -++ -++ const purposeCell = document.createElement("td"); -++ purposeCell.append(createSelect(GAME_HUB_GAME_PURPOSES, "Game", "gamePurposeInput", "Purpose")); -++ -++ const statusCell = document.createElement("td"); -++ statusCell.append(createSelect(GAME_HUB_GAME_STATUSES, "Planning", "gameStatusInput", "Status")); -++ -++ const actions = document.createElement("td"); -++ actions.append( -++ createActionButton("Save", "save-add-game", { primary: true }), -++ createActionButton("Cancel", "cancel-add-game"), -++ ); -++ -++ row.append(nameCell, purposeCell, statusCell, actions); -++ tbody.append(row); -++} -++ -++function renderEditGameRow(tbody, game) { -++ const row = document.createElement("tr"); -++ row.dataset.gameEditRow = game.id; -++ -++ const nameCell = document.createElement("th"); -++ nameCell.scope = "row"; -++ nameCell.append(createInput(game.name, "gameNameInput", "Game", { -++ readOnly: true, -++ })); -++ -++ const purposeCell = document.createElement("td"); -++ purposeCell.append(createSelect(GAME_HUB_GAME_PURPOSES, game.purpose, "gamePurposeInput", "Purpose")); -++ -++ const statusCell = document.createElement("td"); -++ statusCell.append(createSelect(GAME_HUB_GAME_STATUSES, game.status, "gameStatusInput", "Status")); -++ -++ const actions = document.createElement("td"); -++ actions.append( -++ createActionButton("Save", "save-edit-game", { -++ gameId: game.id, -++ primary: true, -++ }), -++ createActionButton("Cancel", "cancel-edit-game", { -++ gameId: game.id, -++ }), -++ ); -++ -++ row.append( -++ nameCell, -++ purposeCell, -++ statusCell, -++ actions, -++ ); -++ tbody.append(row); -++} -++ -++function renderGameParentRow(tbody, game, activeGame, progress) { -++ const expanded = state.expandedGameId === game.id; -++ const active = activeGame?.id === game.id; -++ const editing = state.editingGameId === game.id; -++ -++ if (editing) { -++ renderEditGameRow(tbody, game); -++ return; -++ } -++ -++ const row = document.createElement("tr"); -++ row.dataset.gameRow = game.id; -+ -++ const nameCell = document.createElement("th"); -++ nameCell.scope = "row"; -++ nameCell.append(createGameToggleButton(game, expanded, active)); -++ row.append( -++ nameCell, -++ createCell(game.purpose || "Game"), -++ createCell(game.status || "No status"), -++ ); -++ -++ const actions = document.createElement("td"); -++ actions.append(createGameButton(game)); -++ row.append(actions); -++ tbody.append(row); -++ -++ if (expanded) { -++ renderExpandedGameRow(tbody, game, progress, active); -++ } -++} -++ -++function renderGameTableStatus() { -+ setProjectRecordStatus(projectRecordsSaveAllowed() -+- ? "Project Information loaded." -+- : "Project Information loaded. Sign in to save changes."); -++ ? "Game table loaded." -++ : "Game table loaded. Sign in to save changes."); -+ } -+ -+-function renderGameList() { -++function renderGameList(progress) { -+ if (!elements.gameList) { -+ return; -+ } -+@@ -252,40 +543,34 @@ function renderGameList() { -+ const activeGame = normalizeActiveGame(repository.getActiveGame()); -+ const gameUserKey = currentGameUserKey(activeGame); -+ const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {}); -+- const games = Array.isArray(listResult) ? listResult : []; -+- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) { -+- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly."); -+- } -+ -+ elements.gameList.replaceChildren(); -+ -+- if (games.length === 0) { -+- const emptyState = document.createElement("p"); -+- emptyState.className = "status"; -+- emptyState.textContent = "No games. Create a game to continue."; -+- elements.gameList.append(emptyState); -++ if (!Array.isArray(listResult)) { -++ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."; -++ reportRepositoryError(listResult, "Game Hub projects"); -++ setStatusLog(message); -++ elements.gameList.append(createGameListStatus(message, "unavailable")); -+ return; -+ } -+ -+- games.forEach((game) => { -+- const row = document.createElement("article"); -+- row.className = "callout"; -+- row.dataset.gameRow = game.id; -+- -+- const title = document.createElement("h4"); -+- title.textContent = game.name; -+- -+- const meta = document.createElement("p"); -+- meta.className = "eyebrow"; -+- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`; -+- -+- const isActive = activeGame?.id === game.id; -+- const action = createGameButton(game, isActive); -+- -+- row.append(title, meta, action); -++ if (listResult.length === 0) { -++ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Add a game to start building.", "empty")); -++ } -+ -+- elements.gameList.append(row); -+- }); -++ const wrapper = document.createElement("div"); -++ wrapper.className = "table-wrapper"; -++ const table = document.createElement("table"); -++ table.className = "data-table data-table--fixed"; -++ table.dataset.gameRowsTable = "true"; -++ table.setAttribute("aria-label", "Games"); -++ table.innerHTML = ""; -++ const body = document.createElement("tbody"); -++ listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress)); -++ renderAddGameRow(body); -++ table.append(body); -++ wrapper.append(table); -++ elements.gameList.append(wrapper); -+ } -+ -+ function renderMembersTable(activeGame) { -+@@ -352,29 +637,6 @@ function renderTableCounts() { -+ }); -+ } -+ -+-function renderSourceIdea(activeGame) { -+- const sourceIdea = isRecord(activeGame?.sourceIdea) ? activeGame.sourceIdea : null; -+- const name = String(sourceIdea?.idea || "").trim(); -+- const pitch = String(sourceIdea?.pitch || "").trim(); -+- const notes = Array.isArray(sourceIdea?.notes) -+- ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) -+- : []; -+- -+- setText(elements.sourceIdeaName, name || "No source idea yet"); -+- setText(elements.sourceIdeaDisplay, name || "No source idea yet"); -+- setText(elements.sourceIdeaPitch, pitch || "Create a project from Idea Board to see source details."); -+- -+- if (elements.sourceIdeaNotes) { -+- elements.sourceIdeaNotes.replaceChildren(); -+- const visibleNotes = notes.length ? notes : ["No source notes."]; -+- visibleNotes.forEach((note) => { -+- const item = document.createElement("li"); -+- item.textContent = note; -+- elements.sourceIdeaNotes.append(item); -+- }); -+- } -+-} -+- -+ function renderChecklist(progress) { -+ if (!elements.progressChecklist) { -+ return; -+@@ -395,134 +657,199 @@ function renderWorkspace() { -+ const progress = normalizeProgress(repository.getGameProgress()); -+ const currentMember = currentGameMember(activeGame); -+ -+- setText(elements.activeGameName, activeGame?.name || "No game open"); -+- setText(elements.activeGameOwner, activeGame?.ownerDisplayName || "No owner"); -+- setText(elements.activeGamePurpose, activeGame?.purpose || "No purpose"); -+- setText(elements.activeGameStatus, activeGame?.status || "No Game"); -+- setText(elements.currentUserRole, currentMember?.role || "Viewer"); -+- setText(elements.gameStatus, progress.gameStatus); -+- setText(elements.gameProgress, progress.gameProgress); -+- setText(elements.publishingProgress, progress.publishingProgress); -+- setText(elements.currentFocus, progress.currentFocus); -+- setText(elements.recommendedNextTool, progress.recommendedNextTool); -+- if (elements.purposeInput && activeGame?.purpose) { -+- elements.purposeInput.value = activeGame.purpose; -+- } -+- if (elements.gameStatusInput && activeGame?.status) { -+- elements.gameStatusInput.value = activeGame.status; -+- } -+ if (elements.currentUserRoleInput) { -+ elements.currentUserRoleInput.value = currentMember?.role || "Viewer"; -+ } -+- if (elements.gameJourneyLink) { -+- if (activeGame) { -+- elements.gameJourneyLink.href = `toolbox/game-journey/index.html?game=${encodeURIComponent(activeGame.id)}`; -+- elements.gameJourneyLink.setAttribute("aria-disabled", "false"); -+- } else { -+- elements.gameJourneyLink.href = "toolbox/game-journey/index.html?game=none"; -+- elements.gameJourneyLink.setAttribute("aria-disabled", "true"); -+- } -+- } -+ -+- renderGameList(); -++ renderGameList(progress); -+ renderMembersTable(activeGame); -+ renderTableCounts(); -+ renderChecklist(progress); -+- renderProjectInformation(activeGame, currentMember, progress); -+- renderSourceIdea(activeGame); -++ renderGameTableStatus(); -+ refreshSaveControls(activeGame); -++ notifySelectedGameChanged(activeGame); -+ } -+ -+-elements.form?.addEventListener("submit", (event) => { -+- event.preventDefault(); -+- if (!ensureProjectRecordsSaveAllowed("create")) { -++function readGameRowFields(row) { -++ return { -++ name: row?.querySelector("[data-game-name-input]")?.value, -++ purpose: row?.querySelector("[data-game-purpose-input]")?.value, -++ status: row?.querySelector("[data-game-status-input]")?.value, -++ }; -++} -++ -++function validateAddedGameFields(row) { -++ const input = readGameRowFields(row); -++ const nameInput = row?.querySelector("[data-game-name-input]"); -++ input.name = String(input.name || "").trim(); -++ if (!input.name) { -++ if (nameInput) { -++ nameInput.setAttribute("aria-invalid", "true"); -++ nameInput.focus(); -++ } -++ setStatusLog("Enter a game name before saving."); -++ return null; -++ } -++ if (nameInput) { -++ nameInput.removeAttribute("aria-invalid"); -++ } -++ return input; -++} -++ -++function saveAddedGame(row) { -++ if (!ensureProjectRecordsSaveAllowedForSave()) { -++ return; -++ } -++ const input = validateAddedGameFields(row); -++ if (!input) { -+ return; -+ } -+- const activeGame = normalizeActiveGame(repository.getActiveGame()); -+ const game = repository.createGame({ -+- name: elements.nameInput?.value, -+- purpose: elements.purposeInput?.value, -+- status: elements.gameStatusInput?.value, -++ name: input.name, -++ purpose: input.purpose, -++ status: input.status, -+ }); -+ -+- if (reportRepositoryError(game, "Create Game") || !isRecord(game) || !String(game.name || "").trim()) { -++ if (reportRepositoryError(game, "Add game") || !isRecord(game) || !String(game.name || "").trim()) { -+ if (!isRepositoryErrorResult(game)) { -+- setStatusLog("Create Game could not be completed. Refresh the page or try again shortly."); -++ setStatusLog("Add game could not be completed. Refresh the page or try again shortly."); -+ } -+ renderWorkspace(); -+ return; -+ } -+ -+- if (elements.nameInput) { -+- elements.nameInput.value = ""; -+- } -+- -++ state.addingGame = false; -++ state.editingGameId = ""; -+ setStatusLog(`Created and opened ${game.name}.`); -+ renderWorkspace(); -+-}); -+- -+-elements.gameList?.addEventListener("click", (event) => { -+- const button = event.target.closest("[data-game-open]"); -++} -+ -+- if (!button) { -++function saveEditedGame(row, gameId) { -++ if (!ensureProjectRecordsSaveAllowedForSave()) { -++ return; -++ } -++ const input = readGameRowFields(row); -++ let game = repository.openGame(gameId); -++ if (reportRepositoryError(game, "Edit game") || !isRecord(game)) { -++ if (!isRepositoryErrorResult(game)) { -++ setStatusLog("Edit game could not be completed. Refresh the page or try again shortly."); -++ } -++ renderWorkspace(); -+ return; -+ } -+ -+- const game = repository.openGame(button.dataset.gameOpen); -++ if (input.purpose && input.purpose !== game.purpose) { -++ game = repository.updateGamePurpose(gameId, input.purpose); -++ if (reportRepositoryError(game, "Update game purpose") || !isRecord(game)) { -++ if (!isRepositoryErrorResult(game)) { -++ setStatusLog("Update game purpose could not be completed. Refresh the page or try again shortly."); -++ } -++ renderWorkspace(); -++ return; -++ } -++ } -++ -++ if (input.status && input.status !== game.status) { -++ game = repository.updateGameStatus(gameId, input.status); -++ if (reportRepositoryError(game, "Update game status") || !isRecord(game)) { -++ if (!isRepositoryErrorResult(game)) { -++ setStatusLog("Update game status could not be completed. Refresh the page or try again shortly."); -++ } -++ renderWorkspace(); -++ return; -++ } -++ } -++ -++ state.editingGameId = ""; -++ setStatusLog(`Saved ${game.name}.`); -++ renderWorkspace(); -++} -+ -+- if (game) { -+- setStatusLog(`Opened ${game.name}.`); -++elements.gameList?.addEventListener("click", (event) => { -++ const toggle = event.target.closest("[data-game-toggle]"); -++ if (toggle) { -++ const game = repository.openGame(toggle.dataset.gameToggle); -++ if (reportRepositoryError(game, "Select game")) { -++ renderWorkspace(); -++ return; -++ } -++ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle; -+ renderWorkspace(); -++ return; -+ } -+-}); -+ -+-elements.deleteOpenGame?.addEventListener("click", () => { -+- if (!ensureProjectRecordsSaveAllowed("delete")) { -++ const action = event.target.closest("[data-game-action]"); -++ -++ if (!action) { -+ return; -+ } -+- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Delete active game"); -+ -+- if (!activeGame) { -+- setStatusLog("No game is open for deletion."); -++ if (action.dataset.gameAction === "start-add-game") { -++ state.addingGame = true; -++ state.editingGameId = ""; -+ renderWorkspace(); -+ return; -+ } -+- if (isSourceLinkedGame(activeGame)) { -+- setStatusLog("Source-linked projects stay connected to Idea Board."); -++ -++ if (action.dataset.gameAction === "cancel-add-game") { -++ state.addingGame = false; -++ setStatusLog("Cancelled game add."); -+ renderWorkspace(); -+ return; -+ } -+ -+- repository.deleteGame(activeGame.id); -+- setStatusLog(`Deleted ${activeGame.name}.`); -+- renderWorkspace(); -+-}); -++ if (action.dataset.gameAction === "save-add-game") { -++ saveAddedGame(action.closest("[data-game-add-row='input']")); -++ return; -++ } -+ -+-elements.purposeInput?.addEventListener("change", () => { -+- if (!ensureProjectRecordsSaveAllowed("update")) { -++ if (action.dataset.gameAction === "edit-game") { -++ const game = repository.openGame(action.dataset.gameId); -++ if (reportRepositoryError(game, "Edit game") || !isRecord(game)) { -++ if (!isRepositoryErrorResult(game)) { -++ setStatusLog("Edit game could not be completed. Refresh the page or try again shortly."); -++ } -++ renderWorkspace(); -++ return; -++ } -++ state.addingGame = false; -++ state.editingGameId = game.id; -++ setStatusLog(`Editing ${game.name}.`); -++ renderWorkspace(); -+ return; -+ } -+- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Update game purpose"); -+- if (!activeGame) { -++ -++ if (action.dataset.gameAction === "cancel-edit-game") { -++ state.editingGameId = ""; -++ setStatusLog("Cancelled game edit."); -++ renderWorkspace(); -+ return; -+ } -+ -+- const game = repository.updateGamePurpose(activeGame.id, elements.purposeInput.value); -+- setStatusLog(`Updated ${game.name} purpose to ${game.purpose}.`); -+- renderWorkspace(); -++ if (action.dataset.gameAction === "save-edit-game") { -++ saveEditedGame(action.closest("[data-game-edit-row]"), action.dataset.gameId); -++ } -+ }); -+ -+-elements.gameStatusInput?.addEventListener("change", () => { -+- if (!ensureProjectRecordsSaveAllowed("update")) { -++elements.deleteOpenGame?.addEventListener("click", () => { -++ if (!ensureProjectRecordsSaveAllowed("delete")) { -+ return; -+ } -+- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Update game status"); -++ const activeGame = normalizeActiveGame(repository.getActiveGame(), "Delete active game"); -++ -+ if (!activeGame) { -++ setStatusLog("No game is open for deletion."); -++ renderWorkspace(); -++ return; -++ } -++ if (isSourceLinkedGame(activeGame)) { -++ setStatusLog("Source-linked projects stay connected to Idea Board."); -++ renderWorkspace(); -+ return; -+ } -+ -+- const game = repository.updateGameStatus(activeGame.id, elements.gameStatusInput.value); -+- setStatusLog(`Updated ${game.name} status to ${game.status}.`); -++ repository.deleteGame(activeGame.id); -++ setStatusLog(`Deleted ${activeGame.name}.`); -+ renderWorkspace(); -+ }); -+ -+@@ -540,8 +867,6 @@ elements.currentUserRoleInput?.addEventListener("change", () => { -+ renderWorkspace(); -+ }); -+ -+-populateSelect(elements.purposeInput, GAME_HUB_GAME_PURPOSES); -+-populateSelect(elements.gameStatusInput, GAME_HUB_GAME_STATUSES); -+ populateSelect(elements.currentUserRoleInput, GAME_HUB_MEMBER_ROLES); -+ const requestedGameId = new URL(window.location.href).searchParams.get("game"); -+ if (requestedGameId) { -+diff --git a/toolbox/game-hub/index.html b/toolbox/game-hub/index.html -+index ef7b50004..0b34801e9 100644 -+--- a/toolbox/game-hub/index.html -++++ b/toolbox/game-hub/index.html -+@@ -26,137 +26,17 @@ -+
-+
Database Health - Postgres OnlySource Idea
ContextDetails
Readiness Output
OutputStatus
GamePurposeStatusActions
-+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+-
-+-
-+- -+- -+- -+- -+- -+- -+-
-+- Open Games -+-
-+-
-+-
-+-
-++
-++ -+
-+ -+
-+-

Project Information

-+-

Review the open project and its source idea.

-+-
Project Information ready.
-+-
-+-
-+-
-+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+-
Project Information
ProjectStatusPurposeOwnerRoleNext Tool
Demo GameUnder ConstructionGameNo ownerOwnerGame Configuration
-+-
-+- -+-
-+-
-+-
-+-
-+-
-+-
Source Idea
-+-

No source idea yet

-+-
-+-
-+- -+- -+- -+- -+- -+- -+-
IdeaNo source idea yet
PitchCreate a project from Idea Board to see source details.
Notes
  • No source notes.
-+-
-+-
-+-
-+-
-+-
-+-
-+-
Game Foundation
-+-

Game Progress

-+-
-+-
-+-

Game Status

Under Construction

-+-

Game Progress

Demo Game identity ready

-+-

Launch Progress

Publish blocked until configuration and required assets are ready

-+-
-+-
-+-

Current Focus

Complete Game Configuration

-+-

Recommended Next Tool

Game Configuration

-+-

Checklist

  • Game identity: Complete
-+-
-+-
Game Hub ready.
-+-
-+-
-+-
-+-
-+- Readiness Output -+-
-+-
-+- -+- -+- -+- -+- -+- -+- -+- -+- -+- -+-
Readiness output
PathStatusNext Tool
PlanUnder ConstructionGame Configuration
ConfigurePlannedBuild Game
ReleasePlannedPublish
-+-
-+-
-+-
-+-
-++

Games

-++
Game table ready.
-++
-++
Game Hub ready.
-+
-+ -+
-+@@ -49,16 +59,6 @@ -+

Scan, compare, and update early ideas.

-+
-+ Idea Board table with expandable notes rows -+-
-+- Show -+-
-+-
-+- -+- -+-
-+-
-+-
-+-
-+
-+
-+ -+@@ -67,7 +67,6 @@ -+ -+ -+ -+- -+ -+ -+ -+@@ -103,7 +102,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/messages/messages.js b/toolbox/messages/messages.js -+index 6eb31f6f1..cfa68bae4 100644 -+--- a/toolbox/messages/messages.js -++++ b/toolbox/messages/messages.js -+@@ -1,7 +1,7 @@ -+ import { -+ readSavedTextToSpeechProfiles, -+ textToSpeechProfilesToMessageOptions, -+-} from "../text-to-speech/tts-profile-store.js"; -++} from "../../assets/js/shared/tts-profile-store.js"; -+ import { -+ createEmotionProfile, -+ createMessage, -+diff --git a/toolbox/objects/index.html b/toolbox/objects/index.html -+index 63056d723..117409b70 100644 -+--- a/toolbox/objects/index.html -++++ b/toolbox/objects/index.html -+@@ -157,7 +157,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/objects/objects-api-client.js b/toolbox/objects/objects-api-client.js -+deleted file mode 100644 -+index 658116553..000000000 -+--- a/toolbox/objects/objects-api-client.js -++++ /dev/null -+@@ -1,39 +0,0 @@ -+-import { -+- createServerRepositoryClient, -+- readServerToolConstants, -+- requireServerConstant, -+-} from "../../src/api/server-api-client.js"; -+- -+-const constants = readServerToolConstants("objects"); -+- -+-function freezeTemplate(template = {}) { -+- return Object.freeze({ -+- ...template, -+- capabilities: Object.freeze(Array.isArray(template.capabilities) ? [...template.capabilities] : []), -+- }); -+-} -+- -+-function freezeStarterObject(object = {}) { -+- return Object.freeze({ -+- ...object, -+- render: Object.freeze({ ...(object.render || {}) }), -+- }); -+-} -+- -+-export const CAPABILITY_LABELS = Object.freeze( -+- { ...requireServerConstant(constants, "CAPABILITY_LABELS", "objects") }, -+-); -+- -+-export const OBJECT_TYPE_TEMPLATES = Object.freeze( -+- requireServerConstant(constants, "OBJECT_TYPE_TEMPLATES", "objects").map(freezeTemplate), -+-); -+- -+-export const OBJECTS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "OBJECTS_TOOL_TABLES", "objects")); -+- -+-export const STARTER_OBJECTS = Object.freeze( -+- requireServerConstant(constants, "STARTER_OBJECTS", "objects").map(freezeStarterObject), -+-); -+- -+-export function createObjectsToolApiRepository(options = {}) { -+- return createServerRepositoryClient("objects", options); -+-} -+diff --git a/toolbox/tags/index.html b/toolbox/tags/index.html -+index d8b2f89fb..974e8b672 100644 -+--- a/toolbox/tags/index.html -++++ b/toolbox/tags/index.html -+@@ -116,7 +116,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -+diff --git a/toolbox/tags/tags-api-client.js b/toolbox/tags/tags-api-client.js -+deleted file mode 100644 -+index 2046bf88e..000000000 -+--- a/toolbox/tags/tags-api-client.js -++++ /dev/null -+@@ -1,13 +0,0 @@ -+-import { -+- createServerRepositoryClient, -+- readServerToolConstants, -+- requireServerConstant, -+-} from "../../src/api/server-api-client.js"; -+- -+-const constants = readServerToolConstants("tags"); -+- -+-export const TAGS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "TAGS_TOOL_TABLES", "tags")); -+- -+-export function createTagsToolApiRepository(options = {}) { -+- return createServerRepositoryClient("tags", options); -+-} -+diff --git a/toolbox/text-to-speech/index.html b/toolbox/text-to-speech/index.html -+index 35f232048..66cfb2330 100644 -+--- a/toolbox/text-to-speech/index.html -++++ b/toolbox/text-to-speech/index.html -+@@ -124,7 +124,7 @@ -+
-+ -+ -+- -++ -+ -+ -+ -diff --git a/package.json b/package.json -index d13364e3f..72c3c9c32 100644 ---- a/package.json -+++ b/package.json -@@ -28,6 +28,7 @@ - "validate:database-drift": "node ./scripts/validate-database-drift.mjs", - "validate:runtime-connections": "node --use-system-ca ./scripts/validate-runtime-connections.mjs", - "validate:browser-env-agnostic": "node ./scripts/validate-browser-env-agnostic.mjs", -+ "validate:canonical-structure": "node ./scripts/validate-canonical-repository-structure.mjs", - "apply:database-ddl": "node ./scripts/apply-database-ddl.mjs", - "apply:database-dml": "node ./scripts/apply-database-dml.mjs", - "seed:database-dev": "node --use-system-ca ./scripts/apply-database-seed.mjs", -diff --git a/scripts/run-targeted-test-lanes.mjs b/scripts/run-targeted-test-lanes.mjs -index 3fef73319..42ecb1b71 100644 ---- a/scripts/run-targeted-test-lanes.mjs -+++ b/scripts/run-targeted-test-lanes.mjs -@@ -363,8 +363,8 @@ const laneDefinitions = Object.freeze({ - nodeCommand( - "scripts/run-node-test-files.mjs", - "tests/core/EngineCoreBoundaryBaseline.test.mjs", -- "tests/core/FrameClock.test.mjs", -- "tests/core/FixedTicker.test.mjs", -+ "tests/engine/core/FrameClock.test.mjs", -+ "tests/engine/core/FixedTicker.test.mjs", - "tests/assets/AssetLoaderSystem.test.mjs", - "tests/audio/AudioService.test.mjs", - "tests/input/InputMap.test.mjs", -@@ -380,8 +380,8 @@ const laneDefinitions = Object.freeze({ - "tests/assets/AssetLoaderSystem.test.mjs", - "tests/audio/AudioService.test.mjs", - "tests/core/EngineCoreBoundaryBaseline.test.mjs", -- "tests/core/FixedTicker.test.mjs", -- "tests/core/FrameClock.test.mjs", -+ "tests/engine/core/FixedTicker.test.mjs", -+ "tests/engine/core/FrameClock.test.mjs", - "tests/input/GamepadHapticsService.test.mjs", - "tests/input/GamepadInputAdapter.test.mjs", - "tests/input/InputMap.test.mjs", -diff --git a/scripts/validate-browser-env-agnostic.mjs b/scripts/validate-browser-env-agnostic.mjs -index ac69a4324..53a165d48 100644 ---- a/scripts/validate-browser-env-agnostic.mjs -+++ b/scripts/validate-browser-env-agnostic.mjs -@@ -60,15 +60,15 @@ const accountAuthPages = Object.freeze([ - }), - ]); - const productApiClientFiles = Object.freeze([ -- "toolbox/assets/assets-api-client.js", -- "toolbox/colors/palette-api-client.js", -- "toolbox/controls/controls-api-client.js", -- "toolbox/game-configuration/game-configuration-api-client.js", -- "toolbox/game-design/game-design-api-client.js", -- "toolbox/game-journey/game-journey-api-client.js", -+ "assets/js/shared/assets-api-client.js", -+ "assets/toolbox/colors/js/index.js", -+ "assets/js/shared/controls-api-client.js", -+ "assets/toolbox/game-configuration/js/index.js", -+ "assets/toolbox/game-design/js/index.js", -+ "assets/js/shared/game-journey-api-client.js", - "toolbox/game-hub/game-hub-api-client.js", -- "toolbox/objects/objects-api-client.js", -- "toolbox/tags/tags-api-client.js", -+ "assets/toolbox/objects/js/index.js", -+ "assets/toolbox/tags/js/index.js", - ]); - const userFacingUiRoots = Object.freeze(["account", "toolbox"]); - const nonUiCompatibilityFiles = new Set([ -diff --git a/scripts/validate-canonical-repository-structure.mjs b/scripts/validate-canonical-repository-structure.mjs -new file mode 100644 -index 000000000..e9296d04c ---- /dev/null -+++ b/scripts/validate-canonical-repository-structure.mjs -@@ -0,0 +1,393 @@ -+import { spawnSync } from "node:child_process"; -+import fs from "node:fs/promises"; -+import path from "node:path"; -+import { fileURLToPath } from "node:url"; -+ -+const repoRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url))); -+ -+export const APPROVED_LEGACY_JS_PATHS = Object.freeze(new Set([ -+ "toolbox/game-hub/game-hub-api-client.js", -+ "toolbox/game-hub/game-hub.js", -+ "toolbox/messages/message-tts-service-registry.js", -+ "toolbox/messages/messages-api-client.js", -+ "toolbox/messages/messages.js", -+ "toolbox/tool-registry-api-client.js", -+ "toolbox/toolRegistry.js", -+ "toolbox/tools-page-accordions.js", -+ "src/engine/paletteList.js", -+])); -+ -+export const APPROVED_LEGACY_CSS_PATHS = Object.freeze(new Set([ -+ "src/engine/ui/baseLayout.css", -+ "src/engine/ui/hubCommon.css", -+ "src/engine/ui/spriteEditor.css", -+])); -+ -+export const APPROVED_LEGACY_TEST_SEGMENTS = Object.freeze(new Set([ -+ "ai", -+ "assets", -+ "audio", -+ "combat", -+ "config", -+ "core", -+ "dev-runtime", -+ "engine", -+ "entity", -+ "events", -+ "final", -+ "fixtures", -+ "fx", -+ "games", -+ "helpers", -+ "index.html", -+ "input", -+ "persistence", -+ "playwright", -+ "playwright_installation.txt", -+ "production", -+ "README.md", -+ "render", -+ "replay", -+ "run-tests.mjs", -+ "runtime", -+ "samples", -+ "scenes", -+ "schemas", -+ "shared", -+ "testRunner.html", -+ "testRunner.js", -+ "tools", -+ "validation", -+ "vector", -+ "world", -+])); -+ -+const canonicalTestPrefixes = Object.freeze([ -+ "tests/toolbox/", -+ "tests/engine/", -+ "tests/api/", -+ "tests/server/", -+ "tests/js/shared/", -+ "tests/regression/", -+]); -+ -+function normalizeRepoPath(filePath) { -+ return String(filePath || "") -+ .replace(/\\/g, "/") -+ .replace(/^\.\//, "") -+ .replace(/^\/+/, ""); -+} -+ -+function fileExtension(filePath) { -+ return path.posix.extname(normalizeRepoPath(filePath)).toLowerCase(); -+} -+ -+function isJavaScript(filePath) { -+ return [".js", ".mjs"].includes(fileExtension(filePath)); -+} -+ -+function isCss(filePath) { -+ return fileExtension(filePath) === ".css"; -+} -+ -+function relevantPath(filePath) { -+ return filePath.startsWith("assets/") || -+ filePath.startsWith("toolbox/") || -+ filePath.startsWith("src/engine/") || -+ filePath.startsWith("tests/"); -+} -+ -+function record(severity, area, file, message, expected) { -+ return Object.freeze({ area, expected, file, message, severity }); -+} -+ -+function isCanonicalAssetJs(filePath) { -+ return /^assets\/toolbox\/[^/]+\/js\/index\.js$/.test(filePath) || -+ /^assets\/toolbox\/[^/]+\/js\/[^/]+-worker\.js$/.test(filePath) || -+ filePath.startsWith("assets/js/shared/") || -+ filePath.startsWith("assets/theme-v2/js/"); -+} -+ -+function isCanonicalAssetCss(filePath) { -+ return /^assets\/toolbox\/[^/]+\/css\/index\.css$/.test(filePath) || -+ filePath.startsWith("assets/theme-v2/css/") || -+ filePath.startsWith("assets/theme-v2/fonts/"); -+} -+ -+function auditJavaScript(filePath) { -+ if (!isJavaScript(filePath)) { -+ return null; -+ } -+ if (filePath.startsWith("assets/") && !isCanonicalAssetJs(filePath)) { -+ return record( -+ "FAIL", -+ "JS", -+ filePath, -+ "JavaScript under assets must use assets/toolbox/{tool}/js/index.js, assets/toolbox/{tool}/js/{worker-name}.js for tool-local workers, assets/js/shared/, or assets/theme-v2/js/.", -+ "assets/toolbox/{tool}/js/index.js, assets/toolbox/{tool}/js/{worker-name}.js, or assets/js/shared/", -+ ); -+ } -+ if (filePath.startsWith("toolbox/")) { -+ if (APPROVED_LEGACY_JS_PATHS.has(filePath)) { -+ return record( -+ "LEGACY", -+ "JS", -+ filePath, -+ "Approved legacy toolbox JavaScript sidecar awaiting canonical migration.", -+ "assets/toolbox/{tool}/js/index.js, assets/toolbox/{tool}/js/{worker-name}.js, or assets/js/shared/", -+ ); -+ } -+ return record( -+ "FAIL", -+ "JS", -+ filePath, -+ "New or unapproved toolbox JavaScript sidecar is outside canonical structure.", -+ "assets/toolbox/{tool}/js/index.js or assets/js/shared/", -+ ); -+ } -+ if (/^src\/engine\/[^/]+\.m?js$/.test(filePath)) { -+ if (APPROVED_LEGACY_JS_PATHS.has(filePath)) { -+ return record( -+ "LEGACY", -+ "JS", -+ filePath, -+ "Approved legacy root-level engine JavaScript awaiting feature-folder migration.", -+ "src/engine/{feature-name}/", -+ ); -+ } -+ return record( -+ "FAIL", -+ "JS", -+ filePath, -+ "Root-level engine JavaScript is outside src/engine/{feature-name}/.", -+ "src/engine/{feature-name}/", -+ ); -+ } -+ return null; -+} -+ -+function auditCss(filePath) { -+ if (!isCss(filePath)) { -+ return null; -+ } -+ if (filePath.startsWith("assets/") && !isCanonicalAssetCss(filePath)) { -+ return record( -+ "FAIL", -+ "CSS", -+ filePath, -+ "CSS under assets must use assets/toolbox/{tool}/css/index.css or assets/theme-v2/css/.", -+ "assets/toolbox/{tool}/css/index.css or assets/theme-v2/css/", -+ ); -+ } -+ if (filePath.startsWith("toolbox/")) { -+ return record( -+ "FAIL", -+ "CSS", -+ filePath, -+ "Toolbox CSS sidecar is outside canonical tool asset structure.", -+ "assets/toolbox/{tool}/css/index.css", -+ ); -+ } -+ if (filePath.startsWith("src/engine/") && APPROVED_LEGACY_CSS_PATHS.has(filePath)) { -+ return record( -+ "LEGACY", -+ "CSS", -+ filePath, -+ "Approved legacy engine UI CSS awaiting asset/theme placement decision.", -+ "assets/theme-v2/css/ or approved engine UI style policy", -+ ); -+ } -+ if (filePath.startsWith("src/engine/")) { -+ return record( -+ "FAIL", -+ "CSS", -+ filePath, -+ "Engine CSS is outside canonical asset/theme roots.", -+ "assets/theme-v2/css/ or approved engine UI style policy", -+ ); -+ } -+ return null; -+} -+ -+function auditTestPath(filePath) { -+ if (!filePath.startsWith("tests/")) { -+ return null; -+ } -+ if (canonicalTestPrefixes.some((prefix) => filePath.startsWith(prefix))) { -+ return null; -+ } -+ const segment = filePath.slice("tests/".length).split("/")[0] || ""; -+ if (segment === "results") { -+ return record( -+ "FAIL", -+ "Tests", -+ filePath, -+ "Generated test result artifacts must not be tracked under active tests/results/.", -+ "ignored tmp/test-results/ or docs_build/dev/reports/", -+ ); -+ } -+ if (APPROVED_LEGACY_TEST_SEGMENTS.has(segment)) { -+ return record( -+ "LEGACY", -+ "Tests", -+ filePath, -+ "Approved legacy test location awaiting canonical test structure migration.", -+ "tests/toolbox/, tests/engine/, tests/api/, tests/server/, tests/js/shared/, or tests/regression/", -+ ); -+ } -+ return record( -+ "FAIL", -+ "Tests", -+ filePath, -+ "New or unapproved test location is outside canonical test roots.", -+ "tests/toolbox/, tests/engine/, tests/api/, tests/server/, tests/js/shared/, or tests/regression/", -+ ); -+} -+ -+export function auditCanonicalRepositoryStructure(files) { -+ const findings = []; -+ const legacy = []; -+ [...new Set(files.map(normalizeRepoPath).filter(Boolean))] -+ .filter(relevantPath) -+ .sort((left, right) => left.localeCompare(right)) -+ .forEach((filePath) => { -+ const records = [ -+ auditJavaScript(filePath), -+ auditCss(filePath), -+ auditTestPath(filePath), -+ ].filter(Boolean); -+ records.forEach((entry) => { -+ if (entry.severity === "FAIL") { -+ findings.push(entry); -+ } else { -+ legacy.push(entry); -+ } -+ }); -+ }); -+ -+ return Object.freeze({ -+ findings, -+ legacy, -+ status: findings.length === 0 ? "PASS" : "FAIL", -+ }); -+} -+ -+async function walkFiles(root) { -+ const results = []; -+ async function visit(absolutePath) { -+ let entries = []; -+ try { -+ entries = await fs.readdir(absolutePath, { withFileTypes: true }); -+ } catch { -+ return; -+ } -+ for (const entry of entries) { -+ if ([".git", "node_modules", "tmp"].includes(entry.name)) { -+ continue; -+ } -+ const child = path.join(absolutePath, entry.name); -+ if (entry.isDirectory()) { -+ await visit(child); -+ } else if (entry.isFile()) { -+ results.push(path.relative(root, child).replace(/\\/g, "/")); -+ } -+ } -+ } -+ await visit(root); -+ return results; -+} -+ -+export async function collectRepositoryFiles(root = repoRoot) { -+ const result = spawnSync("git", ["ls-files"], { -+ cwd: root, -+ encoding: "utf8", -+ shell: false, -+ }); -+ if (result.status === 0 && result.stdout.trim()) { -+ return result.stdout.split(/\r?\n/).map(normalizeRepoPath).filter(Boolean); -+ } -+ return walkFiles(root); -+} -+ -+function markdownRows(records) { -+ if (!records.length) { -+ return ["| none | none | PASS | none |"]; -+ } -+ return records.map((entry) => -+ `| ${entry.area} | \`${entry.file}\` | ${entry.severity} | ${entry.message} Expected: \`${entry.expected}\`. |` -+ ); -+} -+ -+export function formatCanonicalStructureReport(result) { -+ return [ -+ "# Canonical Repository Structure Guardrail", -+ "", -+ `Status: ${result.status}`, -+ "", -+ "## Blocking Violations", -+ "", -+ "| Area | File | Severity | Details |", -+ "| --- | --- | --- | --- |", -+ ...markdownRows(result.findings), -+ "", -+ "## Approved Legacy Exceptions", -+ "", -+ "| Area | File | Severity | Details |", -+ "| --- | --- | --- | --- |", -+ ...markdownRows(result.legacy), -+ "", -+ "## Result", -+ "", -+ result.status === "PASS" -+ ? "- PASS - No unapproved JS, CSS, or test structure violations were found." -+ : "- FAIL - Unapproved JS, CSS, or test structure violations were found.", -+ "", -+ ].join("\n"); -+} -+ -+function parseArgs(argv) { -+ const options = { reportPath: "", root: repoRoot }; -+ for (let index = 0; index < argv.length; index += 1) { -+ const argument = argv[index]; -+ if (argument === "--report") { -+ options.reportPath = argv[index + 1] || ""; -+ index += 1; -+ } else if (argument.startsWith("--report=")) { -+ options.reportPath = argument.slice("--report=".length); -+ } else if (argument === "--root") { -+ options.root = path.resolve(argv[index + 1] || repoRoot); -+ index += 1; -+ } else if (argument.startsWith("--root=")) { -+ options.root = path.resolve(argument.slice("--root=".length)); -+ } else { -+ throw new Error(`Unknown argument: ${argument}`); -+ } -+ } -+ return options; -+} -+ -+async function main() { -+ const options = parseArgs(process.argv.slice(2)); -+ const files = await collectRepositoryFiles(options.root); -+ const result = auditCanonicalRepositoryStructure(files); -+ const report = formatCanonicalStructureReport(result); -+ if (options.reportPath) { -+ const absoluteReportPath = path.resolve(options.root, options.reportPath); -+ await fs.mkdir(path.dirname(absoluteReportPath), { recursive: true }); -+ await fs.writeFile(absoluteReportPath, report, "utf8"); -+ } -+ console.log(`Canonical repository structure guardrail: ${result.status}`); -+ console.log(`Blocking violations: ${result.findings.length}`); -+ console.log(`Approved legacy exceptions: ${result.legacy.length}`); -+ if (result.findings.length > 0) { -+ console.error(report); -+ process.exitCode = 1; -+ } -+} -+ -+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { -+ main().catch((error) => { -+ console.error(error instanceof Error ? error.stack || error.message : error); -+ process.exitCode = 1; -+ }); -+} -diff --git a/scripts/validate-storage-config.mjs b/scripts/validate-storage-config.mjs -index 032195b24..e84193ba9 100644 ---- a/scripts/validate-storage-config.mjs -+++ b/scripts/validate-storage-config.mjs -@@ -3,7 +3,11 @@ import fs from "node:fs"; - import path from "node:path"; - import process from "node:process"; - import { createConfiguredProjectAssetStorage } from "../src/dev-runtime/storage/r2-project-asset-storage.mjs"; --import { STORAGE_ENV_KEYS, loadStorageConfig } from "../src/dev-runtime/storage/storage-config.mjs"; -+import { -+ STORAGE_ENV_KEYS, -+ STORAGE_PROJECTS_ALLOWED_PREFIXES, -+ loadStorageConfig, -+} from "../src/dev-runtime/storage/storage-config.mjs"; - - const ENV_FILE = ".env"; - -@@ -64,6 +68,7 @@ console.log(`PASS - Storage env keys present=${presentKeys.length}/${STORAGE_ENV - - if (!config.configured) { - console.log(`SKIP - Storage DEV values are not fully configured in .env (${config.missingKeys?.join(", ") || config.validationError}).`); -+ console.log(`SKIP - Approved project storage prefixes: ${STORAGE_PROJECTS_ALLOWED_PREFIXES.join(", ")}.`); - process.exit(0); - } - -diff --git a/src/dev-runtime/persistence/mock-db-store.js b/src/dev-runtime/persistence/mock-db-store.js -index c98c915b5..e96fb833f 100644 ---- a/src/dev-runtime/persistence/mock-db-store.js -+++ b/src/dev-runtime/persistence/mock-db-store.js -@@ -125,7 +125,7 @@ const MOCK_DB_TABLE_SCHEMAS = Object.freeze({ - input_custom_action_records: Object.freeze(["key", "id", "gameId", "label", "recordOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]), - game_journey_note_types: Object.freeze(["key", "typeSlug", "name", "seeded", "userExtensible", "createdAt", "updatedAt", "createdBy", "updatedBy"]), - game_journey_completion_metrics: Object.freeze(["key", "bucketKey", "bucketOrder", "bucketName", "friendlyDescription", "requiredForMvp", "canSkip", "plannedCount", "completedCount", "active", "status", "createdAt", "updatedAt", "createdBy", "updatedBy"]), -- game_journey_notes: Object.freeze(["key", "slug", "gameKey", "ownerKey", "name", "typeKey", "createdAt", "updatedAt", "createdBy", "updatedBy"]), -+ game_journey_notes: Object.freeze(["key", "slug", "gameKey", "ownerKey", "name", "typeKey", "bucketOrder", "createdAt", "updatedAt", "createdBy", "updatedBy"]), - game_journey_templates: Object.freeze(["key", "templateSlug", "originalMeaning", "systemGuidance", "linkedToolContexts", "version", "isActive", "createdAt", "updatedAt", "createdBy", "updatedBy"]), - game_journey_items: Object.freeze(["key", "gameKey", "noteKey", "status", "title", "userDetails", "templateKey", "linkedRecordType", "linkedRecordId", "indent", "order", "createdAt", "updatedAt", "createdBy", "updatedBy"]), - game_journey_activity: Object.freeze(["key", "gameKey", "noteKey", "message", "createdAt", "updatedAt", "createdBy", "updatedBy"]), -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 905b1dba6..db24253af 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 -@@ -101,6 +101,23 @@ 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"; -+const JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE = "journey-bootstrap-bucket"; -+ -+export const GAME_JOURNEY_BOOTSTRAP_BUCKETS = Object.freeze([ -+ "Idea", -+ "Design", -+ "Graphics", -+ "Audio", -+ "Objects", -+ "Worlds", -+ "Interface", -+ "Controls", -+ "Rules", -+ "Progression", -+ "Play Test", -+ "Publish", -+ "Share", -+]); - - export const GAME_JOURNEY_STATUSES = [ - { -@@ -248,28 +265,34 @@ export const GAME_JOURNEY_TOOL_OWNERSHIP_AREAS = Object.freeze([ - - export const GAME_JOURNEY_RECOMMENDED_TARGETS = Object.freeze([ - Object.freeze({ -- key: "heroes", -- label: "Heroes", -+ key: "hero", -+ label: "Hero", - sectionName: "Objects", - suggestedCount: 1, - }), - Object.freeze({ -- key: "enemies", -- label: "Enemies", -+ key: "enemy", -+ label: "Enemy", - sectionName: "Objects", -- suggestedCount: 3, -+ suggestedCount: 4, - }), - Object.freeze({ -- key: "levels", -- label: "Levels", -- sectionName: "Worlds", -- suggestedCount: 5, -+ key: "boss", -+ label: "Boss", -+ sectionName: "Objects", -+ suggestedCount: 1, - }), - Object.freeze({ -- key: "audio", -- label: "Audio", -+ key: "background", -+ label: "Background", -+ sectionName: "Graphics", -+ suggestedCount: 3, -+ }), -+ Object.freeze({ -+ key: "music", -+ label: "Music", - sectionName: "Audio", -- suggestedCount: 6, -+ suggestedCount: 5, - }), - ]); - -@@ -708,6 +731,106 @@ export function createGameJourneyMockRepository(options = {}) { - return idea ? `Source Idea: ${idea}` : "Source Idea"; - } - -+ function noteTypeKeyForBootstrapBucket(bucketName) { -+ const slug = slugSegment(bucketName, "task"); -+ const matchingType = tables.game_journey_note_types.find((type) => type.typeSlug === slug); -+ return matchingType?.key || GAME_JOURNEY_KEYS.noteTypes.task; -+ } -+ -+ function ensureJourneyBootstrapBuckets(activeGame) { -+ if (!activeGame) { -+ return { -+ buckets: [], -+ createdItems: 0, -+ createdNotes: 0, -+ }; -+ } -+ -+ const ownerKey = safeCurrentUserKey(); -+ const timestampValue = new Date().toISOString(); -+ let createdNotes = 0; -+ let createdItems = 0; -+ const bucketSummaries = []; -+ GAME_JOURNEY_BOOTSTRAP_BUCKETS.forEach((bucketName, index) => { -+ const bucketOrder = index + 1; -+ const bucketSlug = slugSegment(bucketName, "bucket"); -+ const noteSlug = `journey-bucket-${slugSegment(activeGame.id || activeGame.key)}-${String(bucketOrder).padStart(2, "0")}-${bucketSlug}`; -+ 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: bucketName, -+ typeKey: noteTypeKeyForBootstrapBucket(bucketName), -+ bucketOrder, -+ createdAt: timestampValue, -+ updatedAt: timestampValue, -+ createdBy: ownerKey, -+ updatedBy: ownerKey, -+ }; -+ nextNoteNumber += 1; -+ tables.game_journey_notes.push(note); -+ createdNotes += 1; -+ } -+ -+ const linkedRecordId = `${slugSegment(activeGame.id || activeGame.key)}:${String(bucketOrder).padStart(2, "0")}:${bucketSlug}`; -+ let item = getItemsForNote(note.key).find( -+ (candidate) => -+ candidate.linkedRecordType === JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE && -+ candidate.linkedRecordId === linkedRecordId, -+ ); -+ -+ if (!item) { -+ item = { -+ key: makeUlid(nextItemNumber), -+ gameKey: activeGame.key, -+ noteKey: note.key, -+ status: "not-started", -+ title: `${bucketName} progress placeholder`, -+ userDetails: "", -+ createdBy: ownerKey, -+ updatedBy: ownerKey, -+ templateKey: "", -+ linkedRecordType: JOURNEY_BOOTSTRAP_LINKED_RECORD_TYPE, -+ linkedRecordId, -+ indent: 0, -+ order: 1, -+ createdAt: timestampValue, -+ updatedAt: timestampValue, -+ }; -+ nextItemNumber += 1; -+ tables.game_journey_items.push(item); -+ createdItems += 1; -+ } -+ -+ bucketSummaries.push({ -+ bucketName, -+ itemKey: item.key, -+ noteKey: note.key, -+ order: bucketOrder, -+ }); -+ }); -+ -+ if (createdNotes || createdItems) { -+ const firstBucket = bucketSummaries[0]; -+ selectedNoteKey = firstBucket?.noteKey || selectedNoteKey; -+ selectedItemKey = firstBucket?.itemKey || selectedItemKey; -+ addActivity(activeGame.key, firstBucket?.noteKey || "", `Created ${GAME_JOURNEY_BOOTSTRAP_BUCKETS.length} Game Journey starter buckets.`, ownerKey); -+ persistTables(); -+ } -+ -+ return { -+ buckets: bucketSummaries, -+ createdItems, -+ createdNotes, -+ }; -+ } -+ - function ensureSourceIdeaJourneyItems(activeGame) { - const sourceIdea = activeGame?.sourceIdea && typeof activeGame.sourceIdea === "object" - ? activeGame.sourceIdea -@@ -889,16 +1012,17 @@ export function createGameJourneyMockRepository(options = {}) { - } - } - -- function findRecommendedTargetItem(targetKey) { -+ function findRecommendedTargetItem(targetKey, gameKey = requireActiveGame()?.key) { - return tables.game_journey_items.find((item) => -- item.gameKey === GAME_JOURNEY_KEYS.game && -+ item.gameKey === gameKey && - item.linkedRecordType === RECOMMENDED_TARGET_LINKED_RECORD_TYPE && - item.linkedRecordId === targetKey, - ); - } - - function hydrateRecommendedTarget(target) { -- const item = findRecommendedTargetItem(target.key); -+ const activeGame = requireActiveGame(); -+ const item = activeGame ? findRecommendedTargetItem(target.key, activeGame.key) : null; - return { - ...clone(target), - suggestedCount: readTargetCount(item, target.suggestedCount), -@@ -913,6 +1037,16 @@ export function createGameJourneyMockRepository(options = {}) { - return GAME_JOURNEY_RECOMMENDED_TARGETS.map(hydrateRecommendedTarget); - } - -+ function findRecommendedTargetNoteKey(target, activeGame) { -+ const sectionNote = tables.game_journey_notes.find( -+ (note) => note.gameKey === activeGame.key && note.name === target.sectionName, -+ ); -+ if (sectionNote) { -+ return sectionNote.key; -+ } -+ return activeGame.key === GAME_JOURNEY_KEYS.game ? RECOMMENDED_TARGET_NOTE_KEY : ""; -+ } -+ - function updateRecommendedTarget(targetKey, suggestedCount) { - const activeGame = requireActiveGame(); - const target = GAME_JOURNEY_RECOMMENDED_TARGETS.find((item) => item.key === targetKey); -@@ -920,15 +1054,23 @@ export function createGameJourneyMockRepository(options = {}) { - return null; - } - -+ const noteKey = findRecommendedTargetNoteKey(target, activeGame); -+ if (!noteKey) { -+ return { -+ error: true, -+ message: `Game Journey ${target.sectionName} bucket is not available for ${activeGame.name}.`, -+ }; -+ } -+ - const normalizedCount = normalizeTargetCount(suggestedCount); - const timestampValue = new Date().toISOString(); - const userKey = safeCurrentUserKey(); -- let item = findRecommendedTargetItem(target.key); -+ let item = findRecommendedTargetItem(target.key, activeGame.key); - if (!item) { - item = { - key: makeUlid(nextItemNumber), - gameKey: activeGame.key, -- noteKey: RECOMMENDED_TARGET_NOTE_KEY, -+ noteKey, - status: "not-started", - title: `Recommended target: ${target.label}`, - userDetails: "", -@@ -947,9 +1089,10 @@ export function createGameJourneyMockRepository(options = {}) { - } - - item.userDetails = JSON.stringify({ suggestedCount: normalizedCount }); -+ item.noteKey = noteKey; - item.updatedAt = timestampValue; - item.updatedBy = userKey; -- addActivity(activeGame.key, RECOMMENDED_TARGET_NOTE_KEY, `Updated ${target.label} recommended target to ${normalizedCount}`, userKey); -+ addActivity(activeGame.key, noteKey, `Updated ${target.label} recommended target to ${normalizedCount}`, userKey); - persistTables(); - return hydrateRecommendedTarget(target); - } -@@ -1039,7 +1182,11 @@ export function createGameJourneyMockRepository(options = {}) { - .filter(currentUserCanSeeNote) - .filter((note) => noteMatchesFilter(note, filterId)) - .map((note) => hydrateNote(note, filterId)) -- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)); -+ .sort((left, right) => { -+ const leftOrder = Number.isFinite(Number(left.bucketOrder)) ? Number(left.bucketOrder) : Number.POSITIVE_INFINITY; -+ const rightOrder = Number.isFinite(Number(right.bucketOrder)) ? Number(right.bucketOrder) : Number.POSITIVE_INFINITY; -+ return leftOrder - rightOrder || right.updatedAt.localeCompare(left.updatedAt); -+ }); - } - - function addNote({ name, typeKey } = {}) { -@@ -1500,11 +1647,29 @@ export function createGameJourneyMockRepository(options = {}) { - gameId === GAME_JOURNEY_KEYS.game ? GAME_JOURNEY_ROUTE_GAME_ALIAS : gameId; - const openedGame = gameWorkspaceRepository.openGame(workspaceGameId); - if (openedGame) { -- ensureSourceIdeaJourneyItems(getActiveGame()); -+ bootstrapGameJourneyForGame(getActiveGame()); - } - return openedGame; - } - -+ function bootstrapGameJourneyForGame(game = getActiveGame()) { -+ const activeGame = game?.key ? game : game ? { ...game, key: journeyGameKey(game) } : getActiveGame(); -+ if (!activeGame) { -+ return { -+ buckets: [], -+ createdItems: 0, -+ createdNotes: 0, -+ sourceIdeaItems: [], -+ }; -+ } -+ const bucketResult = ensureJourneyBootstrapBuckets(activeGame); -+ const sourceIdeaItems = ensureSourceIdeaJourneyItems(activeGame); -+ return { -+ ...bucketResult, -+ sourceIdeaItems, -+ }; -+ } -+ - return { - getTables: async () => clone({ - game_journey_completion_metrics: await completionMetricsStore.listMetrics(), -@@ -1519,6 +1684,7 @@ export function createGameJourneyMockRepository(options = {}) { - getSystemUser: () => getMockDbSystemUser(), - getActiveGame, - openGame, -+ bootstrapGameJourneyForGame, - clearActiveGame: () => gameWorkspaceRepository.clearTestData(), - listNoteTypes: () => clone(tables.game_journey_note_types), - addNoteType, -diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs -index 314a35d03..1c6e9c126 100644 ---- a/src/dev-runtime/server/local-api-router.mjs -+++ b/src/dev-runtime/server/local-api-router.mjs -@@ -17,7 +17,11 @@ import { - createInputMappingToolMockRepository, - } from "../persistence/tool-repositories/input-mapping-mock-repository.js"; - import { createConfiguredBackupStorage, createConfiguredProjectAssetStorage } from "../storage/r2-project-asset-storage.mjs"; --import { loadStorageConfig } from "../storage/storage-config.mjs"; -+import { -+ STORAGE_PROJECTS_PREFIX_LANES, -+ loadStorageConfig, -+ normalizeStorageProjectsPrefix, -+} from "../storage/storage-config.mjs"; - import { createPostgresBackup } from "../database/postgres-backup-service.mjs"; - import { - GFSP_PACKAGE_REQUIRED_FILES, -@@ -311,6 +315,12 @@ const RUNTIME_ENV_SECRET_MARKERS = Object.freeze([ - "JWT", - "DATABASE_URL", - ]); -+const LOCAL_API_STARTUP_DEFAULT_HOST = "127.0.0.1"; -+const LOCAL_API_STARTUP_DEFAULT_PORT = "5501"; -+const LOCAL_API_STARTUP_DEFAULT_PORT_BY_PROTOCOL = Object.freeze({ -+ "http:": "80", -+ "https:": "443", -+}); - const SYSTEM_HEALTH_USAGE_NOT_AVAILABLE = "NOT AVAILABLE"; - const SYSTEM_HEALTH_USAGE_CONTRACTS = Object.freeze({ - GAMEFOUNDRY_DB_CONNECTION_LIMIT: Object.freeze({ -@@ -329,12 +339,6 @@ const SYSTEM_HEALTH_USAGE_CONTRACTS = Object.freeze({ - integrationPoint: "Future R2 provider telemetry can report project asset storage bytes used through the Local API.", - }), - }); --const STORAGE_PROJECTS_PREFIX_LANES = Object.freeze([ -- Object.freeze({ lane: "DEV", path: "/dev/projects/" }), -- Object.freeze({ lane: "IST", path: "/ist/projects/" }), -- Object.freeze({ lane: "UAT", path: "/uat/projects/" }), -- Object.freeze({ lane: "PRD", path: "/prd/projects/" }), --]); - const STORAGE_CONNECTIVITY_ACTIONS = Object.freeze([ - Object.freeze({ id: "storage-list", label: "List" }), - Object.freeze({ id: "storage-write-test-object", label: "Write test object" }), -@@ -497,8 +501,9 @@ function dotEnvValue(key) { - - function storageProjectsPrefixStatus() { - const currentPath = dotEnvValue(STORAGE_PROJECTS_PREFIX_ENV_KEY); -- const matchedLane = STORAGE_PROJECTS_PREFIX_LANES.find((lane) => lane.path === currentPath.value); -- const invalidPath = !currentPath.found || !currentPath.value || !matchedLane; -+ const normalizedPath = normalizeStorageProjectsPrefix(currentPath.value); -+ const matchedLane = STORAGE_PROJECTS_PREFIX_LANES.find((lane) => lane.path === normalizedPath); -+ const invalidPath = !currentPath.found || !normalizedPath || !matchedLane; - const rows = STORAGE_PROJECTS_PREFIX_LANES.map((lane) => { - if (invalidPath) { - return { -@@ -508,7 +513,7 @@ function storageProjectsPrefixStatus() { - value: "ERROR", - }; - } -- const active = currentPath.value === lane.path; -+ const active = normalizedPath === lane.path; - return { - ...lane, - active, -@@ -519,7 +524,7 @@ function storageProjectsPrefixStatus() { - return { - configured: !invalidPath, - invalidPath, -- missing: !currentPath.found || !currentPath.value, -+ missing: !currentPath.found || !normalizedPath, - rows, - secretsExposed: false, - status: invalidPath ? "ERROR" : "PASS", -@@ -735,6 +740,117 @@ function overallHealthStatus(rows) { - return "PASS"; - } - -+function localApiStartupPortFromUrl(value) { -+ const rawValue = String(value || "").trim(); -+ if (!rawValue) { -+ return "not configured"; -+ } -+ try { -+ const parsedUrl = new URL(rawValue); -+ return parsedUrl.port || LOCAL_API_STARTUP_DEFAULT_PORT_BY_PROTOCOL[parsedUrl.protocol] || "not configured"; -+ } catch { -+ return "invalid URL"; -+ } -+} -+ -+function localApiStartupUrlDisplay(value, fallback = "not configured") { -+ const rawValue = String(value || "").trim(); -+ if (!rawValue) { -+ return fallback; -+ } -+ try { -+ const parsedUrl = new URL(rawValue); -+ if (parsedUrl.username) { -+ parsedUrl.username = "********"; -+ } -+ if (parsedUrl.password) { -+ parsedUrl.password = "********"; -+ } -+ parsedUrl.search = ""; -+ parsedUrl.hash = ""; -+ return parsedUrl.toString(); -+ } catch { -+ return "invalid URL"; -+ } -+} -+ -+function localApiStartupBindTarget(env = process.env) { -+ const host = String(env.GAMEFOUNDRY_LOCAL_API_HOST || LOCAL_API_STARTUP_DEFAULT_HOST).trim() || LOCAL_API_STARTUP_DEFAULT_HOST; -+ const port = String(env.GAMEFOUNDRY_LOCAL_API_PORT || LOCAL_API_STARTUP_DEFAULT_PORT).trim() || LOCAL_API_STARTUP_DEFAULT_PORT; -+ const portStatus = /^[1-9]\d*$/.test(port) ? "PASS" : "WARN"; -+ return { -+ host, -+ port, -+ status: portStatus, -+ value: `${host}:${port}`, -+ }; -+} -+ -+function systemHealthLocalApiStartupDiagnostics(env = process.env) { -+ const bindTarget = localApiStartupBindTarget(env); -+ const configuredApiUrl = String(env.GAMEFOUNDRY_API_URL || "").trim(); -+ const derivedApiUrl = `http://${bindTarget.value}/api`; -+ const siteUrl = String(env.GAMEFOUNDRY_SITE_URL || "").trim(); -+ const rows = [ -+ { -+ field: "Approved diagnostics format", -+ reason: "Startup output includes deterministic Environment Variables and All Runtime Ports sections.", -+ status: "PASS", -+ value: "Environment Variables + All Runtime Ports", -+ }, -+ { -+ field: "Environment variable diagnostics", -+ reason: "Startup output masks secret-like values and redacts URL credentials before printing.", -+ status: "PASS", -+ value: "masked and redacted", -+ }, -+ { -+ field: "Configured startup bind target", -+ reason: bindTarget.status === "PASS" -+ ? "Local API startup uses the configured or default host and port for the bind target." -+ : "GAMEFOUNDRY_LOCAL_API_PORT must be a positive integer.", -+ status: bindTarget.status, -+ value: bindTarget.value, -+ }, -+ { -+ field: "Configured site URL", -+ reason: siteUrl -+ ? "GAMEFOUNDRY_SITE_URL is available for startup diagnostics." -+ : "GAMEFOUNDRY_SITE_URL is not configured; startup diagnostics will print not configured.", -+ status: siteUrl ? "PASS" : "WARN", -+ value: localApiStartupUrlDisplay(siteUrl), -+ }, -+ { -+ field: "Configured API URL", -+ reason: configuredApiUrl -+ ? "GAMEFOUNDRY_API_URL is configured and displayed without URL credentials." -+ : "GAMEFOUNDRY_API_URL is not configured; startup diagnostics derive /api from the bind target.", -+ status: "PASS", -+ value: localApiStartupUrlDisplay(configuredApiUrl || derivedApiUrl), -+ }, -+ { -+ field: "Configured API URL port", -+ reason: "Port is derived from the configured or startup-derived API URL for display only.", -+ status: "PASS", -+ value: localApiStartupPortFromUrl(configuredApiUrl || derivedApiUrl), -+ }, -+ { -+ field: "Configurable multiple runtime ports", -+ reason: "Configurable multiple runtime ports are explicitly deferred/cancelled for this PR.", -+ status: "PENDING", -+ value: "deferred/cancelled", -+ }, -+ ]; -+ const actionableRows = rows.filter((row) => row.status !== "PENDING"); -+ return { -+ message: "Local API startup diagnostics use the approved safe output format; configurable multiple runtime ports remain deferred.", -+ rows, -+ secretEditingAllowed: false, -+ secretsExposed: false, -+ status: overallHealthStatus(actionableRows), -+ }; -+} -+ - function systemHealthSummary(rows) { - const counts = systemHealthCounts(rows); - const total = counts.PASS + counts.WARN + counts.FAIL; -@@ -3512,6 +3628,7 @@ LIMIT 1; - const databaseStatus = await this.ownerDatabaseStatus(); - const storageStatus = this.ownerStorageStatus(); - const environmentStatus = storageProjectsPrefixStatus(); -+ const localApiStartup = systemHealthLocalApiStartupDiagnostics(); - const limitRows = systemHealthLimitRows(); - const limitsStatus = systemHealthLimitStatus(limitRows); - const packageStatus = projectPackageReadinessStatus(); -@@ -3563,6 +3680,11 @@ LIMIT 1; - ? "Required limits are configured correctly; live usage is NOT AVAILABLE until provider usage metrics are exposed safely." - : "One or more required limits are missing or invalid in current .env.", - }, -+ { -+ area: "Local API startup diagnostics", -+ status: localApiStartup.status, -+ summary: localApiStartup.message, -+ }, - { - area: "Migration status", - status: databaseStatus.migrationStatus || "WARN", -@@ -3596,6 +3718,12 @@ LIMIT 1; - { area: "Environment configuration", field: environmentStatus.variableName, status: environmentStatus.status, value: environmentStatus.configured ? "valid lane match" : "missing or invalid" }, - { area: "Secrets status", field: "Storage access key", status: storageStatus.accessKeyStatus || "WARN", value: storageStatus.accessKeyConfigured ? "configured; value hidden" : "not configured" }, - { area: "Secrets status", field: "Storage secret key", status: storageStatus.secretKeyStatus || "WARN", value: storageStatus.secretKeyConfigured ? "configured; value hidden" : "not configured" }, -+ ...localApiStartup.rows.map((row) => ({ -+ area: "Local API startup diagnostics", -+ field: row.field, -+ status: row.status, -+ value: row.value, -+ })), - { area: "Migration status", field: "Migration counts", status: databaseStatus.migrationStatus || "WARN", value: `DDL=${databaseStatus.migrationCounts?.DDL || 0}; DML=${databaseStatus.migrationCounts?.DML || 0}` }, - { area: "Project package readiness", field: ".gfsp decision", status: packageStatus.status, value: packageStatus.decisionPath }, - { area: "Project package readiness", field: "Runtime scaffold", status: packageStatus.status, value: `${packageStatus.contract?.packageType || "Game Foundry Studio Project"} ${packageStatus.contract?.contractVersion || ""}`.trim() }, -@@ -3615,6 +3743,7 @@ LIMIT 1; - adminOperations: "/admin/operations.html", - }, - limits: limitRows, -+ localApiStartup, - message: "Admin System Health loaded safe status only.", - operationsHealth, - overview, -@@ -5306,6 +5435,13 @@ LIMIT 1; - } - const result = await method(...args); - assertRepositoryMethodResult(repositoryId, methodName, result); -+ if (repository === this.gameWorkspaceRepository && methodName === "createGame") { -+ const journeyBootstrap = this.gameJourneyRepository.bootstrapGameJourneyForGame(result); -+ if (!journeyBootstrap || !Array.isArray(journeyBootstrap.buckets)) { -+ throw repositoryMethodError("Game Journey bootstrap did not return starter bucket records. Restore the Local API/service contract."); -+ } -+ result.journeyBootstrap = journeyBootstrap; -+ } - const methodPersistsThroughToolStore = - repository === this.gameJourneyRepository && GAME_JOURNEY_TOOL_STORE_METHODS.has(methodName); - if (repositoryMethodRequiresPersistence(methodName) && !methodPersistsThroughToolStore) { -diff --git a/src/dev-runtime/storage/storage-config.mjs b/src/dev-runtime/storage/storage-config.mjs -index e54f404cc..ceecc8469 100644 ---- a/src/dev-runtime/storage/storage-config.mjs -+++ b/src/dev-runtime/storage/storage-config.mjs -@@ -8,6 +8,15 @@ export const STORAGE_ENV_KEYS = Object.freeze([ - ...STORAGE_CONNECTION_ENV_KEYS, - "GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX", - ]); -+export const STORAGE_PROJECTS_PREFIX_LANES = Object.freeze([ -+ Object.freeze({ lane: "DEV", path: "/dev/projects/" }), -+ Object.freeze({ lane: "IST", path: "/ist/projects/" }), -+ Object.freeze({ lane: "UAT", path: "/uat/projects/" }), -+ Object.freeze({ lane: "PRD", path: "/prod/projects/" }), -+]); -+export const STORAGE_PROJECTS_ALLOWED_PREFIXES = Object.freeze( -+ STORAGE_PROJECTS_PREFIX_LANES.map((lane) => lane.path), -+); - export const DB_BACKUP_STORAGE_PROVIDER_ENV = "GAMEFOUNDRY_DB_BACKUP_STORAGE_PROVIDER"; - export const DB_BACKUP_PREFIX_ENV = "GAMEFOUNDRY_DB_BACKUP_PREFIX"; - export const DB_BACKUP_STORAGE_ENV_KEYS = Object.freeze([ -@@ -38,6 +47,10 @@ export function normalizeStorageProjectsPrefix(value) { - return normalizeStoragePrefix(value); - } - -+function storageProjectsPrefixValidationError() { -+ return `GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX must be one of ${STORAGE_PROJECTS_ALLOWED_PREFIXES.join(", ")}.`; -+} -+ - export function loadStorageConfig(env = process.env) { - const missingKeys = STORAGE_ENV_KEYS.filter((key) => !envValue(env, key)); - if (missingKeys.length) { -@@ -80,6 +93,18 @@ export function loadStorageConfig(env = process.env) { - }, - }; - } -+ if (!STORAGE_PROJECTS_ALLOWED_PREFIXES.includes(projectsPrefix)) { -+ return { -+ configured: false, -+ missingKeys: [], -+ safe: { -+ bucket: envValue(env, "GAMEFOUNDRY_STORAGE_BUCKET"), -+ endpoint: endpoint.origin, -+ projectsPrefix, -+ }, -+ validationError: storageProjectsPrefixValidationError(), -+ }; -+ } - - return { - accessKeyId: envValue(env, "GAMEFOUNDRY_STORAGE_ACCESS_KEY_ID"), -diff --git a/src/shared/toolbox/tool-metadata-inventory.js b/src/shared/toolbox/tool-metadata-inventory.js -index 7a4cb7587..955f54d8a 100644 ---- a/src/shared/toolbox/tool-metadata-inventory.js -+++ b/src/shared/toolbox/tool-metadata-inventory.js -@@ -88,8 +88,8 @@ export const TOOL_REGISTRY = Object.freeze([ - "requiredForTestable": false, - "requiredForPublish": false, - "requires": [], -- "status": "Wireframe", -- "releaseChannel": "wireframe", -+ "status": "Ready", -+ "releaseChannel": "complete", - "progressChecklist": [ - "Idea table workflow visible", - "Add Idea and Add Note actions remain inline", -@@ -153,7 +153,7 @@ export const TOOL_REGISTRY = Object.freeze([ - "requiredForPublish": true, - "requires": [], - "status": "Ready", -- "releaseChannel": "beta", -+ "releaseChannel": "complete", - "progressChecklist": [ - "Review readiness", - "Static planned text only" -diff --git a/tests/dev-runtime/AdminHealthOperations.test.mjs b/tests/dev-runtime/AdminHealthOperations.test.mjs -index 7f7ae4f3c..69f2b146d 100644 ---- a/tests/dev-runtime/AdminHealthOperations.test.mjs -+++ b/tests/dev-runtime/AdminHealthOperations.test.mjs -@@ -108,6 +108,8 @@ async function apiJson(baseUrl, pathName, request = {}) { - - test("Admin can view operational health while Creator sessions are blocked", async () => { - await withEnv({ -+ GAMEFOUNDRY_API_URL: "http://api-user:api-secret@127.0.0.1:5501/api", -+ GAMEFOUNDRY_SITE_URL: "http://site-user:site-secret@127.0.0.1:5500", - GAMEFOUNDRY_SUPABASE_ANON_KEY: undefined, - GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: undefined, - GAMEFOUNDRY_SUPABASE_URL: undefined, -@@ -127,6 +129,22 @@ test("Admin can view operational health while Creator sessions are blocked", asy - method: "POST", - }); - const health = await apiJson(server.baseUrl, "/api/admin/system-health/status"); -+ assert.equal(health.localApiStartup.secretEditingAllowed, false); -+ assert.equal(health.localApiStartup.secretsExposed, false); -+ assert.equal(Array.isArray(health.localApiStartup.rows), true); -+ assert.equal( -+ health.localApiStartup.rows.some((row) => row.field === "Approved diagnostics format" && row.status === "PASS"), -+ true, -+ ); -+ assert.equal( -+ health.localApiStartup.rows.some((row) => row.field === "Configurable multiple runtime ports" && row.status === "PENDING" && row.value === "deferred/cancelled"), -+ true, -+ ); -+ const startupText = JSON.stringify(health.localApiStartup); -+ assert.equal(startupText.includes("api-user"), false); -+ assert.equal(startupText.includes("api-secret"), false); -+ assert.equal(startupText.includes("site-user"), false); -+ assert.equal(startupText.includes("site-secret"), false); - assert.equal(Array.isArray(health.operationsHealth.summaryRows), true); - assert.deepEqual( - health.operationsHealth.summaryRows.map((row) => row.area), -diff --git a/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs b/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -index 15fd5ae10..d64d94340 100644 ---- a/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -+++ b/tests/dev-runtime/ProductDataProviderContractHardening.test.mjs -@@ -109,15 +109,15 @@ test("Active browser product-data entrypoints use API or service clients", () => - assert.doesNotMatch(registryClient, /tools:\s*\[\]/); - - const productApiClients = [ -- "toolbox/assets/assets-api-client.js", -- "toolbox/colors/palette-api-client.js", -- "toolbox/controls/controls-api-client.js", -- "toolbox/game-configuration/game-configuration-api-client.js", -- "toolbox/game-design/game-design-api-client.js", -- "toolbox/game-journey/game-journey-api-client.js", -+ "assets/js/shared/assets-api-client.js", -+ "assets/toolbox/colors/js/index.js", -+ "assets/js/shared/controls-api-client.js", -+ "assets/toolbox/game-configuration/js/index.js", -+ "assets/toolbox/game-design/js/index.js", -+ "assets/js/shared/game-journey-api-client.js", - "toolbox/game-hub/game-hub-api-client.js", -- "toolbox/objects/objects-api-client.js", -- "toolbox/tags/tags-api-client.js", -+ "assets/toolbox/objects/js/index.js", -+ "assets/toolbox/tags/js/index.js", - ]; - productApiClients.forEach((filePath) => { - assert.equal(existsSync(path.join(repoRoot, filePath)), true, `${filePath} missing`); -diff --git a/tests/dev-runtime/StorageConfig.test.mjs b/tests/dev-runtime/StorageConfig.test.mjs -new file mode 100644 -index 000000000..b7e30cd6f ---- /dev/null -+++ b/tests/dev-runtime/StorageConfig.test.mjs -@@ -0,0 +1,72 @@ -+import assert from "node:assert/strict"; -+import test from "node:test"; -+import { -+ STORAGE_PROJECTS_ALLOWED_PREFIXES, -+ loadStorageConfig, -+ normalizeStorageProjectsPrefix, -+} from "../../src/dev-runtime/storage/storage-config.mjs"; -+ -+function validStorageEnv(projectsPrefix = "/dev/projects/") { -+ return { -+ GAMEFOUNDRY_STORAGE_ACCESS_KEY_ID: "test-access-key", -+ GAMEFOUNDRY_STORAGE_BUCKET: "gamefoundry-test-assets", -+ GAMEFOUNDRY_STORAGE_ENDPOINT: "https://r2.example.invalid", -+ GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX: projectsPrefix, -+ GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY: "test-secret-key", -+ }; -+} -+ -+test("storage projects prefix normalizes slash variants", () => { -+ assert.equal(normalizeStorageProjectsPrefix("dev/projects"), "/dev/projects/"); -+ assert.equal(normalizeStorageProjectsPrefix("\\ist\\projects\\"), "/ist/projects/"); -+ assert.equal(normalizeStorageProjectsPrefix(" /uat/projects/ "), "/uat/projects/"); -+ assert.equal(normalizeStorageProjectsPrefix("prod/projects/"), "/prod/projects/"); -+}); -+ -+test("storage config accepts only approved project storage prefixes", () => { -+ assert.deepEqual(STORAGE_PROJECTS_ALLOWED_PREFIXES, [ -+ "/dev/projects/", -+ "/ist/projects/", -+ "/uat/projects/", -+ "/prod/projects/", -+ ]); -+ -+ STORAGE_PROJECTS_ALLOWED_PREFIXES.forEach((projectsPrefix) => { -+ const config = loadStorageConfig(validStorageEnv(projectsPrefix)); -+ assert.equal(config.configured, true); -+ assert.equal(config.safe.projectsPrefix, projectsPrefix); -+ }); -+}); -+ -+test("storage config rejects unapproved project storage prefixes", () => { -+ ["/production/projects/", "/qa/projects/", "/projects/"].forEach((projectsPrefix) => { -+ const config = loadStorageConfig(validStorageEnv(projectsPrefix)); -+ assert.equal(config.configured, false); -+ assert.equal(config.safe.projectsPrefix, projectsPrefix); -+ assert.match(config.validationError, /GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX must be one of/); -+ assert.equal(config.validationError.includes("/prod/projects/"), true); -+ }); -+}); -+ -+test("storage config reports missing project storage prefix", () => { -+ const env = validStorageEnv(""); -+ const config = loadStorageConfig(env); -+ assert.equal(config.configured, false); -+ assert.deepEqual(config.missingKeys, ["GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX"]); -+ assert.equal(config.safe.projectsPrefix, ""); -+}); -+ -+test("storage safe config does not expose credential values", () => { -+ const env = validStorageEnv("/prod/projects/"); -+ const config = loadStorageConfig(env); -+ assert.equal(config.configured, true); -+ assert.deepEqual(config.safe, { -+ bucket: "gamefoundry-test-assets", -+ endpoint: "https://r2.example.invalid", -+ projectsPrefix: "/prod/projects/", -+ }); -+ assert.equal(Object.hasOwn(config.safe, "accessKeyId"), false); -+ assert.equal(Object.hasOwn(config.safe, "secretAccessKey"), false); -+ assert.equal(JSON.stringify(config.safe).includes("test-access-key"), false); -+ assert.equal(JSON.stringify(config.safe).includes("test-secret-key"), false); -+}); -diff --git a/tests/core/FixedTicker.test.mjs b/tests/engine/core/FixedTicker.test.mjs -similarity index 89% -rename from tests/core/FixedTicker.test.mjs -rename to tests/engine/core/FixedTicker.test.mjs -index ed83c2069..8c42ed386 100644 ---- a/tests/core/FixedTicker.test.mjs -+++ b/tests/engine/core/FixedTicker.test.mjs -@@ -5,7 +5,7 @@ David Quesenberry - FixedTicker.test.mjs - */ - import assert from "node:assert/strict"; --import FixedTicker from "../../src/engine/core/FixedTicker.js"; -+import FixedTicker from "../../../src/engine/core/FixedTicker.js"; - - export function run() { - const ticker = new FixedTicker({ stepMs: 10, maxCatchUpSteps: 3 }); -diff --git a/tests/core/FrameClock.test.mjs b/tests/engine/core/FrameClock.test.mjs -similarity index 88% -rename from tests/core/FrameClock.test.mjs -rename to tests/engine/core/FrameClock.test.mjs -index da1f20401..a89f7c102 100644 ---- a/tests/core/FrameClock.test.mjs -+++ b/tests/engine/core/FrameClock.test.mjs -@@ -5,7 +5,7 @@ David Quesenberry - FrameClock.test.mjs - */ - import assert from "node:assert/strict"; --import FrameClock from "../../src/engine/core/FrameClock.js"; -+import FrameClock from "../../../src/engine/core/FrameClock.js"; - - export function run() { - const clock = new FrameClock({ maxDeltaMs: 50 }); -diff --git a/tests/core/RuntimeMetrics.test.mjs b/tests/engine/core/RuntimeMetrics.test.mjs -similarity index 90% -rename from tests/core/RuntimeMetrics.test.mjs -rename to tests/engine/core/RuntimeMetrics.test.mjs -index 89b37376c..30a0af924 100644 ---- a/tests/core/RuntimeMetrics.test.mjs -+++ b/tests/engine/core/RuntimeMetrics.test.mjs -@@ -5,7 +5,7 @@ David Quesenberry - RuntimeMetrics.test.mjs - */ - import assert from 'node:assert/strict'; --import RuntimeMetrics from '../../src/engine/core/RuntimeMetrics.js'; -+import RuntimeMetrics from '../../../src/engine/core/RuntimeMetrics.js'; - - export function run() { - const metrics = new RuntimeMetrics({ sampleWindowSeconds: 0.25 }); -diff --git a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -index 9287e4310..f3c860186 100644 ---- a/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -+++ b/tests/playwright/tools/AdminHealthOperationsPage.spec.mjs -@@ -129,6 +129,11 @@ test("Admin System Health renders Postgres diagnostics through the safe status A - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("IST"); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("UAT"); - await expect(page.getByRole("table", { name: "Environment summary" })).toContainText("PRD"); -+ await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Approved diagnostics format"); -+ await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Environment Variables + All Runtime Ports"); -+ await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("Configurable multiple runtime ports"); -+ await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).toContainText("deferred/cancelled"); -+ await expect(page.getByRole("table", { name: "Local API startup diagnostics" })).not.toContainText("secret"); - await expect(page.getByRole("table", { name: "Database health" })).toContainText("Postgres"); - await expect(page.locator("[data-admin-system-health-db-value='provider']")).toHaveText("Postgres"); - await expect(page.locator("[data-admin-system-health-db-value='host']")).not.toHaveText("Configured host placeholder"); -@@ -213,6 +218,7 @@ test("Admin System Health operations page keeps scripts and styles external", as - expect(pageSource).not.toContain("No active failure is declared"); - expect(pageSource).not.toContain("SQLite"); - expect(pageSource).toContain("Diagnostics Plan"); -+ expect(pageSource).toContain("Local API Startup Diagnostics"); - expect(pageSource).toContain("Server-owned Postgres health reader"); - expect(pageSource).toContain("Server-owned Cloudflare R2 storage diagnostic"); - expect(pageSource).toContain("assets/theme-v2/js/admin-system-health.js"); -diff --git a/tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs b/tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs -index bed813761..61ba37122 100644 ---- a/tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs -+++ b/tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs -@@ -37,7 +37,7 @@ function storagePathStatusFor(configuredPath) { - { lane: "DEV", path: "/dev/projects/" }, - { lane: "IST", path: "/ist/projects/" }, - { lane: "UAT", path: "/uat/projects/" }, -- { lane: "PRD", path: "/prd/projects/" }, -+ { lane: "PRD", path: "/prod/projects/" }, - ]; - const matchedLane = lanes.find((lane) => lane.path === configuredPath); - const invalidPath = !matchedLane; -@@ -480,7 +480,7 @@ async function expectStoragePathStatusRows(page, expectedValues) { - const storageRows = page.locator("[data-admin-storage-path-status-rows] tr"); - await expect(storageRows).toHaveCount(4); - const lanes = ["DEV", "IST", "UAT", "PRD"]; -- const paths = ["/dev/projects/", "/ist/projects/", "/uat/projects/", "/prd/projects/"]; -+ const paths = ["/dev/projects/", "/ist/projects/", "/uat/projects/", "/prod/projects/"]; - for (let index = 0; index < lanes.length; index += 1) { - await expect(storageRows.nth(index).locator("td")).toHaveText([ - lanes[index], -@@ -554,7 +554,7 @@ for (const adminPage of ADMIN_WIREFRAME_PAGES) { - await expect(page.locator("body")).toContainText("/dev/projects/"); - await expect(page.locator("body")).toContainText("/ist/projects/"); - await expect(page.locator("body")).toContainText("/uat/projects/"); -- await expect(page.locator("body")).toContainText("/prd/projects/"); -+ await expect(page.locator("body")).toContainText("/prod/projects/"); - await expect(page.locator("body")).toContainText("GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX"); - await expectStoragePathStatusRows(page, ["yes", "no", "no", "no"]); - const infrastructureImage = page.locator("[data-image-zoom-target='admin-infrastructure-image-zoom']"); -diff --git a/tests/playwright/tools/AssetToolMockRepository.spec.mjs b/tests/playwright/tools/AssetToolMockRepository.spec.mjs -index c38ee6ec5..6a3f6e56f 100644 ---- a/tests/playwright/tools/AssetToolMockRepository.spec.mjs -+++ b/tests/playwright/tools/AssetToolMockRepository.spec.mjs -@@ -83,6 +83,13 @@ async function openRepoPage(page, pathName, options = {}) { - const failedRequests = []; - const pageErrors = []; - const consoleErrors = []; -+ await page.addInitScript(({ apiUrl, siteUrl }) => { -+ window.GameFoundryPublicConfig = { -+ apiUrl, -+ environmentLabel: "Development Environment", -+ siteUrl, -+ }; -+ }, { apiUrl: `${server.baseUrl}/api`, siteUrl: server.baseUrl }); - - page.on("pageerror", (error) => { - pageErrors.push(error.message); -@@ -810,7 +817,7 @@ test("Assets worker keeps UI responsive while server-received upload progress dr - const workerPromise = page.waitForEvent("worker"); - await editRow.getByLabel("Upload File").setInputFiles(uploadFile("worker-progress.png", "image/png", Buffer.alloc(48, 7))); - const worker = await workerPromise; -- expect(worker.url()).toContain("toolbox/assets/assets-upload-worker.js"); -+ expect(worker.url()).toContain("assets/toolbox/assets/js/assets-upload-worker.js"); - - await expect(inlineProgress).toBeVisible(); - await expect(progressBar).toHaveJSProperty("value", 0); -diff --git a/tests/playwright/tools/BuildPathProgressSimplification.spec.mjs b/tests/playwright/tools/BuildPathProgressSimplification.spec.mjs -index f4644795e..68d71f7a7 100644 ---- a/tests/playwright/tools/BuildPathProgressSimplification.spec.mjs -+++ b/tests/playwright/tools/BuildPathProgressSimplification.spec.mjs -@@ -101,18 +101,24 @@ test("Toolbox removes Progress view and renders the DB-backed Build Path table", - await expect(page.locator("[data-build-path-table='workflow']")).toBeVisible(); - await expect(page.locator("[data-build-path-table='workflow'] th")).toHaveText(["Order", "Tool", "Status"]); - await expect(page.getByText("What should I do next? Game Configuration")).toBeVisible(); -- await expect(page.getByText("Game Progress: Demo Game identity ready")).toBeVisible(); - await expect(page.getByText("Work top-to-bottom and left-to-right through the workflow table.")).toBeVisible(); - - await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (27)", -- "Wireframe (5)", -- "Beta (7)", -- "Complete (1)", -+ "Wireframe (4)", -+ "Beta (6)", -+ "Complete (3)", - "Deprecated (1)", - ]); - const rows = await buildPathRows(page); - expect(rows).toEqual([ -+ expect.objectContaining({ -+ metadataSource: "toolbox_tool_metadata", -+ order: 1, -+ releaseChannel: "complete", -+ status: "Complete", -+ tool: "Game Hub", -+ }), - expect.objectContaining({ - metadataSource: "toolbox_tool_metadata", - order: 3, -@@ -141,14 +147,16 @@ test("Build Path preserves DB order across selected status filters", async ({ pa - "Game Hub", - "Game Design", - "Colors", -+ "Message Studio", - "Assets", - "Game Configuration", - "Objects", - "Tags", - "Game Journey", -+ "Text To Speech", - ]); -- expect(rows.map((row) => row.order)).toEqual([1, 2, 3, 4, 5, 6, 13, 14]); -- expect(rows.map((row) => row.releaseChannel)).toEqual(["beta", "beta", "complete", "beta", "beta", "beta", "beta", "beta"]); -+ expect(rows.map((row) => row.order)).toEqual([1, 2, 3, 3, 4, 5, 6, 13, 14, 38]); -+ expect(rows.map((row) => row.releaseChannel)).toEqual(["complete", "beta", "complete", "beta", "beta", "beta", "beta", "beta", "beta", "beta"]); - expect(rows.every((row) => row.metadataSource === "toolbox_tool_metadata")).toBe(true); - - await expectNoPageFailures(failures); -@@ -164,9 +172,10 @@ test("Build Path tool names link to registered routes and render badge images", - try { - await page.getByRole("button", { name: "Build Path" }).click(); - const rows = page.locator("[data-build-path-table='workflow'] tbody tr"); -- await expect(rows).toHaveCount(1); -+ await expect(rows).not.toHaveCount(0); - -- const row = rows.first(); -+ const row = page.locator("[data-build-path-tool='Colors']"); -+ await expect(row).toHaveCount(1); - const toolName = await row.getAttribute("data-build-path-tool"); - const registrySnapshot = await fetchApiData(failures.server, "/api/toolbox/registry/snapshot"); - const registryToolsByDisplayName = new Map(registrySnapshot.activeTools.map((tool) => [tool.displayName, tool])); -diff --git a/tests/playwright/tools/GameConfigurationMockRepository.spec.mjs b/tests/playwright/tools/GameConfigurationMockRepository.spec.mjs -index 177835909..8fc1f6e38 100644 ---- a/tests/playwright/tools/GameConfigurationMockRepository.spec.mjs -+++ b/tests/playwright/tools/GameConfigurationMockRepository.spec.mjs -@@ -1,4 +1,5 @@ - import { expect, test } from "@playwright/test"; -+import process from "node:process"; - import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; - import { clearPlaywrightStorage, installPlaywrightStorageIsolation } from "../../helpers/playwrightStorageIsolation.mjs"; - import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs"; -@@ -18,8 +19,26 @@ test.afterAll(async () => { - await workspaceV2CoverageReporter.writeReport(); - }); - -+function restoreEnvValue(key, value) { -+ if (value === undefined) { -+ delete process.env[key]; -+ return; -+ } -+ process.env[key] = value; -+} -+ - async function openRepoPage(page, pathName) { - const server = await startRepoServer(); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ const closeServer = server.close.bind(server); -+ server.close = async () => { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ await closeServer(); -+ }; - const failedRequests = []; - const pageErrors = []; - const consoleErrors = []; -diff --git a/tests/playwright/tools/GameDesignMockRepository.spec.mjs b/tests/playwright/tools/GameDesignMockRepository.spec.mjs -index f6385183e..ebdaf6f9c 100644 ---- a/tests/playwright/tools/GameDesignMockRepository.spec.mjs -+++ b/tests/playwright/tools/GameDesignMockRepository.spec.mjs -@@ -1,4 +1,5 @@ - import { expect, test } from "@playwright/test"; -+import process from "node:process"; - import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; - import { clearPlaywrightStorage, installPlaywrightStorageIsolation } from "../../helpers/playwrightStorageIsolation.mjs"; - import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs"; -@@ -18,8 +19,26 @@ test.afterAll(async () => { - await workspaceV2CoverageReporter.writeReport(); - }); - -+function restoreEnvValue(key, value) { -+ if (value === undefined) { -+ delete process.env[key]; -+ return; -+ } -+ process.env[key] = value; -+} -+ - async function openRepoPage(page, pathName) { - const server = await startRepoServer(); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ const closeServer = server.close.bind(server); -+ server.close = async () => { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ await closeServer(); -+ }; - const failedRequests = []; - const pageErrors = []; - const consoleErrors = []; -diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs -index 2d583e1d7..29e11c37a 100644 ---- a/tests/playwright/tools/GameHubMockRepository.spec.mjs -+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs -@@ -209,6 +209,19 @@ async function openRepoPage(page, pathName, options = {}) { - }); - } - -+ if (pathName.includes("/toolbox/index.html")) { -+ await page.route("**/api/game-journey/completion-metrics", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { records: [] }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ }); -+ }); -+ } -+ - await workspaceV2CoverageReporter.start(page); - await page.goto(`${server.baseUrl}${pathName}`, { waitUntil: "networkidle" }); - return { failedRequests, pageErrors, consoleErrors, server }; -@@ -235,7 +248,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-hub/index.html"); -+ await expect(page.locator("main").getByRole("link", { name: "Open Game Hub" })).toHaveAttribute("href", "toolbox/game-hub/index.html"); - - await expectNoPageFailures(failures); - } finally { -@@ -249,45 +262,194 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { - try { - await expect(page.locator(".tool-workspace")).toBeVisible(); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -- await expect(page.getByRole("button", { name: "Create Game" })).toHaveClass("btn"); -- await expect(page.getByRole("button", { name: "Create Game" })).toBeEnabled(); -+ await expect(page.getByRole("button", { name: "Add Game" })).toHaveClass(/\bbtn\b/); -+ await expect(page.getByRole("button", { name: "Add Game" })).toHaveClass(/\bbtn--compact\b/); -+ await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); -+ await expect(page.getByLabel("Game Name")).toHaveCount(0); -+ await expect(page.getByLabel("Game Purpose")).toHaveCount(0); -+ await expect(page.locator("input[aria-label='Game Status'], textarea[aria-label='Game Status'], select[aria-label='Game Status']")).toHaveCount(0); - await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveClass("btn"); - await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeEnabled(); -- await expect(page.locator("[data-project-record-status]")).toHaveText("Project Information loaded."); -- await expect(page.locator("[data-game-project-information]")).toContainText("Project Information"); -- await expect(page.locator("[data-project-records-table]")).toContainText("Demo Game"); -- await expect(page.locator("[data-source-idea-section]")).toContainText("No source idea yet"); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); -- await expect(page.locator("[data-active-game-purpose]")).toHaveText("Game"); -- await expect(page.locator("[data-current-user-role]")).toHaveText("Owner"); -+ await expect(page.locator("summary").filter({ hasText: /^Game Setup$/ })).toHaveCount(0); -+ await expect(page.locator("summary").filter({ hasText: /^Game Crew$/ })).toHaveCount(0); -+ await expect(page.locator("main")).not.toContainText("game-hub/Game Crew"); -+ await expect(page.getByLabel("Current User Role")).toHaveCount(0); -+ await expect(page.getByRole("link", { name: "Open Game Journey" })).toHaveCount(0); -+ await expect(page.locator(".tool-center-panel")).not.toContainText("Review games in the parent table"); -+ await expect(page.locator("[data-project-record-status]")).toHaveText("Game table loaded."); -+ await expect(page.locator("[data-game-project-information]")).toHaveCount(0); -+ await expect(page.locator("[data-project-records-table]")).toHaveCount(0); -+ await expect(page.locator("[data-active-game-name]")).toHaveCount(0); -+ await expect(page.locator("[data-current-user-role]")).toHaveCount(0); -+ await expect(page.locator("[data-recommended-next-tool]")).toHaveCount(0); -+ await expect(page.locator("[data-source-idea-section]")).toHaveCount(0); -+ await expect(page.locator("[data-game-output-panels]")).toHaveCount(0); -+ await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); -+ await expect(page.locator("aside [data-game-list]")).toHaveCount(0); -+ await expect(page.locator(".tool-center-panel [data-game-list]")).toContainText("Demo Game"); - await expect(page.locator("[data-game-list]")).toContainText("Demo Game"); - await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo"); - await expect(page.locator("[data-game-list]")).toContainText("Collision Demo"); - await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo"); -+ await expect(page.locator("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); -+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); -+ await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); -+ await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); -+ await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ -+ "Game", -+ "Purpose", -+ "Status", -+ "Actions", -+ ]); -+ await expect(page.locator("[data-game-rows-table='true'] thead")).not.toContainText(/Owner|Role|Next Tool/); - const demoGameRow = page.locator("[data-game-row='demo-game']"); -+ await expect(demoGameRow.locator("td")).toHaveText(["Game", "Under Construction", "Edit"]); -+ await expect(demoGameRow).not.toContainText("User 1"); -+ await expect(demoGameRow).not.toHaveAttribute("data-game-active", "true"); -+ await expect(demoGameRow).not.toHaveAttribute("aria-current", "true"); -+ await expect(demoGameRow.locator("th[data-game-active-cell='true']")).toHaveCount(0); -+ await expect(page.locator("[data-game-row][data-game-active='true']")).toHaveCount(0); -+ await expect(page.locator("[data-game-row][aria-current='true']")).toHaveCount(0); -+ await expect(page.locator("[data-game-active-cell='true']")).toHaveCount(0); -+ const rowVisuals = await page.locator("[data-game-row]").evaluateAll((rows) => rows.map((row) => { -+ const cells = Array.from(row.children).slice(1); -+ return cells.map((cell) => { -+ const styles = getComputedStyle(cell); -+ return { -+ backgroundColor: styles.backgroundColor, -+ boxShadow: styles.boxShadow, -+ }; -+ }); -+ })); -+ expect(rowVisuals[0]).toEqual(rowVisuals[1]); - await expect(demoGameRow.locator("> .status")).toHaveCount(0); -- await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/); -- await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true"); -- -- await page.getByLabel("Game Name").fill("Launch Test Game"); -- await page.getByRole("button", { name: "Create Game" }).click(); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Launch Test Game"); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false"); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass(/\bprimary\b/); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass(/\bbtn--compact\b/); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("data-game-active", "true"); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1); -+ await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1); -+ const activeButtonStyle = await demoGameRow.locator("[data-game-toggle='demo-game']").evaluate((button) => { -+ const styles = getComputedStyle(button); -+ return { -+ backgroundColor: styles.backgroundColor, -+ borderColor: styles.borderColor, -+ boxShadow: styles.boxShadow, -+ }; -+ }); -+ const inactiveButtonStyle = await page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']").evaluate((button) => { -+ const styles = getComputedStyle(button); -+ return { -+ backgroundColor: styles.backgroundColor, -+ borderColor: styles.borderColor, -+ boxShadow: styles.boxShadow, -+ }; -+ }); -+ expect(activeButtonStyle).not.toEqual(inactiveButtonStyle); -+ await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveText("Edit"); -+ await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/); -+ await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveClass(/\bbtn--compact\b/); -+ await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).not.toHaveAttribute("aria-current", "true"); -+ await demoGameRow.locator("[data-game-toggle='demo-game']").click(); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true"); -+ const demoChildRows = page.locator("[data-game-expanded-row='demo-game']"); -+ await expect(demoChildRows).toHaveCount(1); -+ await expect(demoChildRows.nth(0)).toHaveAttribute("data-game-child-row", "readiness-output"); -+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toHaveCount(0); -+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table]")).toHaveCount(1); -+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='source-idea']")).toHaveCount(0); -+ const readinessOutputTable = demoChildRows.nth(0).locator("[data-game-child-table='readiness-output']"); -+ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output"); -+ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]); -+ await expect(readinessOutputTable.locator("tbody tr")).toHaveText([ -+ "Game StatusUnder Construction", -+ "Game ProgressDemo Game identity ready", -+ "Launch ProgressPublish blocked until configuration and required assets are ready", -+ "Current FocusComplete Game Configuration", -+ "Recommended Next ToolGame Configuration", -+ "Game identityComplete", -+ "Game configurationUnder Construction", -+ "Playable buildPlanned", -+ "Publishing reviewPlanned", -+ ]); -+ await demoGameRow.locator("[data-game-toggle='demo-game']").click(); -+ await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0); -+ -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ const addGameRow = page.locator("[data-game-add-row='input']"); -+ await expect(addGameRow.locator("[data-game-action]")).toHaveText(["Save", "Cancel"]); -+ await expect(addGameRow.getByRole("button", { name: "Save" })).toHaveClass(/\bbtn--compact\b/); -+ await expect(addGameRow.getByRole("button", { name: "Save" })).toHaveClass("btn btn--compact primary"); -+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass("btn btn--compact primary"); -+ await expect(addGameRow.locator("td")).toHaveCount(3); -+ const addGameNameInput = addGameRow.getByLabel("Game"); -+ await expect(addGameNameInput).toHaveAttribute("required", ""); -+ await addGameRow.getByRole("button", { name: "Save" }).click(); -+ await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); -+ await expect(addGameNameInput).toBeFocused(); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); -+ await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); -+ await addGameNameInput.fill(" "); -+ await addGameRow.getByRole("button", { name: "Save" }).click(); -+ await expect(addGameNameInput).toHaveAttribute("aria-invalid", "true"); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Enter a game name before saving."); -+ await expect(page.locator("[data-game-list]")).not.toContainText("Untitled Game"); -+ await addGameNameInput.fill("Launch Test Game"); -+ await addGameRow.getByLabel("Purpose").selectOption("Learning Game"); -+ await addGameRow.getByLabel("Status").selectOption("Ready for Testing"); -+ await addGameRow.getByRole("button", { name: "Save" }).click(); - 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-row='launch-test-game-1']")).not.toHaveAttribute("data-game-active", "true"); -+ await expect(page.locator("[data-game-row='launch-test-game-1']")).not.toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] td").nth(0)).toHaveText("Learning Game"); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] td").nth(1)).toHaveText("Ready for Testing"); - 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(); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Archive Game"); -- -- 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-hub-log]")).toHaveText("Opened Launch Test Game."); -+ await page.getByRole("button", { name: "Edit Launch Test Game" }).click(); -+ const editGameRow = page.locator("[data-game-edit-row='launch-test-game-1']"); -+ await expect(editGameRow.locator("[data-game-action]")).toHaveText(["Save", "Cancel"]); -+ await expect(editGameRow.getByRole("button", { name: "Save" })).toHaveClass(/\bbtn--compact\b/); -+ await expect(editGameRow.getByLabel("Game")).toHaveValue("Launch Test Game"); -+ await expect(editGameRow.getByLabel("Game")).toHaveAttribute("readonly", ""); -+ await editGameRow.getByLabel("Purpose").selectOption("Capability Demo"); -+ await editGameRow.getByLabel("Status").selectOption("Ready for Publish"); -+ await editGameRow.getByRole("button", { name: "Save" }).click(); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] td").nth(0)).toHaveText("Capability Demo"); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] td").nth(1)).toHaveText("Ready for Publish"); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Saved Launch Test Game."); -+ -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ const archiveAddRow = page.locator("[data-game-add-row='input']"); -+ await archiveAddRow.getByLabel("Game").fill("Archive Game"); -+ await archiveAddRow.getByRole("button", { name: "Cancel" }).click(); -+ await expect(page.locator("[data-game-list]")).not.toContainText("Archive Game"); -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ await page.locator("[data-game-add-row='input']").getByLabel("Game").fill("Archive Game"); -+ await page.locator("[data-game-add-row='input']").getByRole("button", { name: "Save" }).click(); -+ await expect(page.locator("[data-game-row='archive-game-2']")).not.toHaveAttribute("data-game-active", "true"); -+ await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).toHaveClass("btn btn--compact primary"); -+ -+ await page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']").click(); -+ await expect(page.locator("[data-game-row='launch-test-game-1']")).not.toHaveAttribute("data-game-active", "true"); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).not.toHaveAttribute("aria-current", "true"); -+ await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1); -+ await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1); -+ await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(1); -+ await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveAttribute("data-game-child-row", "readiness-output"); -+ await expect(page.locator("[data-game-expanded-row='archive-game-2']")).toHaveCount(0); -+ await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/); -+ await expect(page.locator("[data-game-hub-log]")).not.toHaveText("Selected 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-row='launch-test-game-1']")).toHaveCount(0); - await expect(page.locator("[data-game-list]")).not.toContainText("Launch Test Game"); - await expect(page.locator("[data-game-hub-log]")).toHaveText("Deleted Launch Test Game."); - -@@ -297,31 +459,268 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => { - } - }); - -+test("Game Hub validates game parent rows and child tables", async ({ page }) => { -+ const sourceLinkedGame = { -+ id: "lantern-reef", -+ ownerKey: MOCK_DB_KEYS.users.user1, -+ name: "Lantern Reef", -+ purpose: "Game", -+ status: "Ready for Testing", -+ ownerDisplayName: "User 1", -+ members: [ -+ { -+ displayName: "User 1", -+ gameId: "lantern-reef", -+ permission: "Owner", -+ role: "Owner", -+ userKey: MOCK_DB_KEYS.users.user1, -+ }, -+ ], -+ sourceIdea: { -+ idea: "Lantern Reef", -+ pitch: "Guide reef keepers through dusk storms.", -+ notes: [ -+ "Keep traversal gentle.", -+ "Use warm lantern art.", -+ ], -+ }, -+ }; -+ const journeyBuckets = [ -+ "Idea", -+ "Design", -+ "Graphics", -+ "Audio", -+ "Objects", -+ "Worlds", -+ "Interface", -+ "Controls", -+ "Rules", -+ "Progression", -+ "Play Test", -+ "Publish", -+ "Share", -+ ]; -+ -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { result: sourceLinkedGame }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { -+ result: { -+ currentFocus: "Review source idea context", -+ gameProgress: "Lantern Reef identity ready", -+ gameStatus: "Ready for Testing", -+ publishingProgress: "Launch review pending", -+ recommendedNextTool: "Game Journey", -+ progressChecklist: journeyBuckets.map((label) => ({ label, status: "Planned" })), -+ }, -+ }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { result: [sourceLinkedGame] }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); -+ -+ try { -+ await expect(page.locator("summary").filter({ hasText: /^Open Games$/ })).toHaveCount(0); -+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveCount(0); -+ await expect(page.locator("[data-game-rows-table='true']")).toHaveAttribute("aria-label", "Games"); -+ await expect(page.locator("[data-game-rows-table='true'] caption")).toHaveCount(0); -+ const parentRows = page.locator("[data-game-rows-table='true'] tbody > [data-game-row]"); -+ await expect(parentRows).toHaveCount(1); -+ const gameRow = page.locator("[data-game-row='lantern-reef']"); -+ await expect(gameRow).toContainText("Lantern Reef"); -+ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "false"); -+ -+ await gameRow.locator("[data-game-toggle='lantern-reef']").click(); -+ await expect(gameRow.locator("[data-game-toggle='lantern-reef']")).toHaveAttribute("aria-expanded", "true"); -+ const expandedRows = page.locator("[data-game-expanded-row='lantern-reef']"); -+ await expect(expandedRows).toHaveCount(2); -+ await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); -+ await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); -+ await expect(expandedRows.locator("[data-game-child-table]")).toHaveCount(2); -+ await expect(expandedRows.locator("[data-game-child-table='summary']")).toHaveCount(0); -+ -+ const sourceIdeaTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); -+ await expect(sourceIdeaTable.locator("caption")).toHaveText("Source Idea"); -+ await expect(sourceIdeaTable.locator("tbody tr")).toHaveText([ -+ "IdeaLantern Reef", -+ "PitchGuide reef keepers through dusk storms.", -+ "Note 1Keep traversal gentle.", -+ "Note 2Use warm lantern art.", -+ ]); -+ await expect(sourceIdeaTable.locator("button, input, textarea, select, [contenteditable='true'], [role='button']")).toHaveCount(0); -+ await expect(sourceIdeaTable).not.toContainText(/Edit|Delete|Current Focus|Recommended Next Tool/); -+ -+ const readinessOutputTable = expandedRows.nth(1).locator("[data-game-child-table='readiness-output']"); -+ await expect(readinessOutputTable.locator("caption")).toHaveText("Readiness Output"); -+ await expect(readinessOutputTable.locator("thead th")).toHaveText(["Output", "Status"]); -+ await expect(readinessOutputTable).not.toContainText(/Guide reef keepers|Keep traversal gentle|Use warm lantern art/); -+ await expect(readinessOutputTable.locator("[data-readiness-checklist-row] th")).toHaveText(journeyBuckets); -+ -+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveCount(0); -+ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveCount(0); -+ await expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ - test("Game Hub preserves guest browsing and blocks guest saves", async ({ page }) => { - const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); - - try { -- await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); -+ await expect(page.locator("[data-game-row='demo-game'] [data-game-toggle='demo-game']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/); -+ await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).toBeEnabled(); - await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo"); -- await expect(page.locator("[data-project-record-status]")).toHaveText("Project Information loaded. Sign in to save changes."); -- await expect(page.locator("[data-project-records-table]")).toContainText("Demo Game"); -- await expect(page.getByRole("button", { name: "Create Game" })).toBeDisabled(); -+ await expect(page.locator("[data-project-record-status]")).toHaveText("Game table loaded. Sign in to save changes."); -+ await expect(page.locator("[data-project-records-table]")).toHaveCount(0); -+ await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); - await expect(page.getByRole("button", { name: "Delete Open Game" })).toBeDisabled(); -- await expect(page.getByLabel("Game Name")).toBeDisabled(); -- await expect(page.getByLabel("Game Purpose")).toBeDisabled(); -- await expect(page.getByLabel("Game Status")).toBeDisabled(); -- await expect(page.getByLabel("Current User Role")).toBeDisabled(); -- -- await page.getByRole("button", { name: "Open Gravity Demo" }).click(); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Gravity Demo"); -+ await expect(page.getByLabel("Game Name")).toHaveCount(0); -+ await expect(page.getByLabel("Game Purpose")).toHaveCount(0); -+ await expect(page.locator("input[aria-label='Game Status'], textarea[aria-label='Game Status'], select[aria-label='Game Status']")).toHaveCount(0); -+ await expect(page.getByLabel("Current User Role")).toHaveCount(0); -+ -+ await page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']").click(); -+ await expect(page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='demo-game'] [data-game-toggle='demo-game']")).not.toHaveClass(/primary/); -+ await expect(page.locator("[data-game-row='gravity-demo']").getByRole("button", { name: "Edit Gravity Demo" })).toBeEnabled(); - await expect(page.locator("[data-game-hub-log]")).toHaveText("Sign in to create or update Game Hub projects."); - -+ await page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" }).click(); -+ await page.locator("[data-game-edit-row='demo-game']").getByRole("button", { name: "Save" }).click(); -+ await page.waitForURL(/\/account\/sign-in\.html$/); -+ -+ await page.goto(`${failures.server.baseUrl}/toolbox/game-hub/index.html`, { waitUntil: "networkidle" }); -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ await page.locator("[data-game-add-row='input']").getByRole("button", { name: "Save" }).click(); -+ await page.waitForURL(/\/account\/sign-in\.html$/); -+ -+ expect(failures.pageErrors).toEqual([]); -+ expect(failures.consoleErrors.filter((message) => !/Failed to fetch|gamefoundry-partials\.js/.test(message))).toEqual([]); -+ expect(failures.failedRequests.filter((request) => /^\d/.test(request) && !request.includes("/account/sign-in.html"))).toEqual([]); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("Game Hub shows a creator-safe empty state when no projects exist", async ({ page }) => { -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { result: null }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getGameProgress", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { -+ result: { -+ gameStatus: "No Game", -+ gameProgress: "No active game", -+ publishingProgress: "Not started", -+ currentFocus: "Create a game", -+ recommendedNextTool: "Game Hub", -+ progressChecklist: [], -+ }, -+ }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ data: { result: [] }, -+ ok: true, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 200, -+ }); -+ }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); -+ -+ try { -+ await expect(page.locator("[data-active-game-name]")).toHaveCount(0); -+ await expect(page.locator("[data-game-list] [data-game-list-status='empty']")).toHaveText("No Game Hub projects yet. Add a game to start building."); -+ await expect(page.locator("[data-game-rows-table='true'] thead th")).toHaveText([ -+ "Game", -+ "Purpose", -+ "Status", -+ "Actions", -+ ]); -+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); -+ await expect(page.locator("[data-game-list] [data-game-add-row='button']")).toHaveCount(1); -+ await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); -+ await expect(page.locator("[data-game-hub-log]")).not.toContainText(/server|API|repository|database|stack|error/i); - await expectNoPageFailures(failures); - } finally { - await failures.server.close(); - } - }); - -+test("Game Hub shows a creator-safe unavailable state when project list API fails", async ({ page }) => { -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/listGames", async (route) => { -+ await route.fulfill({ -+ body: JSON.stringify({ -+ error: "postgres://service-role-secret@internal.example:5432/gamefoundry failed with stack trace", -+ ok: false, -+ rule: "Browser -> Server API -> Data Source", -+ }), -+ contentType: "application/json; charset=utf-8", -+ status: 503, -+ }); -+ }); -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); -+ -+ try { -+ expect(failures.failedRequests.some((request) => request.includes("503") && request.includes("/methods/listGames"))).toBe(true); -+ await expect(page.locator("[data-game-list] [data-game-list-status='unavailable']")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); -+ await expect(page.locator("[data-game-list] [data-game-row]")).toHaveCount(0); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."); -+ await expect(page.locator("main")).not.toContainText(/postgres|service-role-secret|internal\.example|stack trace|repository|database/i); -+ expect(failures.pageErrors).toEqual([]); -+ expect(failures.consoleErrors.filter((message) => !message.includes("status of 503"))).toEqual([]); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ - test("Game Hub shows active-game errors without throwing", async ({ page }) => { - await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { - await route.fulfill({ -@@ -338,7 +737,7 @@ test("Game Hub shows active-game errors without throwing", async ({ page }) => { - - 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-active-game-name]")).toHaveCount(0); - 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([]); -@@ -367,10 +766,10 @@ test("Game Hub reports malformed active-game payloads without throwing", async ( - 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-active-game-name]")).toHaveCount(0); -+ await expect(page.locator("[data-current-user-role]")).toHaveCount(0); - await expect(page.locator("[data-game-hub-log]")).toContainText("Active game is temporarily unavailable."); -- await expect(page.getByLabel("Game Purpose")).toBeDisabled(); -+ await expect(page.getByRole("button", { name: "Add Game" })).toBeEnabled(); - - await expectNoPageFailures(failures); - } finally { -@@ -378,55 +777,44 @@ test("Game Hub reports malformed active-game payloads without throwing", async ( - } - }); - --test("Game Hub displays and edits game purpose and member role", async ({ page }) => { -+test("Game Hub displays and edits game purpose", async ({ page }) => { - const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); - - try { -- await expect(page.locator("#gamePurposeInput option")).toHaveText([ -+ await expect(page.getByLabel("Current User Role")).toHaveCount(0); -+ -+ await page.getByRole("button", { name: "Edit Demo Game" }).click(); -+ const editRow = page.locator("[data-game-edit-row='demo-game']"); -+ await expect(editRow.locator("[data-game-action]")).toHaveText(["Save", "Cancel"]); -+ await expect(editRow.getByLabel("Purpose").locator("option")).toHaveText([ - "Game", - "Capability Demo", - "Learning Game", - "Template Game" - ]); -- await expect(page.locator("#gameStatusInput option")).toHaveText([ -+ await expect(editRow.getByLabel("Status").locator("option")).toHaveText([ - "Planning", - "Under Construction", - "Ready for Testing", - "Ready for Publish" - ]); -- await expect(page.locator("#currentUserRoleInput option")).toHaveText([ -- "Owner", -- "Designer", -- "World Builder", -- "Artist", -- "Audio Creator", -- "Translator", -- "Tester", -- "Publisher", -- "Viewer" -- ]); -- await expect(page.getByLabel("Game Purpose")).toHaveValue("Game"); -- await expect(page.getByLabel("Game Status")).toHaveValue("Under Construction"); -- await expect(page.getByLabel("Current User Role")).toHaveValue("Owner"); -- -- await page.getByLabel("Game Purpose").selectOption("Learning Game"); -- await expect(page.locator("[data-active-game-purpose]")).toHaveText("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-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-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"); -- await page.getByRole("button", { name: "Create Game" }).click(); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Purpose Review Game"); -- await expect(page.locator("[data-active-game-purpose]")).toHaveText("Capability Demo"); -- await expect(page.locator("[data-current-user-role]")).toHaveText("Owner"); -+ await expect(editRow.getByLabel("Purpose")).toHaveValue("Game"); -+ await expect(editRow.getByLabel("Status")).toHaveValue("Under Construction"); -+ await editRow.getByLabel("Purpose").selectOption("Learning Game"); -+ await editRow.getByLabel("Status").selectOption("Ready for Testing"); -+ await editRow.getByRole("button", { name: "Save" }).click(); -+ await expect(page.locator("[data-game-row='demo-game'] td").nth(0)).toHaveText("Learning Game"); -+ await expect(page.locator("[data-game-row='demo-game'] td").nth(1)).toHaveText("Ready for Testing"); -+ await expect(page.locator("[data-game-hub-log]")).toHaveText("Saved Demo Game."); -+ -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ const addRow = page.locator("[data-game-add-row='input']"); -+ await addRow.getByLabel("Game").fill("Purpose Review Game"); -+ await addRow.getByLabel("Purpose").selectOption("Capability Demo"); -+ await addRow.getByRole("button", { name: "Save" }).click(); -+ await expect(page.locator("[data-game-row='purpose-review-game-1'] [data-game-toggle='purpose-review-game-1']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='purpose-review-game-1']").getByRole("button", { name: "Edit Purpose Review Game" })).not.toHaveClass(/primary/); -+ await expect(page.locator("[data-game-row='purpose-review-game-1'] td").nth(0)).toHaveText("Capability Demo"); - await expect(page.locator("[data-game-list]")).toContainText("Purpose Review Game"); - - await expectNoPageFailures(failures); -@@ -435,46 +823,46 @@ 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 }) => { -+test("Game Hub readiness child rows update from mock game state", async ({ page }) => { - const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); - - try { -- await expect(page.locator("[data-game-status]")).toHaveText("Under Construction"); -- await expect(page.locator("[data-game-progress]")).toHaveText("Demo Game identity ready"); -- await expect(page.locator("[data-publishing-progress]")).toHaveText("Publish blocked until configuration and required assets are ready"); -- await expect(page.locator("[data-current-focus]")).toHaveText("Complete Game Configuration"); -- await expect(page.locator("[data-recommended-next-tool]").first()).toHaveText("Game Configuration"); -- await expect(page.locator("[data-game-progress-checklist]")).toContainText("Game identity: Complete"); -- await expect(page.locator("[data-game-output-panels] summary")).toHaveText([ -- "Readiness Output" -+ await expect(page.locator("[data-recommended-next-tool]")).toHaveCount(0); -+ await expect(page.locator("[data-source-idea-section]")).toHaveCount(0); -+ await expect(page.locator("[data-game-output-panels]")).toHaveCount(0); -+ await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); -+ -+ const demoGameRow = page.locator("[data-game-row='demo-game']"); -+ await demoGameRow.locator("[data-game-toggle='demo-game']").click(); -+ let readinessOutputTable = page.locator("[data-game-expanded-row='demo-game'][data-game-child-row='readiness-output'] [data-game-child-table='readiness-output']"); -+ await expect(readinessOutputTable.locator("tbody tr")).toHaveText([ -+ "Game StatusUnder Construction", -+ "Game ProgressDemo Game identity ready", -+ "Launch ProgressPublish blocked until configuration and required assets are ready", -+ "Current FocusComplete Game Configuration", -+ "Recommended Next ToolGame Configuration", -+ "Game identityComplete", -+ "Game configurationUnder Construction", -+ "Playable buildPlanned", -+ "Publishing reviewPlanned", - ]); -- await expect(page.locator("aside.tool-column").last().getByText("Readiness Output")).toHaveCount(0); -- const panelOrderIsCorrect = await page.locator(".tool-center-panel").evaluate((panel) => { -- const projectInformation = panel.querySelector("[data-game-project-information]"); -- const sourceIdea = panel.querySelector("[data-source-idea-section]"); -- const staticOverlay = panel.querySelector("[data-game-hub-foundation]"); -- const outputPanels = panel.querySelector("[data-game-output-panels]"); -- return Boolean( -- projectInformation && -- sourceIdea && -- staticOverlay && -- outputPanels && -- (projectInformation.compareDocumentPosition(sourceIdea) & Node.DOCUMENT_POSITION_FOLLOWING) && -- (sourceIdea.compareDocumentPosition(staticOverlay) & Node.DOCUMENT_POSITION_FOLLOWING) && -- (staticOverlay.compareDocumentPosition(outputPanels) & Node.DOCUMENT_POSITION_FOLLOWING) -- ); -- }); -- expect(panelOrderIsCorrect).toBe(true); - -- await page.getByLabel("Game Name").fill("Progress Review Game"); -- await page.getByRole("button", { name: "Create Game" }).click(); -- await expect(page.locator("[data-game-status]")).toHaveText("Under Construction"); -- await expect(page.locator("[data-game-progress]")).toHaveText("Progress Review Game identity ready"); -- await expect(page.locator("[data-game-project-information]")).toContainText("Progress Review Game"); -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ await page.locator("[data-game-add-row='input']").getByLabel("Game").fill("Progress Review Game"); -+ await page.locator("[data-game-add-row='input']").getByRole("button", { name: "Save" }).click(); -+ await expect(page.locator("[data-game-project-information]")).toHaveCount(0); -+ await expect(page.locator("[data-game-list]")).toContainText("Progress Review Game"); -+ const progressReviewRow = page.locator("[data-game-row='progress-review-game-1']"); -+ await progressReviewRow.locator("[data-game-toggle='progress-review-game-1']").click(); -+ readinessOutputTable = page.locator("[data-game-expanded-row='progress-review-game-1'][data-game-child-row='readiness-output'] [data-game-child-table='readiness-output']"); -+ await expect(readinessOutputTable).toContainText("Progress Review Game identity ready"); - - await page.getByRole("button", { name: "Delete Open Game" }).click(); -- await expect(page.locator("[data-active-game-name]")).toHaveText("Demo Game"); -- await expect(page.locator("[data-game-progress]")).toHaveText("Demo Game identity ready"); -+ await expect(page.locator("[data-game-row='demo-game'] [data-game-toggle='demo-game']")).toHaveClass("btn btn--compact primary"); -+ await expect(page.locator("[data-game-row='demo-game']").getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/); -+ await demoGameRow.locator("[data-game-toggle='demo-game']").click(); -+ readinessOutputTable = page.locator("[data-game-expanded-row='demo-game'][data-game-child-row='readiness-output'] [data-game-child-table='readiness-output']"); -+ await expect(readinessOutputTable).toContainText("Demo Game identity ready"); - - await expectNoPageFailures(failures); - } finally { -diff --git a/tests/playwright/tools/GameJourneyTool.spec.mjs b/tests/playwright/tools/GameJourneyTool.spec.mjs -index 390d1cc89..43d9fd733 100644 ---- a/tests/playwright/tools/GameJourneyTool.spec.mjs -+++ b/tests/playwright/tools/GameJourneyTool.spec.mjs -@@ -69,7 +69,6 @@ async function openRepoPage(page, pathName, options = {}) { - page.on("requestfailed", (request) => { - failedRequests.push(`FAILED ${request.url()}`); - }); -- - if (collectCoverage) { - await workspaceV2CoverageReporter.start(page); - } -@@ -215,10 +214,11 @@ async function expectFilteredSummaryRows(page, statusId, expectedRows) { - test("Game Journey exposes static tool ownership areas without automatic counts", async () => { - expectStaticToolOwnershipAreas(GAME_JOURNEY_TOOL_OWNERSHIP_AREAS); - expect(GAME_JOURNEY_RECOMMENDED_TARGETS.map((target) => target.label)).toEqual([ -- "Heroes", -- "Enemies", -- "Levels", -- "Audio", -+ "Hero", -+ "Enemy", -+ "Boss", -+ "Background", -+ "Music", - ]); - - const gameJourneyCompletionMetricsPostgresClient = createGameJourneyCompletionMetricsPostgresClientStub(); -@@ -230,10 +230,11 @@ test("Game Journey exposes static tool ownership areas without automatic counts" - const constants = await fetchApiData(server, "/api/toolbox/game-journey/constants"); - expectStaticToolOwnershipAreas(constants.GAME_JOURNEY_TOOL_OWNERSHIP_AREAS); - expect(constants.GAME_JOURNEY_RECOMMENDED_TARGETS.map((target) => target.label)).toEqual([ -- "Heroes", -- "Enemies", -- "Levels", -- "Audio", -+ "Hero", -+ "Enemy", -+ "Boss", -+ "Background", -+ "Music", - ]); - } finally { - await server.close(); -@@ -256,6 +257,7 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p - const failedRequests = []; - const pageErrors = []; - const consoleErrors = []; -+ const recommendedTargetRequests = []; - - page.on("pageerror", (error) => { - pageErrors.push(error.message); -@@ -273,6 +275,12 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p - page.on("requestfailed", (request) => { - failedRequests.push(`FAILED ${request.url()}`); - }); -+ page.on("request", (request) => { -+ const requestUrl = request.url(); -+ if (requestUrl.includes("/api/toolbox/game-journey/repositories/") && requestUrl.includes("/methods/updateRecommendedTarget")) { -+ recommendedTargetRequests.push(request.postDataJSON()); -+ } -+ }); - - try { - await workspaceV2CoverageReporter.start(page); -@@ -329,15 +337,48 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p - "Next focus: Create, Design, and Graphics. Complete one small item in each area before expanding the plan.", - "Next action: mark one finished section item complete so overall progress can rise above 0%.", - ]); -- await expect(page.locator("[data-journey-recommended-target]")).toHaveCount(4); -- await expect(page.locator("[data-journey-recommended-target='heroes'] td").nth(0)).toHaveText("Heroes"); -- await expect(page.locator("[data-journey-recommended-target='heroes'] td").nth(1)).toHaveText("Objects"); -- await expect(page.locator("[data-journey-target-input='heroes']")).toHaveValue("1"); -- await page.locator("[data-journey-target-input='heroes']").fill("2"); -- await expect(page.locator("[data-journey-target-status]")).toHaveText("Saved Heroes target at 2."); -- await expect(page.locator("[data-journey-target-input='heroes']")).toHaveValue("2"); -+ await expect(page.locator("[data-journey-recommended-target]")).toHaveCount(5); -+ const recommendedTargetOrder = await page.locator("[data-journey-recommended-target]").evaluateAll((rows) => ( -+ rows.map((row) => row.dataset.journeyRecommendedTarget) -+ )); -+ expect(recommendedTargetOrder).toEqual([ -+ "hero", -+ "enemy", -+ "boss", -+ "background", -+ "music", -+ ]); -+ await expect(page.locator("[data-journey-recommended-targets] th")).toHaveText(["Target", "Section", "Count"]); -+ await expect(page.locator("[data-journey-recommended-target] td:first-child")).toHaveText([ -+ "Hero [1]", -+ "Enemy [4]", -+ "Boss [1]", -+ "Background [3]", -+ "Music [5]", -+ ]); -+ await expect(page.locator("[data-journey-recommended-target='hero'] td").nth(1)).toHaveText("Objects"); -+ await expect(page.locator("[data-journey-recommended-target='enemy'] td").nth(1)).toHaveText("Objects"); -+ await expect(page.locator("[data-journey-recommended-target='boss'] td").nth(1)).toHaveText("Objects"); -+ await expect(page.locator("[data-journey-recommended-target='background'] td").nth(1)).toHaveText("Graphics"); -+ await expect(page.locator("[data-journey-recommended-target='music'] td").nth(1)).toHaveText("Audio"); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("1"); -+ await expect(page.locator("[data-journey-target-input='enemy']")).toHaveValue("4"); -+ await expect(page.locator("[data-journey-target-input='boss']")).toHaveValue("1"); -+ await expect(page.locator("[data-journey-target-input='background']")).toHaveValue("3"); -+ await expect(page.locator("[data-journey-target-input='music']")).toHaveValue("5"); -+ await expect(page.locator("[data-journey-recommended-target] input[type='checkbox']")).toHaveCount(0); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("type", "number"); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("inputmode", "numeric"); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveAttribute("aria-label", "Hero count"); -+ await page.locator("[data-journey-target-input='hero']").fill("2"); -+ await expect(page.locator("[data-journey-target-status]")).toHaveText("Saved Hero target at 2."); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2"); -+ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]"); -+ expect(recommendedTargetRequests).toHaveLength(1); -+ expect(recommendedTargetRequests[0].args).toEqual(["hero", 2]); - await page.reload({ waitUntil: "networkidle" }); -- await expect(page.locator("[data-journey-target-input='heroes']")).toHaveValue("2"); -+ await expect(page.locator("[data-journey-target-input='hero']")).toHaveValue("2"); -+ await expect(page.locator("[data-journey-target-count-preview='hero']")).toHaveText(" [2]"); - const repositoryData = await fetchApiData(server, "/api/toolbox/game-journey/repositories", { - body: JSON.stringify({ options: {} }), - method: "POST", -@@ -347,11 +388,15 @@ test("Game Journey progress dashboard summarizes completion metrics", async ({ p - method: "POST", - }); - const persistedTarget = (tablesData.result.game_journey_items || []).find((item) => -- item.linkedRecordType === "recommended-target" && item.linkedRecordId === "heroes", -+ item.linkedRecordType === "recommended-target" && item.linkedRecordId === "hero", -+ ); -+ const objectsBucketNote = (tablesData.result.game_journey_notes || []).find((note) => -+ note.gameKey === GAME_JOURNEY_KEYS.game && note.name === "Objects", - ); -+ expect(objectsBucketNote?.key).toMatch(ULID_PATTERN); - expect(persistedTarget).toMatchObject({ -- noteKey: GAME_JOURNEY_KEYS.notes.designPass, -- title: "Recommended target: Heroes", -+ noteKey: objectsBucketNote.key, -+ title: "Recommended target: Hero", - }); - expect(JSON.parse(persistedTarget.userDetails)).toMatchObject({ suggestedCount: 2 }); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -@@ -441,7 +486,7 @@ test("Game Journey summary table uses inline notes and item subtables", async ({ - - await expect(page.locator("[data-journey-progress-dashboard]")).toBeVisible(); - await expect(page.getByRole("heading", { name: "What To Do Next" })).toBeVisible(); -- await expect(page.locator("[data-journey-recommended-target='heroes']")).toBeVisible(); -+ await expect(page.locator("[data-journey-recommended-target='hero']")).toBeVisible(); - await expectNoPageFailures(failures); - } finally { - await closeWithCoverage(page, failures); -@@ -1531,7 +1576,7 @@ test("Toolbox registration exposes Game Journey navigation", async ({ page }) => - test("Game Journey source stays separate from notes files and browser persistence", async () => { - const sourcePaths = [ - "toolbox/game-journey/index.html", -- "toolbox/game-journey/game-journey.js", -+ "assets/toolbox/game-journey/js/index.js", - "src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js" - ]; - const banned = [ -diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -index 934faf46f..61d8fdc68 100644 ---- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -@@ -1,8 +1,14 @@ - import { expect, test } from "@playwright/test"; -+import { GAME_JOURNEY_BOOTSTRAP_BUCKETS } from "../../../src/dev-runtime/persistence/tool-repositories/game-journey-mock-repository.js"; - import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; - import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs"; -+import { createGameJourneyCompletionMetricsPostgresClientStub } from "../../helpers/gameJourneyCompletionMetricsPostgresClientStub.mjs"; - import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; - -+const EDITABLE_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready"]; -+const FILTER_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready", "Project", "Archived"]; -+const DEFAULT_VISIBLE_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready", "Project"]; -+ - function restoreEnvValue(key, value) { - if (value === undefined) { - delete process.env[key]; -@@ -19,8 +25,6 @@ async function expectIdeaChevron(page, ideaId, iconName) { - const cellStyles = getComputedStyle(cell); - const labelStyles = getComputedStyle(label); - const iconStyles = getComputedStyle(icon); -- const textRect = text.getBoundingClientRect(); -- const iconRect = icon.getBoundingClientRect(); - return { - iconName: icon.dataset.ideaBoardChevronIcon, - labelDisplay: labelStyles.display, -@@ -28,21 +32,19 @@ async function expectIdeaChevron(page, ideaId, iconName) { - iconHeight: Number.parseFloat(iconStyles.height), - fontSize: Number.parseFloat(cellStyles.fontSize), - iconColor: iconStyles.backgroundColor, -- iconBottom: iconRect.bottom, -- iconLeft: iconRect.left, -+ iconBeforeText: Boolean(icon.compareDocumentPosition(text) & Node.DOCUMENT_POSITION_FOLLOWING), -+ iconVerticalAlign: Number.parseFloat(iconStyles.verticalAlign), - textColor: cellStyles.color, -- textBottom: textRect.bottom, -- textLeft: textRect.left, - maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage, - }; - }, ideaId); - expect(metrics.iconName).toBe(iconName); -- expect(metrics.labelDisplay).toBe("inline-flex"); -+ expect(metrics.labelDisplay).toBe("inline"); - expect(Math.abs(metrics.iconWidth - metrics.fontSize)).toBeLessThanOrEqual(1); - expect(Math.abs(metrics.iconHeight - metrics.fontSize)).toBeLessThanOrEqual(1); - expect(metrics.iconColor).toBe(metrics.textColor); -- expect(metrics.iconLeft).toBeLessThan(metrics.textLeft); -- expect(Math.abs(metrics.iconBottom - metrics.textBottom)).toBeLessThanOrEqual(2); -+ expect(metrics.iconBeforeText).toBe(true); -+ expect(metrics.iconVerticalAlign).toBeLessThan(0); - expect(metrics.maskImage).toContain(iconName); - } - -@@ -112,7 +114,10 @@ async function expectNoNavigationFallbackUi(page) { - } - - test("Idea Board uses accordion table ideas and notes", async ({ page }) => { -- const server = await startRepoServer(); -+ const server = await startRepoServer({ -+ gameJourneyCompletionMetricsLegacyDbPath: null, -+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(), -+ }); - const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; - const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; - const previousSupabaseEnv = { -@@ -131,9 +136,16 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - const pageErrors = []; - const consoleErrors = []; - const mutatingApiRequests = []; -+ const createGamePayloads = []; -+ const createGameResponsePromises = []; -+ const gameHubRepositoryRequests = []; - - page.on("response", (response) => { - if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`); -+ const responseUrl = response.url(); -+ if (responseUrl.includes("/api/toolbox/game-hub/repositories/") && responseUrl.includes("/methods/createGame")) { -+ createGameResponsePromises.push(response.json()); -+ } - }); - page.on("requestfailed", (request) => failedRequests.push(`FAILED ${request.url()}`)); - page.on("pageerror", (error) => { -@@ -144,8 +156,15 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text()); - }); - page.on("request", (request) => { -- if (request.url().includes("/api/") && request.method() !== "GET") { -- mutatingApiRequests.push(`${request.method()} ${request.url()}`); -+ const requestUrl = request.url(); -+ if (requestUrl.includes("/api/") && request.method() !== "GET") { -+ mutatingApiRequests.push(`${request.method()} ${requestUrl}`); -+ } -+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { -+ createGamePayloads.push(request.postDataJSON()); -+ } -+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/")) { -+ gameHubRepositoryRequests.push(`${request.method()} ${requestUrl}`); - } - }); - -@@ -184,7 +203,6 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - "Idea", - "Pitch", - "Status", -- "Updated", - "Notes", - "Actions", - ]); -@@ -193,27 +211,26 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await expect(page.locator("[data-idea-board-add-idea-row]")).toHaveCount(1); - await expect(page.locator("[data-idea-board-add-idea]")).toHaveText("Add Idea"); - await expectButtonLeftAligned(page, "[data-idea-board-add-idea]", "[data-idea-board-add-idea-row] > td"); -- await expect(page.locator("[data-idea-board-show-filter] summary")).toHaveText("Show"); -- const captionMetrics = await page.locator(".idea-board-table-caption").evaluate((caption) => { -- const label = caption.querySelector("span"); -- const filter = caption.querySelector("[data-idea-board-show-filter]"); -- const labelRect = label.getBoundingClientRect(); -- const filterRect = filter.getBoundingClientRect(); -- return { -- filterRight: filterRect.right, -- filterTop: filterRect.top, -- labelRight: labelRect.right, -- labelTop: labelRect.top, -- }; -+ await expect(page.locator(".tool-center-panel [data-idea-board-show-filter]")).toHaveCount(0); -+ const statusFilterAccordion = page.locator("aside.tool-group-idea").first().locator("[data-idea-board-section='Status Filter']"); -+ await expect(page.locator("aside.tool-group-idea").first().locator(".accordion-stack > details").first()).toHaveAttribute("data-idea-board-section", "Status Filter"); -+ await expect(statusFilterAccordion.locator("summary")).toHaveText("Status Filter"); -+ await expect(statusFilterAccordion.locator("[data-idea-board-filter-select-all]")).toHaveText("Select All"); -+ await expect(statusFilterAccordion.locator("[data-idea-board-filter-clear-all]")).toHaveText("Clear All"); -+ await expect(statusFilterAccordion.locator("[data-idea-board-status-filter-option]")).toHaveCount(FILTER_STATUS_OPTIONS.length); -+ const statusFilterTheme = await statusFilterAccordion.locator("[data-idea-board-status-filter-option][value='New']").evaluate((input) => ({ -+ accentColor: getComputedStyle(input).accentColor, -+ toolGroupColor: getComputedStyle(input.closest(".control-lab")).getPropertyValue("--tool-group-color").trim(), -+ })); -+ expect(statusFilterTheme).toEqual({ -+ accentColor: "rgb(255, 45, 45)", -+ toolGroupColor: "#ff2d2d", - }); -- expect(captionMetrics.filterRight).toBeGreaterThan(captionMetrics.labelRight); -- expect(Math.abs(captionMetrics.filterTop - captionMetrics.labelTop)).toBeLessThanOrEqual(4); -- await page.locator("[data-idea-board-show-filter] summary").click(); -- await expect(page.locator("[data-idea-board-status-filter-option]")).toHaveCount(6); -+ await expect(statusFilterAccordion.locator(".idea-board-show-filter__option")).toHaveText(FILTER_STATUS_OPTIONS); - const checkedStatuses = await page.locator("[data-idea-board-status-filter-option]:checked").evaluateAll((inputs) => ( - inputs.map((input) => input.value) - )); -- expect(checkedStatuses).toEqual(["New", "Exploring", "Refining", "Ready", "Project"]); -+ expect(checkedStatuses).toEqual(DEFAULT_VISIBLE_STATUS_OPTIONS); - await expect(page.locator("[data-idea-board-status-filter-option][value='Archived']")).not.toBeChecked(); - await expect(page.getByText(/another/i)).toHaveCount(0); - await expect(page.locator("[data-idea-board-notes-chevron]")).toHaveCount(0); -@@ -225,9 +242,21 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await expectIdeaChevron(page, "top-thoughts", "gfs-chevron-down.svg"); - await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(0)).toHaveText("Smartest person wins..."); - await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(1)).toHaveText("Exploring"); -- await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] td").nth(2)).toHaveText("2026-06-20"); - await expect(page.locator("[data-idea-board-notes-count='top-thoughts']")).toHaveText("3 Notes"); - await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Delete"]); -+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ const ideaLabelWrapping = await page.locator("[data-idea-board-idea-row='top-thoughts'] .idea-board-idea-label").evaluate((label) => { -+ const labelStyles = getComputedStyle(label); -+ const textStyles = getComputedStyle(label.querySelector(".idea-board-idea-label__text")); -+ return { -+ overflowWrap: textStyles.overflowWrap, -+ whiteSpace: labelStyles.whiteSpace, -+ }; -+ }); -+ expect(ideaLabelWrapping).toEqual({ -+ overflowWrap: "anywhere", -+ whiteSpace: "normal", -+ }); - - await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] th")).toHaveText("Sky Orchard"); - await expectIdeaChevron(page, "sky-orchard", "gfs-chevron-down.svg"); -@@ -241,6 +270,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await expect(page.locator("[data-idea-board-expanded-row]")).toHaveCount(0); - await page.locator("[data-idea-board-idea-cell='top-thoughts']").click(); - await expect(page.locator("[data-idea-board-expanded-row='top-thoughts']")).toBeVisible(); -+ await expect(page.locator("[data-idea-board-expanded-row='top-thoughts'] > td")).toHaveAttribute("colspan", "5"); - await expectProductionCopy(page); - await expectIdeaChevron(page, "top-thoughts", "gfs-chevron-up.svg"); - await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] + [data-idea-board-expanded-row='top-thoughts']")).toHaveCount(1); -@@ -297,16 +327,8 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - const ideaInputRow = page.locator("[data-idea-board-idea-input-row]").last(); - await expect(ideaInputRow.locator("[data-idea-board-idea-action]")).toHaveText(["Save", "Cancel"]); - await expect(ideaInputRow.locator("[data-idea-board-idea-status-input]")).toHaveCount(1); -- await expect(ideaInputRow.locator("[data-idea-board-idea-status-input] option")).toHaveText([ -- "New", -- "Exploring", -- "Refining", -- "Ready", -- "Project", -- "Archived", -- ]); -- await expect(ideaInputRow.locator("td").nth(2)).toHaveText(/\d{4}-\d{2}-\d{2}/); -- await expect(ideaInputRow.locator("td").nth(3)).toHaveText("0 Notes"); -+ await expect(ideaInputRow.locator("[data-idea-board-idea-status-input] option")).toHaveText(EDITABLE_STATUS_OPTIONS); -+ await expect(ideaInputRow.locator("td").nth(2)).toHaveText("0 Notes"); - await page.locator("[data-idea-board-idea-input]").fill("Lantern Reef"); - await page.locator("[data-idea-board-pitch-input]").fill("Guide light through a reef that rearranges at dusk."); - await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining"); -@@ -325,6 +347,7 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='edit']").click(); - await expect(page.locator("[data-idea-board-idea-input-row] [data-idea-board-idea-action]")).toHaveText(["Save", "Cancel"]); - await expect(page.locator("[data-idea-board-idea-status-input]")).toHaveCount(1); -+ await expect(page.locator("[data-idea-board-idea-status-input] option")).toHaveText(EDITABLE_STATUS_OPTIONS); - await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); - await page.locator("[data-idea-board-idea-action='save']").click(); - await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] td").nth(1)).toHaveText("Ready"); -@@ -336,6 +359,23 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await expect(page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); - await expect(page.locator("[data-idea-board-add-note='lantern-reef']")).toHaveCount(0); - await expect(page.locator("[data-idea-board-notes-table='lantern-reef'] [data-idea-board-note-action]")).toHaveCount(0); -+ expect(createGamePayloads).toHaveLength(1); -+ const [createGameInput] = createGamePayloads[0].args; -+ expect(Object.keys(createGameInput).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]); -+ expect(createGameInput).toMatchObject({ -+ name: "Lantern Reef", -+ purpose: "Game", -+ sourceIdea: { -+ idea: "Lantern Reef", -+ pitch: "Guide light through a reef that rearranges at dusk.", -+ notes: ["Use dusk tide changes as the first Game Hub planning note."], -+ }, -+ status: "Planning", -+ }); -+ const [createGameResponse] = await Promise.all(createGameResponsePromises); -+ const createdProject = createGameResponse?.data?.result; -+ expect(createdProject?.journeyBootstrap?.buckets.map((bucket) => bucket.bucketName)).toEqual(GAME_JOURNEY_BOOTSTRAP_BUCKETS); -+ expect(createdProject?.journeyBootstrap?.buckets.every((bucket) => bucket.noteKey && bucket.itemKey)).toBe(true); - await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='archive']").click(); - await expect(page.locator("[data-idea-board-idea-row='lantern-reef']")).toHaveCount(0); - await page.locator("[data-idea-board-status-filter-option][value='Archived']").check(); -@@ -348,21 +388,184 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - await page.locator("[data-idea-board-idea-row='lantern-reef'] [data-idea-board-idea-action='open-project']").click(); - 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.locator("[data-active-game-name]")).toHaveCount(0); -+ await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef"); -+ await expect(page.locator("aside [data-game-list]")).toHaveCount(0); -+ await expect(page.locator(".tool-center-panel [data-game-list]")).toContainText("Lantern Reef"); -+ await expect(page.locator("[data-source-idea-section]")).toHaveCount(0); -+ await expect(page.locator("[data-game-output-panels]")).toHaveCount(0); -+ await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); -+ await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0); -+ const activeGameToggle = page.locator("[data-game-toggle][data-game-active='true']"); -+ await expect(activeGameToggle).toHaveText("Lantern Reef"); -+ await activeGameToggle.click(); -+ let expandedRows = page.locator("[data-game-expanded-row]"); -+ await expect(expandedRows).toHaveCount(2); -+ await expect(expandedRows.nth(0)).toHaveAttribute("data-game-child-row", "source-idea"); -+ await expect(expandedRows.nth(1)).toHaveAttribute("data-game-child-row", "readiness-output"); -+ let sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); -+ await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea"); -+ await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]); -+ await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ -+ "IdeaLantern Reef", -+ "PitchGuide light through a reef that rearranges at dusk.", -+ "Note 1Use dusk tide changes as the first Game Hub planning note.", -+ ]); -+ await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0); -+ await expect(expandedRows.nth(1).locator("[data-game-child-table='readiness-output'] caption")).toHaveText("Readiness Output"); -+ await page.reload({ waitUntil: "networkidle" }); -+ await expect(page.locator("[data-active-game-name]")).toHaveCount(0); -+ await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef"); -+ await expect(page.locator("[data-source-idea-section]")).toHaveCount(0); -+ await expect(page.locator("[data-game-output-panels]")).toHaveCount(0); -+ await expect(page.locator("[data-game-hub-foundation]")).toHaveCount(0); -+ await activeGameToggle.click(); -+ expandedRows = page.locator("[data-game-expanded-row]"); -+ await expect(expandedRows).toHaveCount(2); -+ sourceIdeaChildTable = expandedRows.nth(0).locator("[data-game-child-table='source-idea']"); -+ await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([ -+ "IdeaLantern Reef", -+ "PitchGuide light through a reef that rearranges at dusk.", -+ "Note 1Use 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 expect(page.getByRole("link", { name: "Open Game Journey" })).toHaveCount(0); -+ const createdGameKey = new URL(page.url()).searchParams.get("game"); -+ await page.goto(`${server.baseUrl}/toolbox/game-journey/index.html?game=${createdGameKey}`, { waitUntil: "networkidle" }); - 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."); -+ const journeyNoteNames = await page.locator("[data-journey-summary-body] [data-journey-note-button]").evaluateAll((buttons) => ( -+ buttons.map((button) => button.textContent.trim()) -+ )); -+ const journeyBucketNames = journeyNoteNames.filter((name) => GAME_JOURNEY_BOOTSTRAP_BUCKETS.includes(name)); -+ expect(journeyBucketNames).toEqual(GAME_JOURNEY_BOOTSTRAP_BUCKETS); - 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 13 Game Journey starter buckets."); - 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-hub/repositories"))).toBe(true); - expect(mutatingApiRequests.some((request) => request.includes("/methods/createGame"))).toBe(true); -+ expect(gameHubRepositoryRequests.some((request) => request.includes("/methods/openGame"))).toBe(true); -+ expect(gameHubRepositoryRequests.some((request) => request.includes("/methods/listGames"))).toBe(true); -+ expect(failedRequests).toEqual([]); -+ expect(pageErrors).toEqual([]); -+ expect(consoleErrors).toEqual([]); -+ } finally { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); -+ await server.close(); -+ } -+}); -+ -+test("Idea Board gates Create Project to Ready ideas and locks converted projects", async ({ page }) => { -+ const server = await startRepoServer({ -+ gameJourneyCompletionMetricsLegacyDbPath: null, -+ gameJourneyCompletionMetricsPostgresClient: createGameJourneyCompletionMetricsPostgresClientStub(), -+ }); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ const previousSupabaseEnv = { -+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL, -+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY, -+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY, -+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL, -+ }; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board"; -+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`; -+ const createGameRequests = []; -+ const failedRequests = []; -+ const pageErrors = []; -+ const consoleErrors = []; -+ -+ page.on("request", (request) => { -+ const requestUrl = request.url(); -+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { -+ createGameRequests.push(request.postDataJSON()); -+ } -+ }); -+ page.on("response", (response) => { -+ if (response.status() >= 400) failedRequests.push(`${response.status()} ${response.url()}`); -+ }); -+ page.on("pageerror", (error) => { -+ const text = error.stack || error.message; -+ if (!isBrowserExtensionNoise(text)) pageErrors.push(error.message); -+ }); -+ page.on("console", (message) => { -+ if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) consoleErrors.push(message.text()); -+ }); -+ -+ try { -+ await page.route("**/api/platform-settings/banner", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { banner: { active: false, message: "", tone: "info" } }, -+ ok: true, -+ }), -+ }); -+ }); -+ await page.route("**/api/toolbox/registry/snapshot", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { -+ activeTools: [], -+ readinessByStatus: {}, -+ tools: [], -+ toolboxContract: {}, -+ }, -+ ok: true, -+ }), -+ }); -+ }); -+ await page.request.post(`${server.baseUrl}/api/session/user`, { -+ data: { userKey: MOCK_DB_KEYS.users.user1 }, -+ }); -+ -+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); -+ await expect(page.locator("[data-idea-board-idea-row='top-thoughts'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='sky-orchard'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='clockwork-courier'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ -+ await page.locator("[data-idea-board-add-idea]").click(); -+ await page.locator("[data-idea-board-idea-input]").fill("Validation Reef"); -+ await page.locator("[data-idea-board-pitch-input]").fill("Verify project gating and read-only conversion."); -+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Refining"); -+ await page.locator("[data-idea-board-idea-action='save']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Refining"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']")).toHaveCount(0); -+ expect(createGameRequests).toEqual([]); -+ -+ await page.locator("[data-idea-board-idea-cell='validation-reef']").click(); -+ await page.locator("[data-idea-board-add-note='validation-reef']").click(); -+ await page.locator("[data-idea-board-note-input]").fill("This note should become read-only project context."); -+ await page.locator("[data-idea-board-note-action='save']").click(); -+ await expect(page.locator("[data-idea-board-notes-count='validation-reef']")).toHaveText("1 Note"); -+ -+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']").click(); -+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); -+ await page.locator("[data-idea-board-idea-action='save']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Ready"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); -+ -+ await page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='create-project']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] td").nth(1)).toHaveText("Project"); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action]")).toHaveText(["Open in Game Hub", "Archive"]); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='edit']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-idea-row='validation-reef'] [data-idea-board-idea-action='delete']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-add-note='validation-reef']")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-notes-table='validation-reef'] [data-idea-board-note-action]")).toHaveCount(0); -+ await expect(page.locator("[data-idea-board-note-input-row]")).toHaveCount(0); -+ expect(createGameRequests).toHaveLength(1); -+ expect(Object.keys(createGameRequests[0].args[0]).sort()).toEqual(["name", "purpose", "sourceIdea", "status"]); -+ - expect(failedRequests).toEqual([]); - expect(pageErrors).toEqual([]); - expect(consoleErrors).toEqual([]); -@@ -374,6 +577,75 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => { - } - }); - -+test("Idea Board guest Create Project redirects to sign in without creating a project", async ({ page }) => { -+ const server = await startRepoServer(); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ const previousSupabaseEnv = { -+ GAMEFOUNDRY_DATABASE_URL: process.env.GAMEFOUNDRY_DATABASE_URL, -+ GAMEFOUNDRY_SUPABASE_ANON_KEY: process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY, -+ GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY: process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY, -+ GAMEFOUNDRY_SUPABASE_URL: process.env.GAMEFOUNDRY_SUPABASE_URL, -+ }; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ process.env.GAMEFOUNDRY_DATABASE_URL = "postgres://idea-board:test@127.0.0.1:5432/idea_board"; -+ process.env.GAMEFOUNDRY_SUPABASE_ANON_KEY = "idea-board-anon-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY = "idea-board-service-role-key"; -+ process.env.GAMEFOUNDRY_SUPABASE_URL = `${server.baseUrl}/fake-supabase`; -+ const createGameRequests = []; -+ -+ page.on("request", (request) => { -+ const requestUrl = request.url(); -+ if (requestUrl.includes("/api/toolbox/game-hub/repositories/") && requestUrl.includes("/methods/createGame")) { -+ createGameRequests.push(requestUrl); -+ } -+ }); -+ -+ try { -+ await page.route("**/api/platform-settings/banner", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { banner: { active: false, message: "", tone: "info" } }, -+ ok: true, -+ }), -+ }); -+ }); -+ await page.route("**/api/toolbox/registry/snapshot", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { -+ activeTools: [], -+ readinessByStatus: {}, -+ tools: [], -+ toolboxContract: {}, -+ }, -+ ok: true, -+ }), -+ }); -+ }); -+ -+ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); -+ await page.locator("[data-idea-board-add-idea]").click(); -+ await page.locator("[data-idea-board-idea-input]").fill("Guest Reef"); -+ await page.locator("[data-idea-board-pitch-input]").fill("Guest cannot create authoritative project keys."); -+ await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); -+ await page.locator("[data-idea-board-idea-action='save']").click(); -+ await expect(page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); -+ -+ await page.locator("[data-idea-board-idea-row='guest-reef'] [data-idea-board-idea-action='create-project']").click(); -+ await page.waitForURL(/\/account\/sign-in\.html$/); -+ expect(createGameRequests).toEqual([]); -+ } finally { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ Object.entries(previousSupabaseEnv).forEach(([key, value]) => restoreEnvValue(key, value)); -+ await server.close(); -+ } -+}); -+ - test("Idea Board remains usable without visible navigation fallback when registry navigation is unavailable", async ({ page }) => { - const server = await startRepoServer(); - const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -diff --git a/tests/playwright/tools/InputMappingV2Tool.spec.mjs b/tests/playwright/tools/InputMappingV2Tool.spec.mjs -index 11022e07b..c1aff52a5 100644 ---- a/tests/playwright/tools/InputMappingV2Tool.spec.mjs -+++ b/tests/playwright/tools/InputMappingV2Tool.spec.mjs -@@ -94,6 +94,13 @@ function collectPageFailures(page) { - async function openRepoPage(page, path, options = {}) { - const server = await startRepoServer(); - const failures = collectPageFailures(page); -+ await page.addInitScript(({ apiUrl, siteUrl }) => { -+ window.GameFoundryPublicConfig = { -+ apiUrl, -+ environmentLabel: "Development Environment", -+ siteUrl, -+ }; -+ }, { apiUrl: `${server.baseUrl}/api`, siteUrl: server.baseUrl }); - if (options.sessionUserKey !== undefined) { - await fetch(`${server.baseUrl}/api/session/user`, { - body: JSON.stringify({ userKey: options.sessionUserKey || "" }), -@@ -328,7 +335,7 @@ test("Toolbox Controls shows game controls only and keeps presets wireframe safe - expect(constants.ENGINE_OWNED_NORMALIZED_INPUTS).toEqual(["action.pause"]); - expect(constants.CONTROL_EVENT_OPTIONS.map((option) => option.field)).toEqual(["eventD", "eventH", "eventU", "eventDC"]); - expect(constants.NORMALIZED_USAGE_LABELS["action.primary"]).toBe("Primary Action"); -- const controlsSource = await page.evaluate(async () => fetch("/toolbox/controls/controls.js").then((response) => response.text())); -+ const controlsSource = await page.evaluate(async () => fetch("/assets/toolbox/controls/js/index.js").then((response) => response.text())); - expect(controlsSource).not.toMatch(/const\s+(CONTROL_EVENT_OPTIONS|GAME_CONTROL_NORMALIZED_INPUTS|NORMALIZED_USAGE_LABELS|COMMON_DEFAULT_GAME_CONTROLS|ENGINE_OWNED_NORMALIZED_INPUTS)\s*=/); - - const primaryRow = page.locator("[data-input-mapping-row]").filter({ hasText: "Primary Action" }).first(); -@@ -1465,7 +1472,7 @@ test("Controls split keeps shared engine input contracts in the account surface" - try { - const sources = await page.evaluate(async () => { - const [controls, accountControls] = await Promise.all([ -- fetch("/toolbox/controls/controls.js").then((response) => response.text()), -+ fetch("/assets/toolbox/controls/js/index.js").then((response) => response.text()), - fetch("/account/user-controls-page.js").then((response) => response.text()), - ]); - return { accountControls, controls }; -diff --git a/tests/playwright/tools/ObjectsTool.spec.mjs b/tests/playwright/tools/ObjectsTool.spec.mjs -index 49c86364a..7e7d967d4 100644 ---- a/tests/playwright/tools/ObjectsTool.spec.mjs -+++ b/tests/playwright/tools/ObjectsTool.spec.mjs -@@ -77,6 +77,13 @@ function collectPageFailures(page) { - async function openObjectsPage(page) { - const server = await startRepoServer(); - const failures = collectPageFailures(page); -+ await page.addInitScript(({ apiUrl, siteUrl }) => { -+ window.GameFoundryPublicConfig = { -+ apiUrl, -+ environmentLabel: "Development Environment", -+ siteUrl, -+ }; -+ }, { apiUrl: `${server.baseUrl}/api`, siteUrl: server.baseUrl }); - await workspaceV2CoverageReporter.start(page); - await page.goto(`${server.baseUrl}/toolbox/objects/index.html`, { waitUntil: "networkidle" }); - return { ...failures, server }; -@@ -85,6 +92,13 @@ async function openObjectsPage(page) { - async function openToolboxPage(page) { - const server = await startRepoServer(); - const failures = collectPageFailures(page); -+ await page.addInitScript(({ apiUrl, siteUrl }) => { -+ window.GameFoundryPublicConfig = { -+ apiUrl, -+ environmentLabel: "Development Environment", -+ siteUrl, -+ }; -+ }, { apiUrl: `${server.baseUrl}/api`, siteUrl: server.baseUrl }); - await workspaceV2CoverageReporter.start(page); - await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); - return { ...failures, server }; -@@ -169,8 +183,11 @@ test("Objects exposes production copy, setup status, and broad table input", asy - expect(constants.OBJECT_TYPE_TEMPLATES.map((template) => template.type)).toEqual(TYPE_OPTIONS); - expect(constants.STARTER_OBJECTS.map((object) => object.name)).toEqual(["Hero", "Projectile", "Wall"]); - expect(constants.CAPABILITY_LABELS.movable).toBe("Can Move"); -- const objectsSource = await page.evaluate(async () => fetch("/toolbox/objects/objects.js").then((response) => response.text())); -- expect(objectsSource).not.toMatch(/const\s+(CAPABILITY_LABELS|OBJECT_TYPE_TEMPLATES|STARTER_OBJECTS)\s*=/); -+ const objectsSource = await page.evaluate(async () => fetch("/assets/toolbox/objects/js/index.js").then((response) => response.text())); -+ expect(objectsSource).toContain('readServerToolConstants("objects")'); -+ expect(objectsSource).toContain('requireServerConstant(constants, "CAPABILITY_LABELS", "objects")'); -+ expect(objectsSource).toContain('requireServerConstant(constants, "OBJECT_TYPE_TEMPLATES", "objects")'); -+ expect(objectsSource).toContain('requireServerConstant(constants, "STARTER_OBJECTS", "objects")'); - await expect(page.getByRole("heading", { level: 3, name: "Object Status" })).toHaveCount(0); - await expect(page.locator("[aria-label='Object status summary']")).toHaveCount(0); - await expect(page.locator("[data-objects-status-summary]")).toHaveCount(0); -diff --git a/tests/playwright/tools/ToolboxAdminMetadataSsot.spec.mjs b/tests/playwright/tools/ToolboxAdminMetadataSsot.spec.mjs -index 3743687e9..b138c4ba5 100644 ---- a/tests/playwright/tools/ToolboxAdminMetadataSsot.spec.mjs -+++ b/tests/playwright/tools/ToolboxAdminMetadataSsot.spec.mjs -@@ -244,10 +244,10 @@ test("Toolbox and Admin Tool Votes share the same DB-backed metadata and plannin - const counts = countByStatus(snapshot.rows); - const visibleCounts = countByStatus(visibleSnapshotRows); - expect(counts).toMatchObject({ -- beta: 7, -- complete: 1, -+ beta: 6, -+ complete: 3, - planned: 31, -- wireframe: 5, -+ wireframe: 4, - deprecated: 2, - }); - const orderedSetupRows = snapshot.rows -diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs -index 12e463488..9e537de3a 100644 ---- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs -+++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs -@@ -4,6 +4,9 @@ import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs - import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; - import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs"; - -+const IDEA_BOARD_EDITABLE_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready"]; -+const IDEA_BOARD_FILTER_STATUS_OPTIONS = ["New", "Exploring", "Refining", "Ready", "Project", "Archived"]; -+ - const TOOL_ROUTE_SMOKE_CASES = [ - { heading: "Game Journey", route: "/tools/game-journey/index.html" }, - { heading: "Idea Board", route: "/tools/idea-board/index.html" }, -@@ -120,8 +123,6 @@ async function expectIdeaChevron(page, ideaId, iconName) { - const cellStyles = getComputedStyle(cell); - const labelStyles = getComputedStyle(label); - const iconStyles = getComputedStyle(icon); -- const textRect = text.getBoundingClientRect(); -- const iconRect = icon.getBoundingClientRect(); - return { - iconName: icon.dataset.ideaBoardChevronIcon, - labelDisplay: labelStyles.display, -@@ -129,21 +130,19 @@ async function expectIdeaChevron(page, ideaId, iconName) { - iconHeight: Number.parseFloat(iconStyles.height), - fontSize: Number.parseFloat(cellStyles.fontSize), - iconColor: iconStyles.backgroundColor, -- iconBottom: iconRect.bottom, -- iconLeft: iconRect.left, -+ iconBeforeText: Boolean(icon.compareDocumentPosition(text) & Node.DOCUMENT_POSITION_FOLLOWING), -+ iconVerticalAlign: Number.parseFloat(iconStyles.verticalAlign), - textColor: cellStyles.color, -- textBottom: textRect.bottom, -- textLeft: textRect.left, - maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage, - }; - }, ideaId); - expect(metrics.iconName).toBe(iconName); -- expect(metrics.labelDisplay).toBe("inline-flex"); -+ expect(metrics.labelDisplay).toBe("inline"); - expect(Math.abs(metrics.iconWidth - metrics.fontSize)).toBeLessThanOrEqual(1); - expect(Math.abs(metrics.iconHeight - metrics.fontSize)).toBeLessThanOrEqual(1); - expect(metrics.iconColor).toBe(metrics.textColor); -- expect(metrics.iconLeft).toBeLessThan(metrics.textLeft); -- expect(Math.abs(metrics.iconBottom - metrics.textBottom)).toBeLessThanOrEqual(2); -+ expect(metrics.iconBeforeText).toBe(true); -+ expect(metrics.iconVerticalAlign).toBeLessThan(0); - expect(metrics.maskImage).toContain(iconName); - } - -@@ -314,14 +313,17 @@ test("Idea Board launches from Toolbox with accordion table notes model", async - sections.map((section) => section.getAttribute("data-idea-board-section")) - )); - expect(ideaBoardSections).toEqual([ -- "Workflow", -+ "Status Filter", - "Status", -+ "Workflow", - "Idea Table", - "Notes Governance", - "Diagnostics", - ]); - await expect(page.locator("[data-idea-board-table]")).toBeVisible(); -- await expect(page.locator("[data-idea-board-table] > thead th[scope='col']")).toHaveText(["Idea", "Pitch", "Status", "Updated", "Notes", "Actions"]); -+ await expect(page.locator("[data-idea-board-table] > thead th[scope='col']")).toHaveText(["Idea", "Pitch", "Status", "Notes", "Actions"]); -+ await expect(page.locator("[data-idea-board-status-filter-option]")).toHaveCount(IDEA_BOARD_FILTER_STATUS_OPTIONS.length); -+ await expect(page.locator(".idea-board-show-filter__option")).toHaveText(IDEA_BOARD_FILTER_STATUS_OPTIONS); - await expect(page.locator("[data-idea-board-idea-row]")).toHaveCount(3); - await expect(page.locator("[data-idea-board-expanded-row]")).toHaveCount(0); - await expect(page.locator("[data-idea-board-add-idea]")).toHaveText("Add Idea"); -@@ -351,7 +353,7 @@ test("Idea Board launches from Toolbox with accordion table notes model", async - await expectExpandedNotesChildIndentation(page, "top-thoughts"); - await expect(page.locator("[data-idea-board-create-project]")).toHaveCount(0); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -- await expect(page.locator("script[src='toolbox/idea-board/index.js']")).toHaveCount(1); -+ await expect(page.locator("script[src='assets/toolbox/idea-board/js/index.js']")).toHaveCount(1); - mutatingApiRequests.length = 0; - await page.locator("[data-idea-board-add-note='top-thoughts']").click(); - await page.locator("[data-idea-board-note-input]").fill("Capture traversal risks before project creation."); -@@ -363,6 +365,7 @@ test("Idea Board launches from Toolbox with accordion table notes model", async - await page.locator("[data-idea-board-add-idea]").click(); - await page.locator("[data-idea-board-idea-input]").fill("Launch Tile"); - await page.locator("[data-idea-board-pitch-input]").fill("Turn a polished board idea into a project."); -+ await expect(page.locator("[data-idea-board-idea-status-input] option")).toHaveText(IDEA_BOARD_EDITABLE_STATUS_OPTIONS); - await page.locator("[data-idea-board-idea-status-input]").selectOption("Ready"); - await page.locator("[data-idea-board-idea-action='save']").click(); - await expect(page.locator("[data-idea-board-idea-row='launch-tile'] [data-idea-board-idea-action]")).toHaveText(["Edit", "Create Project", "Delete"]); -@@ -510,9 +513,9 @@ test("toolbox status kickers, filters, card order, and voting controls work from - - await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", -- "Wireframe (5)", -- "Beta (7)", -- "Complete (1)", -+ "Wireframe (4)", -+ "Beta (6)", -+ "Complete (3)", - "Deprecated (1)", - ]); - await expect(page.locator("[data-toolbox-status-filter='planned']")).toHaveAttribute("aria-pressed", "false"); -@@ -527,9 +530,9 @@ test("toolbox status kickers, filters, card order, and voting controls work from - await page.locator("[data-tools-view='build-path']").click(); - await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", -- "Wireframe (5)", -- "Beta (7)", -- "Complete (1)", -+ "Wireframe (4)", -+ "Beta (6)", -+ "Complete (3)", - "Deprecated (1)", - ]); - await expect(page.locator("[data-toolbox-status-filter='planned']")).toHaveAttribute("aria-pressed", "false"); -@@ -552,7 +555,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from - await page.locator("[data-toolbox-status-filter='deprecated']").click(); - await expect(page.locator("[data-toolbox-status-filter='deprecated']")).toHaveAttribute("aria-pressed", "true"); - -- for (const toolName of ["Assets", "Tags", "Game Configuration", "Game Design", "Game Journey", "Game Hub"]) { -+ for (const toolName of ["Assets", "Tags", "Game Configuration", "Game Design", "Game Journey"]) { - const betaCard = page.locator(`[data-toolbox-tool-card='${toolName}']`); - await expect(betaCard).toBeVisible(); - await expect(betaCard.locator("[data-toolbox-kicker]")).toHaveText("Beta"); -@@ -562,6 +565,16 @@ test("toolbox status kickers, filters, card order, and voting controls work from - ); - } - -+ for (const toolName of ["Colors", "Game Hub", "Idea Board"]) { -+ const completeCard = page.locator(`[data-toolbox-tool-card='${toolName}']`); -+ await expect(completeCard).toBeVisible(); -+ await expect(completeCard.locator("[data-toolbox-kicker]")).toHaveText("Complete"); -+ await expect(completeCard.locator("[data-toolbox-kicker]")).toHaveAttribute( -+ "title", -+ STATUS_HELP_TEXT.complete, -+ ); -+ } -+ - const wireframeCard = page.locator("[data-toolbox-tool-card='Saved Data']"); - await expect(wireframeCard).toBeVisible(); - await expect(wireframeCard.locator("[data-toolbox-kicker]")).toHaveText("Wireframe"); -@@ -816,7 +829,7 @@ test("toolbox status kickers, filters, card order, and voting controls work from - - await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); - await page.locator("[data-tools-view='build-path']").click(); -- await expect(page.locator("[data-toolbox-status-filter='complete']")).toHaveText("Complete (0)"); -+ await expect(page.locator("[data-toolbox-status-filter='complete']")).toHaveText("Complete (2)"); - await expect(page.locator("[data-build-path-tool='Colors']")).toHaveCount(0); - await page.locator("[data-toolbox-status-filter='beta']").click(); - const colorsBuildPathRow = page.locator("[data-build-path-tool='Colors']"); -@@ -1235,19 +1248,19 @@ test("toolbox Build Path status filters support multi-select registry-matched to - - await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (28)", -- "Wireframe (5)", -- "Beta (7)", -- "Complete (1)", -+ "Wireframe (4)", -+ "Beta (6)", -+ "Complete (3)", - "Deprecated (1)", - ]); - await expectActiveFilters(["complete"]); - await expect(page.locator("[data-build-path-tool='Colors']")).toBeVisible(); -- await expectBuildPathChannels(["complete"], 1); -+ await expectBuildPathChannels(["complete"], 3); - await expectBuildPathOrder("Colors", registryById.get("colors").order); - - await page.locator("[data-toolbox-status-filter='planned']").click(); - await expectActiveFilters(["planned", "complete"]); -- await expectBuildPathChannels(["planned", "complete"], 29); -+ await expectBuildPathChannels(["planned", "complete"], 31); - await expect(page.locator("[data-build-path-tool='AI Command Center']")).toBeVisible(); - await expectBuildPathOrder("AI Command Center", registryById.get("ai-assistant").order); - await expectBuildPathOrder("Colors", registryById.get("colors").order); -@@ -1260,19 +1273,19 @@ test("toolbox Build Path status filters support multi-select registry-matched to - - await page.locator("[data-toolbox-status-filter='wireframe']").click(); - await expectActiveFilters(["planned", "wireframe"]); -- await expectBuildPathChannels(["planned", "wireframe"], 33); -+ await expectBuildPathChannels(["planned", "wireframe"], 32); - await expect(page.locator("[data-build-path-tool='Saved Data']")).toBeVisible(); - await expect(page.locator("[data-build-path-tool='Build Game']")).toHaveCount(0); - - await page.locator("[data-toolbox-status-filter='deprecated']").click(); - await expectActiveFilters(["planned", "wireframe", "deprecated"]); -- await expectBuildPathChannels(["planned", "wireframe", "deprecated"], 34); -+ await expectBuildPathChannels(["planned", "wireframe", "deprecated"], 33); - await expect(page.locator("[data-build-path-tool='Build Game']")).toBeVisible(); - await expectBuildPathOrder("Build Game", registryById.get("build-game").order); - - await page.locator("[data-toolbox-status-filter='beta']").click(); - await expectActiveFilters(["planned", "wireframe", "beta", "deprecated"]); -- await expectBuildPathChannels(["planned", "wireframe", "beta", "deprecated"], 41); -+ await expectBuildPathChannels(["planned", "wireframe", "beta", "deprecated"], 39); - - expect(failedRequests).toEqual([]); - expect(pageErrors).toEqual([]); -diff --git a/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -new file mode 100644 -index 000000000..065fbce4a ---- /dev/null -+++ b/tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -@@ -0,0 +1,275 @@ -+import { expect, test } from "@playwright/test"; -+import process from "node:process"; -+import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; -+import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; -+import { clearPlaywrightStorage, installPlaywrightStorageIsolation } from "../../helpers/playwrightStorageIsolation.mjs"; -+import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs"; -+ -+test.beforeEach(async ({ page }) => { -+ await installPlaywrightStorageIsolation(page, { -+ lane: "toolbox-selected-game-status-bar", -+ surface: "shared toolbox selected game status bar", -+ }); -+}); -+ -+test.afterEach(async ({ page }) => { -+ await workspaceV2CoverageReporter.stop(page); -+ await clearPlaywrightStorage(page); -+}); -+ -+test.afterAll(async () => { -+ await workspaceV2CoverageReporter.writeReport(); -+}); -+ -+function restoreEnvValue(key, value) { -+ if (value === undefined) { -+ delete process.env[key]; -+ return; -+ } -+ process.env[key] = value; -+} -+ -+async function openRepoPage(page, pathName, options = {}) { -+ const server = await startRepoServer(); -+ const previousApiUrl = process.env.GAMEFOUNDRY_API_URL; -+ const previousSiteUrl = process.env.GAMEFOUNDRY_SITE_URL; -+ process.env.GAMEFOUNDRY_API_URL = `${server.baseUrl}/api`; -+ process.env.GAMEFOUNDRY_SITE_URL = server.baseUrl; -+ const closeServer = server.close.bind(server); -+ server.close = async () => { -+ restoreEnvValue("GAMEFOUNDRY_API_URL", previousApiUrl); -+ restoreEnvValue("GAMEFOUNDRY_SITE_URL", previousSiteUrl); -+ await closeServer(); -+ }; -+ const failedRequests = []; -+ const pageErrors = []; -+ const consoleErrors = []; -+ -+ page.on("pageerror", (error) => { -+ pageErrors.push(error.message); -+ }); -+ page.on("console", (message) => { -+ if (message.type() === "error") { -+ consoleErrors.push(message.text()); -+ } -+ }); -+ page.on("response", (response) => { -+ if (response.status() >= 400) { -+ failedRequests.push(`${response.status()} ${response.url()}`); -+ } -+ }); -+ page.on("requestfailed", (request) => { -+ failedRequests.push(`FAILED ${request.url()}`); -+ }); -+ -+ if (options.noSelectedGame) { -+ await page.route("**/api/toolbox/game-hub/repositories/*/methods/getActiveGame", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { result: null }, -+ ok: true, -+ }), -+ }); -+ }); -+ } -+ if (options.session) { -+ const userKey = options.session.userKey || MOCK_DB_KEYS.users.user1; -+ await page.route("**/api/session/current", async (route) => { -+ await route.fulfill({ -+ contentType: "application/json", -+ body: JSON.stringify({ -+ data: { -+ authenticated: true, -+ displayName: options.session.displayName || "User 1", -+ roleSlugs: options.session.roleSlugs || ["creator"], -+ userKey, -+ }, -+ ok: true, -+ }), -+ }); -+ }); -+ await page.request.post(`${server.baseUrl}/api/session/user`, { -+ data: { userKey }, -+ }); -+ } -+ -+ await workspaceV2CoverageReporter.start(page); -+ await page.goto(`${server.baseUrl}${pathName}`, { waitUntil: "networkidle" }); -+ return { consoleErrors, failedRequests, pageErrors, server }; -+} -+ -+function expectNoPageFailures(failures) { -+ expect(failures.failedRequests).toEqual([]); -+ expect(failures.pageErrors).toEqual([]); -+ expect(failures.consoleErrors).toEqual([]); -+} -+ -+function creatorSession() { -+ return { -+ displayName: "User 1", -+ roleSlugs: ["creator"], -+ userKey: MOCK_DB_KEYS.users.user1, -+ }; -+} -+ -+async function statusBarSnapshot(page) { -+ return page.locator("[data-toolbox-status-bar]").evaluate((bar) => { -+ const footer = document.querySelector("footer.footer"); -+ const position = getComputedStyle(bar).position; -+ const barBox = bar.getBoundingClientRect(); -+ const footerBox = footer?.getBoundingClientRect(); -+ return { -+ bottomGap: Math.round(window.innerHeight - barBox.bottom), -+ dataset: { ...bar.dataset }, -+ filter: document.body.dataset.toolboxSelectedGameFilter || "", -+ footerFollowsBar: footer ? Boolean(bar.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING) : false, -+ gameId: document.body.dataset.toolboxSelectedGameId || "", -+ gameText: bar.querySelector("[data-toolbox-selected-game]")?.textContent.replace(/\s+/g, " ").trim() || "", -+ messageText: bar.querySelector("[data-toolbox-status-center]")?.textContent.replace(/\s+/g, " ").trim() || "", -+ position, -+ topBeforeFooter: footerBox ? barBox.bottom <= footerBox.top + 1 : false, -+ }; -+ }); -+} -+ -+test("shared toolbox status bar shows selected Game Hub game above the footer", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-design/index.html"); -+ -+ try { -+ const statusBar = page.locator("[data-toolbox-status-bar]"); -+ await expect(statusBar).toBeVisible(); -+ await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); -+ await expect(statusBar).not.toContainText("Environment"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name-label]")).toHaveText("Selected Game Name"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("Demo Game"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose-label]")).toHaveText("Selected Game Purpose"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Game"); -+ await expect(statusBar.locator("[data-toolbox-selected-game]")).not.toContainText("Under Construction"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); -+ await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Game Design mock repository ready."); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-id", "demo-game"); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); -+ -+ const snapshot = await statusBarSnapshot(page); -+ expect(snapshot.footerFollowsBar).toBe(true); -+ expect(snapshot.topBeforeFooter).toBe(true); -+ expect(snapshot.position).not.toBe("fixed"); -+ expect(snapshot.dataset.selectedGameState).toBe("active"); -+ expect(snapshot.dataset.selectedGameRequired).toBe("true"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("shared toolbox status bar center reports save state after Game Hub saves", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html", { session: creatorSession() }); -+ -+ try { -+ await page.getByRole("button", { name: "Add Game" }).click(); -+ const addGameRow = page.locator("[data-game-add-row='input']"); -+ await addGameRow.getByLabel("Game").fill("Status Bar Save"); -+ await addGameRow.getByLabel("Purpose").selectOption("Learning Game"); -+ await addGameRow.getByLabel("Status").selectOption("Ready for Testing"); -+ await addGameRow.getByRole("button", { name: "Save" }).click(); -+ -+ await expect(page.locator("[data-toolbox-status-context-type]")).toHaveText("Save State"); -+ await expect(page.locator("[data-toolbox-status-message]")).toHaveText("Created and opened Status Bar Save."); -+ await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Status Bar Save"); -+ await expect(page.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Learning Game"); -+ await expect(page.locator("[data-toolbox-status-bar]")).not.toContainText("Environment"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("shared toolbox status bar anchors to the bottom in tool display mode", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-design/index.html"); -+ -+ try { -+ await expect(page.locator("[data-toolbox-status-bar]")).toBeVisible(); -+ await page.locator("#toolDisplayMode summary").click(); -+ await expect(page.locator("body")).toHaveClass(/tool-focus-mode/); -+ -+ const snapshot = await statusBarSnapshot(page); -+ expect(snapshot.position).toBe("fixed"); -+ expect(Math.abs(snapshot.bottomGap)).toBeLessThanOrEqual(2); -+ expect(snapshot.gameText).toContain("Demo Game"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("Game Hub owner selection updates the global toolbox status bar", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-hub/index.html"); -+ -+ try { -+ await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Demo Game"); -+ await page.locator("[data-game-toggle='gravity-demo']").click(); -+ await expect(page.locator("[data-toolbox-selected-game-name]")).toHaveText("Gravity Demo"); -+ await expect(page.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Capability Demo"); -+ await expect(page.locator("[data-toolbox-selected-game]")).not.toContainText("Wireframe"); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-id", "gravity-demo"); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "active"); -+ await expect(page.locator("[data-toolbox-status-context-type]")).toHaveText("Warning"); -+ await expect(page.locator("[data-toolbox-status-message]")).toContainText("Sign in to create or update Game Hub projects."); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("non-Idea Board toolbox pages show a creator-safe prompt when no Game Hub game is selected", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/game-design/index.html", { noSelectedGame: true }); -+ -+ try { -+ const statusBar = page.locator("[data-toolbox-status-bar]"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("No game selected"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Game Hub owns game selection"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); -+ await expect(statusBar.locator("[data-toolbox-status-message]")).toHaveText("Select or create a game in Game Hub before using this toolbox page."); -+ await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveText("Select or Create in Game Hub"); -+ await expect(statusBar.locator("[data-toolbox-status-action]")).toHaveAttribute("href", /toolbox\/game-hub\/index\.html$/); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "missing"); -+ await expect(page.locator("body")).not.toHaveAttribute("data-toolbox-selected-game-id", /.+/); -+ -+ const snapshot = await statusBarSnapshot(page); -+ expect(snapshot.dataset.selectedGameRequired).toBe("true"); -+ expect(snapshot.dataset.selectedGameState).toBe("missing"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -+ -+test("Idea Board is excluded from selected-game filtering and does not show the missing-game prompt", async ({ page }) => { -+ const failures = await openRepoPage(page, "/toolbox/idea-board/index.html", { noSelectedGame: true }); -+ -+ try { -+ const statusBar = page.locator("[data-toolbox-status-bar]"); -+ await expect(statusBar).toBeVisible(); -+ await expect(statusBar.locator("[data-toolbox-selected-game-name]")).toHaveText("No game selected"); -+ await expect(statusBar.locator("[data-toolbox-selected-game-purpose]")).toHaveText("Idea Board optional"); -+ await expect(statusBar.locator("[data-toolbox-status-context-type]")).toHaveText("Tool Action"); -+ await expect(statusBar.locator("[data-toolbox-status-message]")).toContainText("Ready to shape ideas and notes."); -+ await expect(statusBar.locator("[data-toolbox-status-message]")).not.toContainText("Select or create a game"); -+ await expect(page.locator("body")).toHaveAttribute("data-toolbox-selected-game-filter", "optional"); -+ await expect(page.locator("body")).not.toHaveClass(/toolbox-selected-game-missing/); -+ -+ const snapshot = await statusBarSnapshot(page); -+ expect(snapshot.dataset.selectedGameRequired).toBe("false"); -+ expect(snapshot.dataset.selectedGameState).toBe("missing"); -+ -+ expectNoPageFailures(failures); -+ } finally { -+ await failures.server.close(); -+ } -+}); -diff --git a/tests/regression/CanonicalRepositoryStructureGuardrail.test.mjs b/tests/regression/CanonicalRepositoryStructureGuardrail.test.mjs -new file mode 100644 -index 000000000..0d5f9fa27 ---- /dev/null -+++ b/tests/regression/CanonicalRepositoryStructureGuardrail.test.mjs -@@ -0,0 +1,43 @@ -+import assert from "node:assert/strict"; -+import test from "node:test"; -+ -+import { -+ auditCanonicalRepositoryStructure, -+ formatCanonicalStructureReport, -+} from "../../scripts/validate-canonical-repository-structure.mjs"; -+ -+test("canonical repository structure guardrail accepts canonical paths and approved legacy exceptions", () => { -+ const result = auditCanonicalRepositoryStructure([ -+ "assets/toolbox/idea-board/js/index.js", -+ "assets/toolbox/idea-board/css/index.css", -+ "assets/js/shared/dom.js", -+ "assets/theme-v2/js/admin-system-health.js", -+ "assets/theme-v2/css/theme.css", -+ "assets/toolbox/assets/js/assets-upload-worker.js", -+ "src/engine/rendering/Renderer.js", -+ "src/engine/ui/baseLayout.css", -+ "tests/regression/CanonicalRepositoryStructureGuardrail.test.mjs", -+ "tests/runtime/V2SessionValidation.test.mjs", -+ ]); -+ -+ assert.equal(result.status, "PASS"); -+ assert.equal(result.findings.length, 0); -+ assert.equal(result.legacy.length, 2); -+}); -+ -+test("canonical repository structure guardrail fails unapproved violation fixture paths", () => { -+ const result = auditCanonicalRepositoryStructure([ -+ "toolbox/new-tool/new-tool.js", -+ "toolbox/new-tool/new-tool.css", -+ "assets/toolbox/new-tool/js/view.js", -+ "src/engine/rootRuntime.js", -+ "src/engine/ui/newPanel.css", -+ "tests/results/generated-result.json", -+ "tests/new-lane/NewLane.test.mjs", -+ ]); -+ -+ assert.equal(result.status, "FAIL"); -+ assert.equal(result.findings.length, 7); -+ assert.match(formatCanonicalStructureReport(result), /New or unapproved toolbox JavaScript sidecar/); -+ assert.match(formatCanonicalStructureReport(result), /Generated test result artifacts must not be tracked/); -+}); -diff --git a/tests/run-tests.mjs b/tests/run-tests.mjs -index 01f866e4a..3f554ca1a 100644 ---- a/tests/run-tests.mjs -+++ b/tests/run-tests.mjs -@@ -7,13 +7,13 @@ run-tests.mjs - import { run as runAIBehaviors } from './ai/AIBehaviors.test.mjs'; - import { run as runAssetLoaderSystem } from './assets/AssetLoaderSystem.test.mjs'; - import { run as runConfigStore } from './config/ConfigStore.test.mjs'; --import { run as runFixedTicker } from './core/FixedTicker.test.mjs'; -+import { run as runFixedTicker } from './engine/core/FixedTicker.test.mjs'; - import { run as runBackgroundImageAndFullscreenBezel } from './core/BackgroundImageAndFullscreenBezel.test.mjs'; - import { run as runEngineFullscreen } from './core/EngineFullscreen.test.mjs'; - import { run as runEngineSceneLifecycle } from './core/EngineSceneLifecycle.test.mjs'; - import { run as runEngineTiming } from './core/EngineTiming.test.mjs'; --import { run as runFrameClock } from './core/FrameClock.test.mjs'; --import { run as runRuntimeMetrics } from './core/RuntimeMetrics.test.mjs'; -+import { run as runFrameClock } from './engine/core/FrameClock.test.mjs'; -+import { run as runRuntimeMetrics } from './engine/core/RuntimeMetrics.test.mjs'; - import { run as runParticleSystem } from './fx/ParticleSystem.test.mjs'; - import { run as runAudioService } from './audio/AudioService.test.mjs'; - import { run as runFinalSystems } from './final/FinalSystems.test.mjs'; -diff --git a/tests/tools/MessagesPlaybackSource.test.mjs b/tests/tools/MessagesPlaybackSource.test.mjs -index 5ee7607c0..a26ecec3e 100644 ---- a/tests/tools/MessagesPlaybackSource.test.mjs -+++ b/tests/tools/MessagesPlaybackSource.test.mjs -@@ -21,7 +21,8 @@ test("Messages wires profile dropdowns through the Text To Speech profile contra - const source = await readFile(new URL("../../toolbox/messages/messages.js", import.meta.url), "utf8"); - - assert.equal(source.includes("../text-to-speech/text2speech.js"), false); -- assert.equal(source.includes("../text-to-speech/tts-profile-store.js"), true); -+ assert.equal(source.includes("../text-to-speech/tts-profile-store.js"), false); -+ assert.equal(source.includes("../../assets/js/shared/tts-profile-store.js"), true); - assert.equal(source.includes("createMessageStudioDefaultTtsProfiles"), false); - assert.equal(source.includes("createMessageStudioTtsProfileOptions"), false); - assert.equal(source.includes("state.voiceProfiles = voicePayload.ttsProfiles || []"), false); -diff --git a/tests/tools/Text2SpeechShell.test.mjs b/tests/tools/Text2SpeechShell.test.mjs -index f857327f9..71553d7f1 100644 ---- a/tests/tools/Text2SpeechShell.test.mjs -+++ b/tests/tools/Text2SpeechShell.test.mjs -@@ -13,13 +13,13 @@ import { - createTtsMessage, - createVoiceProfile, - previewTtsMessage, --} from "../../toolbox/text-to-speech/text2speech.js"; -+} from "../../assets/toolbox/text-to-speech/js/index.js"; - import { - TEXT_TO_SPEECH_PROFILE_STORAGE_KEY, - readSavedTextToSpeechProfiles, - textToSpeechProfilesToMessageOptions, - writeSavedTextToSpeechProfiles, --} from "../../toolbox/text-to-speech/tts-profile-store.js"; -+} from "../../assets/js/shared/tts-profile-store.js"; - - test("Text2Speech message model separates Design and Audio ownership", () => { - const message = createTtsMessage({ text: "Hello", metadata: { tags: ["intro"] } }); -diff --git a/toolbox/assets/index.html b/toolbox/assets/index.html -index 2bd748d76..fdb9a6495 100644 ---- a/toolbox/assets/index.html -+++ b/toolbox/assets/index.html -@@ -153,7 +153,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/colors/index.html b/toolbox/colors/index.html -index a0e9e8276..c3f03f4de 100644 ---- a/toolbox/colors/index.html -+++ b/toolbox/colors/index.html -@@ -376,7 +376,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/colors/palette-api-client.js b/toolbox/colors/palette-api-client.js -deleted file mode 100644 -index 127b5a70b..000000000 ---- a/toolbox/colors/palette-api-client.js -+++ /dev/null -@@ -1,33 +0,0 @@ --import { -- callServerToolFunction, -- createServerRepositoryClient, -- readServerToolConstants, -- requireServerConstant, --} from "../../src/api/server-api-client.js"; -- --const constants = readServerToolConstants("palette"); -- --export const PALETTE_SOURCE_USER = requireServerConstant(constants, "PALETTE_SOURCE_USER", "palette"); --export const PALETTE_TOOL_KEY = requireServerConstant(constants, "PALETTE_TOOL_KEY", "palette"); --export const PALETTE_WORKSPACE_PATH = requireServerConstant(constants, "PALETTE_WORKSPACE_PATH", "palette"); --export const CURATED_PALETTE_COLLECTIONS = Object.freeze(requireServerConstant(constants, "CURATED_PALETTE_COLLECTIONS", "palette")); --export const NUMERIC_VARIANT_COUNTS = Object.freeze(requireServerConstant(constants, "NUMERIC_VARIANT_COUNTS", "palette")); --export const PALETTE_GENERATOR_DEFAULTS = Object.freeze(requireServerConstant(constants, "PALETTE_GENERATOR_DEFAULTS", "palette")); --export const PALETTE_VARIANTS = Object.freeze(requireServerConstant(constants, "PALETTE_VARIANTS", "palette")); --export const PICKER_PREVIEW_DEFAULTS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_DEFAULTS", "palette")); --export const PICKER_PREVIEW_SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "PICKER_PREVIEW_SORT_OPTIONS", "palette")); --export const SIZE_OPTIONS = Object.freeze(requireServerConstant(constants, "SIZE_OPTIONS", "palette")); --export const SORT_OPTIONS = Object.freeze(requireServerConstant(constants, "SORT_OPTIONS", "palette")); --export const SUGGESTED_TAGS = Object.freeze(requireServerConstant(constants, "SUGGESTED_TAGS", "palette")); -- --export function createGameWorkspacePaletteApiRepository(options = {}) { -- return createServerRepositoryClient("palette", options); --} -- --export function normalizePaletteSwatchInput(input) { -- return callServerToolFunction("palette", "normalizePaletteSwatchInput", input); --} -- --export function validatePaletteSwatchInput(input, existingSwatches, options) { -- return callServerToolFunction("palette", "validatePaletteSwatchInput", input, existingSwatches, options); --} -diff --git a/toolbox/controls/index.html b/toolbox/controls/index.html -index 5e23377e8..99a8e8427 100644 ---- a/toolbox/controls/index.html -+++ b/toolbox/controls/index.html -@@ -118,7 +118,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/events/index.html b/toolbox/events/index.html -index c3e20a6a2..7b3f97f72 100644 ---- a/toolbox/events/index.html -+++ b/toolbox/events/index.html -@@ -98,7 +98,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/game-configuration/game-configuration-api-client.js b/toolbox/game-configuration/game-configuration-api-client.js -deleted file mode 100644 -index 649591dd4..000000000 ---- a/toolbox/game-configuration/game-configuration-api-client.js -+++ /dev/null -@@ -1,14 +0,0 @@ --import { -- createServerRepositoryClient, -- readServerToolConstants, -- requireServerConstant, --} from "../../src/api/server-api-client.js"; -- --const constants = readServerToolConstants("game-configuration"); -- --export const GAME_CONFIGURATION_SECTIONS = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_SECTIONS", "game-configuration")); --export const GAME_CONFIGURATION_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_CONFIGURATION_PLAYER_MODES", "game-configuration")); -- --export function createGameConfigurationApiRepository(options = {}) { -- return createServerRepositoryClient("game-configuration", options); --} -diff --git a/toolbox/game-configuration/index.html b/toolbox/game-configuration/index.html -index c62f1afc0..ae41be477 100644 ---- a/toolbox/game-configuration/index.html -+++ b/toolbox/game-configuration/index.html -@@ -168,7 +168,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/game-design/game-design-api-client.js b/toolbox/game-design/game-design-api-client.js -deleted file mode 100644 -index 561906791..000000000 ---- a/toolbox/game-design/game-design-api-client.js -+++ /dev/null -@@ -1,16 +0,0 @@ --import { -- createServerRepositoryClient, -- readServerToolConstants, -- requireServerConstant, --} from "../../src/api/server-api-client.js"; -- --const constants = readServerToolConstants("game-design"); -- --export const GAME_DESIGN_GAME_TYPES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GAME_TYPES", "game-design")); --export const GAME_DESIGN_GENRES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_GENRES", "game-design")); --export const GAME_DESIGN_PLAYER_MODES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAYER_MODES", "game-design")); --export const GAME_DESIGN_PLAY_STYLES = Object.freeze(requireServerConstant(constants, "GAME_DESIGN_PLAY_STYLES", "game-design")); -- --export function createGameDesignApiRepository(options = {}) { -- return createServerRepositoryClient("game-design", options); --} -diff --git a/toolbox/game-design/index.html b/toolbox/game-design/index.html -index 3335515ac..e8fda5433 100644 ---- a/toolbox/game-design/index.html -+++ b/toolbox/game-design/index.html -@@ -195,7 +195,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js -index d2260fe04..50bbe46f1 100644 ---- a/toolbox/game-hub/game-hub.js -+++ b/toolbox/game-hub/game-hub.js -@@ -9,36 +9,22 @@ import { getSessionCurrent } from "../../src/api/session-api-client.js"; - const repository = createGameHubApiRepository(); - - const elements = { -- activeGameName: document.querySelector("[data-active-game-name]"), -- activeGameOwner: document.querySelector("[data-active-game-owner]"), -- activeGamePurpose: document.querySelector("[data-active-game-purpose]"), -- activeGameStatus: document.querySelector("[data-active-game-status]"), -- currentFocus: document.querySelector("[data-current-focus]"), -- currentUserRole: document.querySelector("[data-current-user-role]"), - currentUserRoleInput: document.querySelector("[data-current-user-role-input]"), - deleteOpenGame: document.querySelector("[data-game-delete-active]"), -- form: document.querySelector("[data-game-form]"), - membersTable: document.querySelector("[data-game-members-table]"), -- nameInput: document.querySelector("[data-game-name-input]"), - progressChecklist: document.querySelector("[data-game-progress-checklist]"), - gameList: document.querySelector("[data-game-list]"), -- gameProgress: document.querySelector("[data-game-progress]"), -- gameJourneyLink: document.querySelector("[data-game-journey-link]"), - projectRecordStatus: document.querySelector("[data-project-record-status]"), -- projectRecordsTable: document.querySelector("[data-project-records-table]"), -- purposeInput: document.querySelector("[data-game-purpose-input]"), -- sourceIdeaDisplay: document.querySelector("[data-source-idea-display]"), -- sourceIdeaName: document.querySelector("[data-source-idea-name]"), -- sourceIdeaNotes: document.querySelector("[data-source-idea-notes]"), -- sourceIdeaPitch: document.querySelector("[data-source-idea-pitch]"), -- gameStatus: document.querySelector("[data-game-status]"), -- gameStatusInput: document.querySelector("[data-game-status-input]"), -- publishingProgress: document.querySelector("[data-publishing-progress]"), -- recommendedNextTool: document.querySelectorAll("[data-recommended-next-tool]"), - statusLog: document.querySelector("[data-game-hub-log]"), - tableCounts: document.querySelector("[data-game-table-counts]"), - }; - -+const state = { -+ addingGame: false, -+ editingGameId: "", -+ expandedGameId: "", -+}; -+ - function setText(element, value) { - if (element && typeof element.forEach === "function" && !element.nodeType) { - element.forEach((item) => { -@@ -56,6 +42,15 @@ function setStatusLog(message) { - setText(elements.statusLog, message); - } - -+function notifySelectedGameChanged(activeGame) { -+ window.dispatchEvent(new CustomEvent("gamefoundry:toolbox-selected-game-changed", { -+ detail: { -+ selectedGameId: activeGame?.id || "", -+ source: "game-hub", -+ }, -+ })); -+} -+ - function isRecord(value) { - return Boolean(value && typeof value === "object"); - } -@@ -143,15 +138,11 @@ function setProjectRecordStatus(message) { - - function refreshSaveControls(activeGame = null) { - const saveAllowed = projectRecordsSaveAllowed(); -- [elements.nameInput, elements.purposeInput, elements.gameStatusInput, elements.currentUserRoleInput].forEach((control) => { -+ [elements.currentUserRoleInput].forEach((control) => { - if (control) { - control.disabled = !saveAllowed; - } - }); -- const submitButton = elements.form?.querySelector("button[type='submit']"); -- if (submitButton) { -- submitButton.disabled = !saveAllowed; -- } - if (elements.deleteOpenGame) { - const sourceLinked = isSourceLinkedGame(activeGame); - elements.deleteOpenGame.disabled = !saveAllowed || sourceLinked; -@@ -176,6 +167,18 @@ function ensureProjectRecordsSaveAllowed(action) { - return false; - } - -+function redirectGuestToSignIn() { -+ window.location.href = "account/sign-in.html"; -+} -+ -+function ensureProjectRecordsSaveAllowedForSave() { -+ if (projectRecordsSaveAllowed()) { -+ return true; -+ } -+ redirectGuestToSignIn(); -+ return false; -+} -+ - function populateSelect(select, options) { - if (!select) { - return; -@@ -204,47 +207,335 @@ function currentGameMember(activeGame) { - return activeGameMembers(activeGame).find((member) => member.userKey === userKey) || null; - } - --function createGameButton(game, isActive) { -+function createActionButton(label, action, options = {}) { - const button = document.createElement("button"); -- button.className = isActive ? "btn primary" : "btn"; -+ button.className = options.primary ? "btn btn--compact primary" : "btn btn--compact"; - button.type = "button"; -- button.dataset.gameOpen = game.id; -- if (isActive) { -+ button.dataset.gameAction = action; -+ if (options.gameId) { -+ button.dataset.gameId = options.gameId; -+ } -+ if (options.disabled) { -+ button.disabled = true; -+ } -+ if (options.ariaLabel) { -+ button.setAttribute("aria-label", options.ariaLabel); -+ } -+ button.textContent = label; -+ return button; -+} -+ -+function createGameButton(game) { -+ const button = createActionButton("Edit", "edit-game", { -+ ariaLabel: `Edit ${game.name}`, -+ gameId: game.id, -+ }); -+ return button; -+} -+ -+function createGameListStatus(message, state) { -+ const emptyState = document.createElement("p"); -+ emptyState.className = "status"; -+ emptyState.dataset.gameListStatus = state; -+ emptyState.textContent = message; -+ return emptyState; -+} -+ -+function createCell(value, tagName = "td") { -+ const cell = document.createElement(tagName); -+ cell.textContent = value; -+ return cell; -+} -+ -+function createSelect(options, selectedValue, datasetName, ariaLabel) { -+ const select = document.createElement("select"); -+ select.dataset[datasetName] = "true"; -+ select.setAttribute("aria-label", ariaLabel); -+ options.forEach((option) => { -+ const item = document.createElement("option"); -+ item.value = option; -+ item.textContent = option; -+ select.append(item); -+ }); -+ select.value = options.includes(selectedValue) ? selectedValue : options[0] || ""; -+ return select; -+} -+ -+function createInput(value, datasetName, ariaLabel, options = {}) { -+ const input = document.createElement("input"); -+ input.dataset[datasetName] = "true"; -+ input.type = "text"; -+ input.value = value || ""; -+ input.placeholder = options.placeholder || ""; -+ input.setAttribute("aria-label", ariaLabel); -+ if (options.required) { -+ input.required = true; -+ } -+ if (options.readOnly) { -+ input.readOnly = true; -+ } -+ return input; -+} -+ -+function createGameToggleButton(game, expanded, active) { -+ const button = document.createElement("button"); -+ button.className = active ? "btn btn--compact primary" : "btn btn--compact"; -+ button.type = "button"; -+ button.dataset.gameToggle = game.id; -+ if (active) { - button.dataset.gameActive = "true"; - button.setAttribute("aria-current", "true"); - } -- button.textContent = isActive ? `Open ${game.name} (Active)` : `Open ${game.name}`; -+ button.setAttribute("aria-expanded", String(expanded)); -+ const controlledRows = []; -+ if (hasSourceIdeaDetails(game)) { -+ controlledRows.push(`game-child-source-idea-${game.id}`); -+ } -+ controlledRows.push(`game-child-readiness-output-${game.id}`); -+ button.setAttribute("aria-controls", controlledRows.join(" ")); -+ button.textContent = game.name; - return button; - } - --function renderProjectInformation(activeGame, currentMember, progress) { -- if (!elements.projectRecordsTable) { -- return; -+function gameSourceIdeaDetails(game) { -+ const sourceIdea = isRecord(game?.sourceIdea) ? game.sourceIdea : null; -+ const name = String(sourceIdea?.idea || "").trim(); -+ const pitch = String(sourceIdea?.pitch || "").trim(); -+ const notes = Array.isArray(sourceIdea?.notes) -+ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) -+ : []; -+ return { -+ name, -+ notes, -+ pitch, -+ }; -+} -+ -+function hasSourceIdeaDetails(game) { -+ const sourceIdea = gameSourceIdeaDetails(game); -+ return Boolean(sourceIdea.name || sourceIdea.pitch || sourceIdea.notes.length); -+} -+ -+function renderSourceIdeaChildTable(parent, game) { -+ const sourceIdea = gameSourceIdeaDetails(game); -+ const wrapper = document.createElement("div"); -+ wrapper.className = "table-wrapper"; -+ const table = document.createElement("table"); -+ table.className = "data-table data-table--fixed"; -+ table.dataset.gameChildTable = "source-idea"; -+ table.setAttribute("aria-label", `${game.name} source idea`); -+ table.innerHTML = ""; -+ const body = document.createElement("tbody"); -+ [ -+ ["Idea", sourceIdea.name || "No source idea yet"], -+ ["Pitch", sourceIdea.pitch || "Create a project from Idea Board to see source details."], -+ ].forEach(([label, value]) => { -+ const row = document.createElement("tr"); -+ row.append(createCell(label, "th"), createCell(value)); -+ row.firstElementChild.scope = "row"; -+ body.append(row); -+ }); -+ -+ const notes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."]; -+ notes.forEach((note, index) => { -+ const row = document.createElement("tr"); -+ row.dataset.sourceIdeaNoteRow = String(index + 1); -+ row.append(createCell(`Note ${index + 1}`, "th"), createCell(note)); -+ row.firstElementChild.scope = "row"; -+ body.append(row); -+ }); -+ -+ table.append(body); -+ wrapper.append(table); -+ parent.append(wrapper); -+} -+ -+function renderReadinessOutputChildTable(parent, game, progress, active) { -+ const readiness = active && isRecord(progress) -+ ? progress -+ : { -+ currentFocus: "Open this game to review readiness", -+ gameProgress: `${game.name} identity ready`, -+ gameStatus: game.status || "No status", -+ publishingProgress: "Open this game to review launch progress", -+ recommendedNextTool: "Game Hub", -+ progressChecklist: [], -+ }; -+ const wrapper = document.createElement("div"); -+ wrapper.className = "table-wrapper"; -+ const table = document.createElement("table"); -+ table.className = "data-table data-table--fixed"; -+ table.dataset.gameChildTable = "readiness-output"; -+ table.setAttribute("aria-label", `${game.name} readiness output`); -+ table.innerHTML = ""; -+ const body = document.createElement("tbody"); -+ [ -+ ["Game Status", readiness.gameStatus], -+ ["Game Progress", readiness.gameProgress], -+ ["Launch Progress", readiness.publishingProgress], -+ ["Current Focus", readiness.currentFocus], -+ ["Recommended Next Tool", readiness.recommendedNextTool], -+ ].forEach(([label, value]) => { -+ const row = document.createElement("tr"); -+ row.append(createCell(label, "th"), createCell(value || "Not available")); -+ row.firstElementChild.scope = "row"; -+ body.append(row); -+ }); -+ -+ const checklist = Array.isArray(readiness.progressChecklist) ? readiness.progressChecklist : []; -+ checklist.forEach((item) => { -+ const row = document.createElement("tr"); -+ row.dataset.readinessChecklistRow = item.label || "Checklist"; -+ row.append(createCell(item.label || "Checklist", "th"), createCell(item.status || "Not available")); -+ row.firstElementChild.scope = "row"; -+ body.append(row); -+ }); -+ -+ table.append(body); -+ wrapper.append(table); -+ parent.append(wrapper); -+} -+ -+function renderExpandedGameRow(tbody, game, progress, active) { -+ const childRows = []; -+ if (hasSourceIdeaDetails(game)) { -+ childRows.push({ -+ id: `game-child-source-idea-${game.id}`, -+ render: (parent) => renderSourceIdeaChildTable(parent, game), -+ type: "source-idea", -+ }); - } -+ childRows.push( -+ { -+ id: `game-child-readiness-output-${game.id}`, -+ render: (parent) => renderReadinessOutputChildTable(parent, game, progress, active), -+ type: "readiness-output", -+ }, -+ ); -+ childRows.forEach(({ id, render, type }) => { -+ const row = document.createElement("tr"); -+ row.dataset.gameExpandedRow = game.id; -+ row.dataset.gameChildRow = type; -+ row.id = id; -+ const content = document.createElement("td"); -+ content.colSpan = 4; -+ render(content); -+ row.append(content); -+ tbody.append(row); -+ }); -+} - -- elements.projectRecordsTable.replaceChildren(); -+function renderAddGameRow(tbody) { - const row = document.createElement("tr"); -- [ -- { datasetName: "activeGameName", value: activeGame?.name || "No game open" }, -- { datasetName: "activeGameStatus", value: activeGame?.status || progress?.gameStatus || "No Game" }, -- { datasetName: "activeGamePurpose", value: activeGame?.purpose || "No purpose" }, -- { datasetName: "activeGameOwner", value: activeGame?.ownerDisplayName || "No owner" }, -- { datasetName: "currentUserRole", value: currentMember?.role || "Viewer" }, -- { datasetName: "recommendedNextTool", value: progress?.recommendedNextTool || "Game Hub" }, -- ].forEach(({ datasetName, value }) => { -+ row.dataset.gameAddRow = state.addingGame ? "input" : "button"; -+ -+ if (!state.addingGame) { - const cell = document.createElement("td"); -- cell.dataset[datasetName] = "true"; -- cell.textContent = value; -+ cell.colSpan = 4; -+ cell.append(createActionButton("Add Game", "start-add-game")); - row.append(cell); -- }); -- elements.projectRecordsTable.append(row); -+ tbody.append(row); -+ return; -+ } -+ -+ const nameCell = document.createElement("th"); -+ nameCell.scope = "row"; -+ nameCell.append(createInput("", "gameNameInput", "Game", { -+ placeholder: "Untitled game", -+ required: true, -+ })); -+ -+ const purposeCell = document.createElement("td"); -+ purposeCell.append(createSelect(GAME_HUB_GAME_PURPOSES, "Game", "gamePurposeInput", "Purpose")); -+ -+ const statusCell = document.createElement("td"); -+ statusCell.append(createSelect(GAME_HUB_GAME_STATUSES, "Planning", "gameStatusInput", "Status")); -+ -+ const actions = document.createElement("td"); -+ actions.append( -+ createActionButton("Save", "save-add-game", { primary: true }), -+ createActionButton("Cancel", "cancel-add-game"), -+ ); -+ -+ row.append(nameCell, purposeCell, statusCell, actions); -+ tbody.append(row); -+} -+ -+function renderEditGameRow(tbody, game) { -+ const row = document.createElement("tr"); -+ row.dataset.gameEditRow = game.id; -+ -+ const nameCell = document.createElement("th"); -+ nameCell.scope = "row"; -+ nameCell.append(createInput(game.name, "gameNameInput", "Game", { -+ readOnly: true, -+ })); -+ -+ const purposeCell = document.createElement("td"); -+ purposeCell.append(createSelect(GAME_HUB_GAME_PURPOSES, game.purpose, "gamePurposeInput", "Purpose")); -+ -+ const statusCell = document.createElement("td"); -+ statusCell.append(createSelect(GAME_HUB_GAME_STATUSES, game.status, "gameStatusInput", "Status")); -+ -+ const actions = document.createElement("td"); -+ actions.append( -+ createActionButton("Save", "save-edit-game", { -+ gameId: game.id, -+ primary: true, -+ }), -+ createActionButton("Cancel", "cancel-edit-game", { -+ gameId: game.id, -+ }), -+ ); -+ -+ row.append( -+ nameCell, -+ purposeCell, -+ statusCell, -+ actions, -+ ); -+ tbody.append(row); -+} -+ -+function renderGameParentRow(tbody, game, activeGame, progress) { -+ const expanded = state.expandedGameId === game.id; -+ const active = activeGame?.id === game.id; -+ const editing = state.editingGameId === game.id; -+ -+ if (editing) { -+ renderEditGameRow(tbody, game); -+ return; -+ } -+ -+ const row = document.createElement("tr"); -+ row.dataset.gameRow = game.id; - -+ const nameCell = document.createElement("th"); -+ nameCell.scope = "row"; -+ nameCell.append(createGameToggleButton(game, expanded, active)); -+ row.append( -+ nameCell, -+ createCell(game.purpose || "Game"), -+ createCell(game.status || "No status"), -+ ); -+ -+ const actions = document.createElement("td"); -+ actions.append(createGameButton(game)); -+ row.append(actions); -+ tbody.append(row); -+ -+ if (expanded) { -+ renderExpandedGameRow(tbody, game, progress, active); -+ } -+} -+ -+function renderGameTableStatus() { - setProjectRecordStatus(projectRecordsSaveAllowed() -- ? "Project Information loaded." -- : "Project Information loaded. Sign in to save changes."); -+ ? "Game table loaded." -+ : "Game table loaded. Sign in to save changes."); - } - --function renderGameList() { -+function renderGameList(progress) { - if (!elements.gameList) { - return; - } -@@ -252,40 +543,34 @@ function renderGameList() { - const activeGame = normalizeActiveGame(repository.getActiveGame()); - const gameUserKey = currentGameUserKey(activeGame); - const listResult = repository.listGames(gameUserKey ? { userKey: gameUserKey } : {}); -- const games = Array.isArray(listResult) ? listResult : []; -- if (!Array.isArray(listResult) && !reportRepositoryError(listResult, "Game list")) { -- setStatusLog("Game list is temporarily unavailable. Refresh the page or try again shortly."); -- } - - elements.gameList.replaceChildren(); - -- if (games.length === 0) { -- const emptyState = document.createElement("p"); -- emptyState.className = "status"; -- emptyState.textContent = "No games. Create a game to continue."; -- elements.gameList.append(emptyState); -+ if (!Array.isArray(listResult)) { -+ const message = "Game Hub projects are temporarily unavailable. Refresh the page or try again shortly."; -+ reportRepositoryError(listResult, "Game Hub projects"); -+ setStatusLog(message); -+ elements.gameList.append(createGameListStatus(message, "unavailable")); - return; - } - -- games.forEach((game) => { -- const row = document.createElement("article"); -- row.className = "callout"; -- row.dataset.gameRow = game.id; -- -- const title = document.createElement("h4"); -- title.textContent = game.name; -- -- const meta = document.createElement("p"); -- meta.className = "eyebrow"; -- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`; -- -- const isActive = activeGame?.id === game.id; -- const action = createGameButton(game, isActive); -- -- row.append(title, meta, action); -+ if (listResult.length === 0) { -+ elements.gameList.append(createGameListStatus("No Game Hub projects yet. Add a game to start building.", "empty")); -+ } - -- elements.gameList.append(row); -- }); -+ const wrapper = document.createElement("div"); -+ wrapper.className = "table-wrapper"; -+ const table = document.createElement("table"); -+ table.className = "data-table data-table--fixed"; -+ table.dataset.gameRowsTable = "true"; -+ table.setAttribute("aria-label", "Games"); -+ table.innerHTML = ""; -+ const body = document.createElement("tbody"); -+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame, progress)); -+ renderAddGameRow(body); -+ table.append(body); -+ wrapper.append(table); -+ elements.gameList.append(wrapper); - } - - function renderMembersTable(activeGame) { -@@ -352,29 +637,6 @@ function renderTableCounts() { - }); - } - --function renderSourceIdea(activeGame) { -- const sourceIdea = isRecord(activeGame?.sourceIdea) ? activeGame.sourceIdea : null; -- const name = String(sourceIdea?.idea || "").trim(); -- const pitch = String(sourceIdea?.pitch || "").trim(); -- const notes = Array.isArray(sourceIdea?.notes) -- ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean) -- : []; -- -- setText(elements.sourceIdeaName, name || "No source idea yet"); -- setText(elements.sourceIdeaDisplay, name || "No source idea yet"); -- setText(elements.sourceIdeaPitch, pitch || "Create a project from Idea Board to see source details."); -- -- if (elements.sourceIdeaNotes) { -- elements.sourceIdeaNotes.replaceChildren(); -- const visibleNotes = notes.length ? notes : ["No source notes."]; -- visibleNotes.forEach((note) => { -- const item = document.createElement("li"); -- item.textContent = note; -- elements.sourceIdeaNotes.append(item); -- }); -- } --} -- - function renderChecklist(progress) { - if (!elements.progressChecklist) { - return; -@@ -395,134 +657,199 @@ function renderWorkspace() { - const progress = normalizeProgress(repository.getGameProgress()); - const currentMember = currentGameMember(activeGame); - -- setText(elements.activeGameName, activeGame?.name || "No game open"); -- setText(elements.activeGameOwner, activeGame?.ownerDisplayName || "No owner"); -- setText(elements.activeGamePurpose, activeGame?.purpose || "No purpose"); -- setText(elements.activeGameStatus, activeGame?.status || "No Game"); -- setText(elements.currentUserRole, currentMember?.role || "Viewer"); -- setText(elements.gameStatus, progress.gameStatus); -- setText(elements.gameProgress, progress.gameProgress); -- setText(elements.publishingProgress, progress.publishingProgress); -- setText(elements.currentFocus, progress.currentFocus); -- setText(elements.recommendedNextTool, progress.recommendedNextTool); -- if (elements.purposeInput && activeGame?.purpose) { -- elements.purposeInput.value = activeGame.purpose; -- } -- if (elements.gameStatusInput && activeGame?.status) { -- elements.gameStatusInput.value = activeGame.status; -- } - if (elements.currentUserRoleInput) { - elements.currentUserRoleInput.value = currentMember?.role || "Viewer"; - } -- if (elements.gameJourneyLink) { -- if (activeGame) { -- elements.gameJourneyLink.href = `toolbox/game-journey/index.html?game=${encodeURIComponent(activeGame.id)}`; -- elements.gameJourneyLink.setAttribute("aria-disabled", "false"); -- } else { -- elements.gameJourneyLink.href = "toolbox/game-journey/index.html?game=none"; -- elements.gameJourneyLink.setAttribute("aria-disabled", "true"); -- } -- } - -- renderGameList(); -+ renderGameList(progress); - renderMembersTable(activeGame); - renderTableCounts(); - renderChecklist(progress); -- renderProjectInformation(activeGame, currentMember, progress); -- renderSourceIdea(activeGame); -+ renderGameTableStatus(); - refreshSaveControls(activeGame); -+ notifySelectedGameChanged(activeGame); - } - --elements.form?.addEventListener("submit", (event) => { -- event.preventDefault(); -- if (!ensureProjectRecordsSaveAllowed("create")) { -+function readGameRowFields(row) { -+ return { -+ name: row?.querySelector("[data-game-name-input]")?.value, -+ purpose: row?.querySelector("[data-game-purpose-input]")?.value, -+ status: row?.querySelector("[data-game-status-input]")?.value, -+ }; -+} -+ -+function validateAddedGameFields(row) { -+ const input = readGameRowFields(row); -+ const nameInput = row?.querySelector("[data-game-name-input]"); -+ input.name = String(input.name || "").trim(); -+ if (!input.name) { -+ if (nameInput) { -+ nameInput.setAttribute("aria-invalid", "true"); -+ nameInput.focus(); -+ } -+ setStatusLog("Enter a game name before saving."); -+ return null; -+ } -+ if (nameInput) { -+ nameInput.removeAttribute("aria-invalid"); -+ } -+ return input; -+} -+ -+function saveAddedGame(row) { -+ if (!ensureProjectRecordsSaveAllowedForSave()) { -+ return; -+ } -+ const input = validateAddedGameFields(row); -+ if (!input) { - return; - } -- const activeGame = normalizeActiveGame(repository.getActiveGame()); - const game = repository.createGame({ -- name: elements.nameInput?.value, -- purpose: elements.purposeInput?.value, -- status: elements.gameStatusInput?.value, -+ name: input.name, -+ purpose: input.purpose, -+ status: input.status, - }); - -- if (reportRepositoryError(game, "Create Game") || !isRecord(game) || !String(game.name || "").trim()) { -+ if (reportRepositoryError(game, "Add game") || !isRecord(game) || !String(game.name || "").trim()) { - if (!isRepositoryErrorResult(game)) { -- setStatusLog("Create Game could not be completed. Refresh the page or try again shortly."); -+ setStatusLog("Add game could not be completed. Refresh the page or try again shortly."); - } - renderWorkspace(); - return; - } - -- if (elements.nameInput) { -- elements.nameInput.value = ""; -- } -- -+ state.addingGame = false; -+ state.editingGameId = ""; - setStatusLog(`Created and opened ${game.name}.`); - renderWorkspace(); --}); -- --elements.gameList?.addEventListener("click", (event) => { -- const button = event.target.closest("[data-game-open]"); -+} - -- if (!button) { -+function saveEditedGame(row, gameId) { -+ if (!ensureProjectRecordsSaveAllowedForSave()) { -+ return; -+ } -+ const input = readGameRowFields(row); -+ let game = repository.openGame(gameId); -+ if (reportRepositoryError(game, "Edit game") || !isRecord(game)) { -+ if (!isRepositoryErrorResult(game)) { -+ setStatusLog("Edit game could not be completed. Refresh the page or try again shortly."); -+ } -+ renderWorkspace(); - return; - } - -- const game = repository.openGame(button.dataset.gameOpen); -+ if (input.purpose && input.purpose !== game.purpose) { -+ game = repository.updateGamePurpose(gameId, input.purpose); -+ if (reportRepositoryError(game, "Update game purpose") || !isRecord(game)) { -+ if (!isRepositoryErrorResult(game)) { -+ setStatusLog("Update game purpose could not be completed. Refresh the page or try again shortly."); -+ } -+ renderWorkspace(); -+ return; -+ } -+ } -+ -+ if (input.status && input.status !== game.status) { -+ game = repository.updateGameStatus(gameId, input.status); -+ if (reportRepositoryError(game, "Update game status") || !isRecord(game)) { -+ if (!isRepositoryErrorResult(game)) { -+ setStatusLog("Update game status could not be completed. Refresh the page or try again shortly."); -+ } -+ renderWorkspace(); -+ return; -+ } -+ } -+ -+ state.editingGameId = ""; -+ setStatusLog(`Saved ${game.name}.`); -+ renderWorkspace(); -+} - -- if (game) { -- setStatusLog(`Opened ${game.name}.`); -+elements.gameList?.addEventListener("click", (event) => { -+ const toggle = event.target.closest("[data-game-toggle]"); -+ if (toggle) { -+ const game = repository.openGame(toggle.dataset.gameToggle); -+ if (reportRepositoryError(game, "Select game")) { -+ renderWorkspace(); -+ return; -+ } -+ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle; - renderWorkspace(); -+ return; - } --}); - --elements.deleteOpenGame?.addEventListener("click", () => { -- if (!ensureProjectRecordsSaveAllowed("delete")) { -+ const action = event.target.closest("[data-game-action]"); -+ -+ if (!action) { - return; - } -- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Delete active game"); - -- if (!activeGame) { -- setStatusLog("No game is open for deletion."); -+ if (action.dataset.gameAction === "start-add-game") { -+ state.addingGame = true; -+ state.editingGameId = ""; - renderWorkspace(); - return; - } -- if (isSourceLinkedGame(activeGame)) { -- setStatusLog("Source-linked projects stay connected to Idea Board."); -+ -+ if (action.dataset.gameAction === "cancel-add-game") { -+ state.addingGame = false; -+ setStatusLog("Cancelled game add."); - renderWorkspace(); - return; - } - -- repository.deleteGame(activeGame.id); -- setStatusLog(`Deleted ${activeGame.name}.`); -- renderWorkspace(); --}); -+ if (action.dataset.gameAction === "save-add-game") { -+ saveAddedGame(action.closest("[data-game-add-row='input']")); -+ return; -+ } - --elements.purposeInput?.addEventListener("change", () => { -- if (!ensureProjectRecordsSaveAllowed("update")) { -+ if (action.dataset.gameAction === "edit-game") { -+ const game = repository.openGame(action.dataset.gameId); -+ if (reportRepositoryError(game, "Edit game") || !isRecord(game)) { -+ if (!isRepositoryErrorResult(game)) { -+ setStatusLog("Edit game could not be completed. Refresh the page or try again shortly."); -+ } -+ renderWorkspace(); -+ return; -+ } -+ state.addingGame = false; -+ state.editingGameId = game.id; -+ setStatusLog(`Editing ${game.name}.`); -+ renderWorkspace(); - return; - } -- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Update game purpose"); -- if (!activeGame) { -+ -+ if (action.dataset.gameAction === "cancel-edit-game") { -+ state.editingGameId = ""; -+ setStatusLog("Cancelled game edit."); -+ renderWorkspace(); - return; - } - -- const game = repository.updateGamePurpose(activeGame.id, elements.purposeInput.value); -- setStatusLog(`Updated ${game.name} purpose to ${game.purpose}.`); -- renderWorkspace(); -+ if (action.dataset.gameAction === "save-edit-game") { -+ saveEditedGame(action.closest("[data-game-edit-row]"), action.dataset.gameId); -+ } - }); - --elements.gameStatusInput?.addEventListener("change", () => { -- if (!ensureProjectRecordsSaveAllowed("update")) { -+elements.deleteOpenGame?.addEventListener("click", () => { -+ if (!ensureProjectRecordsSaveAllowed("delete")) { - return; - } -- const activeGame = normalizeActiveGame(repository.getActiveGame(), "Update game status"); -+ const activeGame = normalizeActiveGame(repository.getActiveGame(), "Delete active game"); -+ - if (!activeGame) { -+ setStatusLog("No game is open for deletion."); -+ renderWorkspace(); -+ return; -+ } -+ if (isSourceLinkedGame(activeGame)) { -+ setStatusLog("Source-linked projects stay connected to Idea Board."); -+ renderWorkspace(); - return; - } - -- const game = repository.updateGameStatus(activeGame.id, elements.gameStatusInput.value); -- setStatusLog(`Updated ${game.name} status to ${game.status}.`); -+ repository.deleteGame(activeGame.id); -+ setStatusLog(`Deleted ${activeGame.name}.`); - renderWorkspace(); - }); - -@@ -540,8 +867,6 @@ elements.currentUserRoleInput?.addEventListener("change", () => { - renderWorkspace(); - }); - --populateSelect(elements.purposeInput, GAME_HUB_GAME_PURPOSES); --populateSelect(elements.gameStatusInput, GAME_HUB_GAME_STATUSES); - populateSelect(elements.currentUserRoleInput, GAME_HUB_MEMBER_ROLES); - const requestedGameId = new URL(window.location.href).searchParams.get("game"); - if (requestedGameId) { -diff --git a/toolbox/game-hub/index.html b/toolbox/game-hub/index.html -index ef7b50004..0b34801e9 100644 ---- a/toolbox/game-hub/index.html -+++ b/toolbox/game-hub/index.html -@@ -26,137 +26,17 @@ -
-
IdeaPitchStatusUpdatedNotesActions
Source Idea
ContextDetails
Readiness Output
OutputStatus
GamePurposeStatusActions
-- -- -- -- -- -- -- -- -- -- -- -- -- -- --
--
-- -- -- -- -- -- --
-- Open Games --
--
--
--
-+
-+ -
- -
--

Project Information

--

Review the open project and its source idea.

--
Project Information ready.
--
--
--
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Project Information
ProjectStatusPurposeOwnerRoleNext Tool
Demo GameUnder ConstructionGameNo ownerOwnerGame Configuration
--
-- --
--
--
--
--
--
Source Idea
--

No source idea yet

--
--
-- -- -- -- -- -- --
IdeaNo source idea yet
PitchCreate a project from Idea Board to see source details.
Notes
  • No source notes.
--
--
--
--
--
--
--
Game Foundation
--

Game Progress

--
--
--

Game Status

Under Construction

--

Game Progress

Demo Game identity ready

--

Launch Progress

Publish blocked until configuration and required assets are ready

--
--
--

Current Focus

Complete Game Configuration

--

Recommended Next Tool

Game Configuration

--

Checklist

  • Game identity: Complete
--
--
Game Hub ready.
--
--
--
--
-- Readiness Output --
--
-- -- -- -- -- -- -- -- -- -- --
Readiness output
PathStatusNext Tool
PlanUnder ConstructionGame Configuration
ConfigurePlannedBuild Game
ReleasePlannedPublish
--
--
--
--
-+

Games

-+
Game table ready.
-+
-+
Game Hub ready.
-
- -
-@@ -49,16 +59,6 @@ -

Scan, compare, and update early ideas.

-
- Idea Board table with expandable notes rows --
-- Show --
--
-- -- --
--
--
--
-
-
- -@@ -67,7 +67,6 @@ - - - -- - - - -@@ -103,7 +102,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/messages/messages.js b/toolbox/messages/messages.js -index 6eb31f6f1..cfa68bae4 100644 ---- a/toolbox/messages/messages.js -+++ b/toolbox/messages/messages.js -@@ -1,7 +1,7 @@ - import { - readSavedTextToSpeechProfiles, - textToSpeechProfilesToMessageOptions, --} from "../text-to-speech/tts-profile-store.js"; -+} from "../../assets/js/shared/tts-profile-store.js"; - import { - createEmotionProfile, - createMessage, -diff --git a/toolbox/objects/index.html b/toolbox/objects/index.html -index 63056d723..117409b70 100644 ---- a/toolbox/objects/index.html -+++ b/toolbox/objects/index.html -@@ -157,7 +157,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/objects/objects-api-client.js b/toolbox/objects/objects-api-client.js -deleted file mode 100644 -index 658116553..000000000 ---- a/toolbox/objects/objects-api-client.js -+++ /dev/null -@@ -1,39 +0,0 @@ --import { -- createServerRepositoryClient, -- readServerToolConstants, -- requireServerConstant, --} from "../../src/api/server-api-client.js"; -- --const constants = readServerToolConstants("objects"); -- --function freezeTemplate(template = {}) { -- return Object.freeze({ -- ...template, -- capabilities: Object.freeze(Array.isArray(template.capabilities) ? [...template.capabilities] : []), -- }); --} -- --function freezeStarterObject(object = {}) { -- return Object.freeze({ -- ...object, -- render: Object.freeze({ ...(object.render || {}) }), -- }); --} -- --export const CAPABILITY_LABELS = Object.freeze( -- { ...requireServerConstant(constants, "CAPABILITY_LABELS", "objects") }, --); -- --export const OBJECT_TYPE_TEMPLATES = Object.freeze( -- requireServerConstant(constants, "OBJECT_TYPE_TEMPLATES", "objects").map(freezeTemplate), --); -- --export const OBJECTS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "OBJECTS_TOOL_TABLES", "objects")); -- --export const STARTER_OBJECTS = Object.freeze( -- requireServerConstant(constants, "STARTER_OBJECTS", "objects").map(freezeStarterObject), --); -- --export function createObjectsToolApiRepository(options = {}) { -- return createServerRepositoryClient("objects", options); --} -diff --git a/toolbox/tags/index.html b/toolbox/tags/index.html -index d8b2f89fb..974e8b672 100644 ---- a/toolbox/tags/index.html -+++ b/toolbox/tags/index.html -@@ -116,7 +116,7 @@ -
- - -- -+ - - - -diff --git a/toolbox/tags/tags-api-client.js b/toolbox/tags/tags-api-client.js -deleted file mode 100644 -index 2046bf88e..000000000 ---- a/toolbox/tags/tags-api-client.js -+++ /dev/null -@@ -1,13 +0,0 @@ --import { -- createServerRepositoryClient, -- readServerToolConstants, -- requireServerConstant, --} from "../../src/api/server-api-client.js"; -- --const constants = readServerToolConstants("tags"); -- --export const TAGS_TOOL_TABLES = Object.freeze(requireServerConstant(constants, "TAGS_TOOL_TABLES", "tags")); -- --export function createTagsToolApiRepository(options = {}) { -- return createServerRepositoryClient("tags", options); --} -diff --git a/toolbox/text-to-speech/index.html b/toolbox/text-to-speech/index.html -index 35f232048..66cfb2330 100644 ---- a/toolbox/text-to-speech/index.html -+++ b/toolbox/text-to-speech/index.html -@@ -124,7 +124,7 @@ -
- - -- -+ - - - ++docs_build/dev/reports/PR_26175_ALFA_010-status-bar-replacement-review.md ++docs_build/dev/reports/codex_changed_files.txt ++docs_build/dev/reports/codex_review.diff
IdeaPitchStatusUpdatedNotesActions