From 6aaa49bed35c2d07bd2c3e1aa022b7b435b6f71d Mon Sep 17 00:00:00 2001 From: Jage9 Date: Mon, 9 Mar 2026 01:21:02 -0400 Subject: [PATCH] Add configurable grid branding --- client/index.html | 2 +- client/public/version.js | 2 +- client/src/main.ts | 23 ++++++++++++++++++++++- client/src/network/protocol.ts | 4 ++++ client/src/ui/domBindings.ts | 3 ++- docs/protocol-notes.md | 3 +++ docs/runtime-flow.md | 3 ++- server/app/config.py | 5 +++++ server/app/models.py | 2 ++ server/app/server.py | 21 ++++++++++++++++++++- server/config.example.toml | 4 ++++ server/tests/test_config.py | 17 +++++++++++++++++ 12 files changed, 83 insertions(+), 6 deletions(-) diff --git a/client/index.html b/client/index.html index 6ad308e..3e68a28 100644 --- a/client/index.html +++ b/client/index.html @@ -7,7 +7,7 @@
-

Chat Grid

+

Chat Grid

Login

diff --git a/client/public/version.js b/client/public/version.js index 292448b..de19643 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,6 +1,6 @@ // Maintainer-controlled web client version metadata. window.CHGRID_RELEASE_VERSION = "0.1.0"; -window.CHGRID_BUILD_REVISION = "R346"; +window.CHGRID_BUILD_REVISION = "R347"; window.CHGRID_WEB_VERSION = `${window.CHGRID_RELEASE_VERSION} ${window.CHGRID_BUILD_REVISION}`; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/client/src/main.ts b/client/src/main.ts index 6fcacdd..95ee5e6 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -98,6 +98,7 @@ declare global { } type Dom = { + gridTitle: HTMLElement; connectionStatus: HTMLElement; appVersion: HTMLElement; loginView: HTMLElement; @@ -135,6 +136,7 @@ type Dom = { }; const dom: Dom = { + gridTitle: requiredById('gridTitle'), connectionStatus: requiredById('connectionStatus'), appVersion: requiredById('appVersion'), loginView: requiredById('loginView'), @@ -225,6 +227,9 @@ const APP_VERSION = String( dom.appVersion.textContent = APP_VERSION ? `Another AI experiment with Jage. Version ${APP_VERSION}` : 'Another AI experiment with Jage. Version unknown'; +const DEFAULT_GRID_NAME = 'Chat Grid'; +const DEFAULT_WELCOME_MESSAGE = + 'Welcome to the Chat Grid, your immersive audio playground. Configure your audio, then Log in or register to join the grid.'; const APP_BASE_URL = import.meta.env.BASE_URL || '/'; /** Resolves an app-relative path against the configured Vite base path. */ function withBase(path: string): string { @@ -259,6 +264,8 @@ let lastFocusedElement: Element | null = null; let lastAnnouncementText = ''; let lastAnnouncementAt = 0; let outputMode = settings.loadOutputMode(); +let activeGridName = DEFAULT_GRID_NAME; +let activeWelcomeMessage = DEFAULT_WELCOME_MESSAGE; const messageBuffer: string[] = []; let messageCursor = -1; const radioRuntime = new RadioStationRuntime(audio, getItemSpatialConfig); @@ -360,6 +367,17 @@ void loadHelp(); void itemBehaviorRegistry.initialize(); void loadChangelog(); +function applyGridBranding(gridName: string | null | undefined, welcomeMessage: string | null | undefined): void { + const nextGridName = String(gridName ?? '').trim() || DEFAULT_GRID_NAME; + const nextWelcomeMessage = String(welcomeMessage ?? '').trim() || DEFAULT_WELCOME_MESSAGE; + activeGridName = nextGridName; + activeWelcomeMessage = nextWelcomeMessage; + document.title = nextGridName; + dom.gridTitle.textContent = nextGridName; + dom.focusGridButton.textContent = nextGridName; + dom.canvas.setAttribute('aria-label', `${nextGridName}, press question mark for help.`); +} + /** Fetches a required DOM element and casts it to the requested element type. */ function requiredById(id: string): T { const found = document.getElementById(id); @@ -405,6 +423,7 @@ const adminController = createAdminController({ signalingSend: (message) => signaling.send(message), announceMenuEntry, updateStatus, + getGridName: () => activeGridName, sfxUiBlip: () => audio.sfxUiBlip(), sfxUiCancel: () => audio.sfxUiCancel(), applyTextInputEdit, @@ -1345,6 +1364,7 @@ function sendAuthRequest(): void { /** Handles server auth-required prompts prior to world welcome. */ function handleAuthRequired(message: Extract): void { + applyGridBranding(message.gridName, message.welcomeMessage); authController.handleAuthRequired(message); } @@ -1563,6 +1583,7 @@ async function onSignalingMessage(message: IncomingMessage): Promise { let connectedAnnouncement: string | null = null; let playSelfLoginSound = false; if (message.type === 'welcome') { + applyGridBranding(message.serverInfo?.gridName, message.serverInfo?.welcomeMessage); const uiAdminActions = (message.uiDefinitions as { adminMenu?: { actions?: Array<{ id: string; label: string }> } } | undefined)?.adminMenu?.actions ?? message.auth?.adminMenuActions; @@ -2676,5 +2697,5 @@ updateDeviceSummary(); setConnectionStatus( isVersionReloadedSession() ? 'Client updated, please reconnect.' - : 'Welcome to the Chat Grid, your immersive audio playground. Configure your audio, then Log in or register to join the grid.', + : activeWelcomeMessage, ); diff --git a/client/src/network/protocol.ts b/client/src/network/protocol.ts index c79b27d..3ba229b 100644 --- a/client/src/network/protocol.ts +++ b/client/src/network/protocol.ts @@ -56,6 +56,8 @@ export const welcomeMessageSchema = z.object({ .object({ instanceId: z.string(), version: z.string().optional(), + gridName: z.string().optional(), + welcomeMessage: z.string().optional(), }) .optional(), auth: z @@ -136,6 +138,8 @@ export const welcomeMessageSchema = z.object({ export const authRequiredSchema = z.object({ type: z.literal('auth_required'), message: z.string(), + gridName: z.string().optional(), + welcomeMessage: z.string().optional(), authPolicy: z .object({ usernameMinLength: z.number().int().positive(), diff --git a/client/src/ui/domBindings.ts b/client/src/ui/domBindings.ts index fbee96d..8533da1 100644 --- a/client/src/ui/domBindings.ts +++ b/client/src/ui/domBindings.ts @@ -24,6 +24,7 @@ type UiBindingsDeps = { openSettings: () => void; closeSettings: () => void; updateStatus: (message: string) => void; + getGridName: () => string; sfxUiBlip: () => void; setupLocalMedia: (audioDeviceId: string) => Promise; setPreferredInput: (id: string, name: string) => void; @@ -46,7 +47,7 @@ export function setupUiHandlers(deps: UiBindingsDeps): void { deps.dom.focusGridButton.addEventListener('click', () => { deps.dom.canvas.focus(); - deps.updateStatus('Chat Grid focused.'); + deps.updateStatus(`${deps.getGridName()} focused.`); deps.sfxUiBlip(); }); diff --git a/docs/protocol-notes.md b/docs/protocol-notes.md index 1d260bc..d1d2dd6 100644 --- a/docs/protocol-notes.md +++ b/docs/protocol-notes.md @@ -95,6 +95,7 @@ This is a behavior guide for packet semantics beyond raw schemas. - `permissions` - `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`) - `auth_required.authPolicy`: server auth limits advertised before login/register submit. +- `auth_required.gridName` / `auth_required.welcomeMessage`: server-owned pre-login branding values. - `auth_result.authPolicy`: server auth limits echoed on auth success/failure responses. - `auth_result.sessionToken` is used by the client to call the instance-scoped HTTP endpoint `GET auth/session/set` (`Authorization: Bearer `, `X-Chgrid-Auth-Client: 1`) so the server can issue an instance-scoped `HttpOnly` session cookie. - `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing. @@ -104,6 +105,8 @@ This is a behavior guide for packet semantics beyond raw schemas. - `welcome.serverInfo`: server process identity/version metadata: - `instanceId`: unique id generated at server startup - `version`: server package version (or `unknown` fallback) + - `gridName`: server-owned user-facing grid name + - `welcomeMessage`: server-owned pre-login welcome string - `welcome.uiDefinitions`: server-provided item UI definitions: - `itemTypeOrder`: add-item menu order - `itemTypes[].tooltip`: item-level tooltip/help text diff --git a/docs/runtime-flow.md b/docs/runtime-flow.md index 9fa6c0b..69f0424 100644 --- a/docs/runtime-flow.md +++ b/docs/runtime-flow.md @@ -7,6 +7,7 @@ 3. Client connects signaling websocket from the configured app origin. 4. Server accepts the socket only on the configured instance websocket path and when the browser `Origin` matches `CHGRID_HOST_ORIGIN`, then attempts cookie-based session resume from the instance-scoped websocket handshake cookie. 5. If resume does not authenticate, server sends `auth_required`. + - includes `gridName` and `welcomeMessage` for pre-login branding. - includes `authPolicy` limits for username/password. 6. Client sends `auth_login` or `auth_register` (or explicit `auth_resume` if provided by caller). 7. Server sends `auth_result`. @@ -19,7 +20,7 @@ - applies `welcome.worldConfig.movementTickMs` as movement pacing guidance - applies `welcome.worldConfig.movementMaxStepsPerTick` for movement-rate parity - uses `welcome.player` as authoritative starting position (restored from server-side account state when available) - - records `welcome.serverInfo` (`instanceId`, `version`) for restart detection + - records `welcome.serverInfo` (`instanceId`, `version`, `gridName`, `welcomeMessage`) for restart detection and client branding - if `welcome.serverInfo.version` differs from running client version, auto-reloads the page - applies `welcome.uiDefinitions` for item menus/properties/options, server-backed command metadata, item-management metadata, and admin menu labels/order - sends initial `update_position` echo from server-assigned starting tile diff --git a/server/app/config.py b/server/app/config.py index 32402fb..aeb0793 100644 --- a/server/app/config.py +++ b/server/app/config.py @@ -14,6 +14,11 @@ class ServerConfigSection(BaseModel): bind_ip: str = "127.0.0.1" port: int = 8765 base_path: str = "/" + grid_name: str = "Chat Grid" + welcome_message: str = ( + "Welcome to the Chat Grid, your immersive audio playground. " + "Configure your audio, then Log in or register to join the grid." + ) class NetworkConfigSection(BaseModel): diff --git a/server/app/models.py b/server/app/models.py index 6316d56..da97761 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -242,6 +242,8 @@ class AuthRequiredPacket(BasePacket): type: Literal["auth_required"] message: str authPolicy: dict | None = None + gridName: str | None = None + welcomeMessage: str | None = None class AuthResultPacket(BasePacket): diff --git a/server/app/server.py b/server/app/server.py index 5f04bcb..99e454f 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -165,6 +165,11 @@ class SignalingServer: state_save_max_delay_ms: int = 1000, host_origin: str | None = None, base_path: str = "/", + grid_name: str = "Chat Grid", + welcome_message: str = ( + "Welcome to the Chat Grid, your immersive audio playground. " + "Configure your audio, then Log in or register to join the grid." + ), ): """Initialize runtime state, TLS context, and item service.""" @@ -194,6 +199,11 @@ class SignalingServer: self.server_version = self._resolve_server_version() self.host_origin = normalize_origin(host_origin, field_name="host origin") if host_origin else None self.base_path = self._normalize_base_path(base_path) + self.grid_name = str(grid_name).strip() or "Chat Grid" + self.welcome_message = ( + str(welcome_message).strip() + or "Welcome to the Chat Grid, your immersive audio playground. Configure your audio, then Log in or register to join the grid." + ) self.auth_session_cookie_name = self._session_cookie_name_for_base_path(self.base_path) self.websocket_path = self._base_path_join(WEBSOCKET_PATH) self.auth_session_cookie_set_path = self._base_path_join(AUTH_SESSION_COOKIE_SET_PATH) @@ -1493,6 +1503,8 @@ class SignalingServer: type="auth_required", message="Authentication required.", authPolicy=self._auth_policy(), + gridName=self.grid_name, + welcomeMessage=self.welcome_message, ), ) async for raw_message in websocket: @@ -1549,7 +1561,12 @@ class SignalingServer: "movementMaxStepsPerTick": self.movement_max_steps_per_tick, }, uiDefinitions=self._build_ui_definitions(client), - serverInfo={"instanceId": self.instance_id, "version": self.server_version}, + serverInfo={ + "instanceId": self.instance_id, + "version": self.server_version, + "gridName": self.grid_name, + "welcomeMessage": self.welcome_message, + }, auth={ "authenticated": client.authenticated, "userId": client.user_id, @@ -3311,5 +3328,7 @@ def run() -> None: state_save_max_delay_ms=config.storage.state_save_max_delay_ms, host_origin=host_origin, base_path=config.server.base_path, + grid_name=config.server.grid_name, + welcome_message=config.server.welcome_message, ) asyncio.run(server.start()) diff --git a/server/config.example.toml b/server/config.example.toml index c82259f..ea06720 100644 --- a/server/config.example.toml +++ b/server/config.example.toml @@ -5,6 +5,10 @@ bind_ip = "127.0.0.1" port = 8765 # Public base path for this grid instance. Examples: "/", "/chgrid/", "/ttgrid/". base_path = "/" +# User-facing grid name shown in the web client. +grid_name = "Chat Grid" +# User-facing pre-connect welcome text shown on the home screen. +welcome_message = "Welcome to the Chat Grid, your immersive audio playground. Configure your audio, then Log in or register to join the grid." [network] # Maximum inbound websocket message size in bytes. diff --git a/server/tests/test_config.py b/server/tests/test_config.py index 6e2c269..206d047 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -59,3 +59,20 @@ base_path = "/ttgrid/" ) cfg = load_config(config_path) assert cfg.server.base_path == "/ttgrid/" + + +def test_load_config_reads_grid_name_and_welcome_message(tmp_path: Path) -> None: + config_path = tmp_path / "config.toml" + config_path.write_text( + """ +[network] +allow_insecure_ws = true + +[server] +grid_name = "TT Grid" +welcome_message = "Welcome to TT Grid." +""".strip() + ) + cfg = load_config(config_path) + assert cfg.server.grid_name == "TT Grid" + assert cfg.server.welcome_message == "Welcome to TT Grid."