Split client and server revision tracking
This commit is contained in:
@@ -38,7 +38,8 @@
|
|||||||
- Runtime/protocol behavior changes: update `docs/runtime-flow.md` and/or `docs/protocol-notes.md`.
|
- Runtime/protocol behavior changes: update `docs/runtime-flow.md` and/or `docs/protocol-notes.md`.
|
||||||
|
|
||||||
## Versioning & Configuration
|
## Versioning & Configuration
|
||||||
- Bump `client/public/version.js` on every user-visible change using release version + revision metadata (`CHGRID_RELEASE_VERSION`, `CHGRID_BUILD_REVISION`, and derived `CHGRID_WEB_VERSION`, for example `0.1.0` + `R340`).
|
- Bump `client/public/version.js` on every user-visible client change using shared release version + client revision metadata (`CHGRID_RELEASE_VERSION` and `CHGRID_CLIENT_REVISION`, for example `0.1.1` + `R350`).
|
||||||
|
- Keep the server-only revision in `server/app/version.py`; server revisions do not require a client version bump unless browser code/assets changed.
|
||||||
- Commit each completed logical change; include the version bump in that same commit when client behavior changes.
|
- Commit each completed logical change; include the version bump in that same commit when client behavior changes.
|
||||||
- Docs-only changes do not require a version bump unless explicitly requested.
|
- Docs-only changes do not require a version bump unless explicitly requested.
|
||||||
- Do not duplicate version constants elsewhere in client code.
|
- Do not duplicate version constants elsewhere in client code.
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// Maintainer-controlled web client version metadata.
|
// Maintainer-controlled web client version metadata.
|
||||||
window.CHGRID_RELEASE_VERSION = "0.1.0";
|
window.CHGRID_RELEASE_VERSION = "0.1.1";
|
||||||
window.CHGRID_BUILD_REVISION = "R349";
|
window.CHGRID_CLIENT_REVISION = "R350";
|
||||||
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.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -92,8 +92,7 @@ const AUTH_POLICY_STORAGE_KEY = 'chgridAuthPolicy';
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CHGRID_RELEASE_VERSION?: string;
|
CHGRID_RELEASE_VERSION?: string;
|
||||||
CHGRID_BUILD_REVISION?: string;
|
CHGRID_CLIENT_REVISION?: string;
|
||||||
CHGRID_WEB_VERSION?: string;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,13 +218,10 @@ function announceMenuEntry(title: string, firstOption: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const APP_RELEASE_VERSION = String(window.CHGRID_RELEASE_VERSION ?? '').trim();
|
const APP_RELEASE_VERSION = String(window.CHGRID_RELEASE_VERSION ?? '').trim();
|
||||||
const APP_BUILD_REVISION = String(window.CHGRID_BUILD_REVISION ?? '').trim();
|
const APP_CLIENT_REVISION = String(window.CHGRID_CLIENT_REVISION ?? '').trim();
|
||||||
const APP_VERSION = String(
|
const APP_DISPLAY_VERSION = [APP_RELEASE_VERSION, APP_CLIENT_REVISION].filter((value) => value.length > 0).join(' ').trim();
|
||||||
window.CHGRID_WEB_VERSION ??
|
dom.appVersion.textContent = APP_DISPLAY_VERSION
|
||||||
[APP_RELEASE_VERSION, APP_BUILD_REVISION].filter((value) => value.length > 0).join(' ')
|
? `Another AI experiment with Jage. Version ${APP_DISPLAY_VERSION}`
|
||||||
).trim();
|
|
||||||
dom.appVersion.textContent = APP_VERSION
|
|
||||||
? `Another AI experiment with Jage. Version ${APP_VERSION}`
|
|
||||||
: 'Another AI experiment with Jage. Version unknown';
|
: 'Another AI experiment with Jage. Version unknown';
|
||||||
const DEFAULT_GRID_NAME = 'Chat Grid';
|
const DEFAULT_GRID_NAME = 'Chat Grid';
|
||||||
const DEFAULT_WELCOME_MESSAGE =
|
const DEFAULT_WELCOME_MESSAGE =
|
||||||
@@ -803,9 +799,9 @@ function handleSignalingStatus(message: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Performs cache-busted navigation so the browser loads the newest client bundle. */
|
/** Performs cache-busted navigation so the browser loads the newest client bundle. */
|
||||||
function reloadClientForVersion(version: string): void {
|
function reloadClientForVersion(versionToken: string): void {
|
||||||
const nextUrl = new URL(window.location.href);
|
const nextUrl = new URL(window.location.href);
|
||||||
nextUrl.searchParams.set('v', version || 'unknown');
|
nextUrl.searchParams.set('v', versionToken || 'unknown');
|
||||||
nextUrl.searchParams.set('t', String(Date.now()));
|
nextUrl.searchParams.set('t', String(Date.now()));
|
||||||
window.location.replace(nextUrl.toString());
|
window.location.replace(nextUrl.toString());
|
||||||
}
|
}
|
||||||
@@ -1385,6 +1381,18 @@ function sendAuthRequest(): void {
|
|||||||
/** Handles server auth-required prompts prior to world welcome. */
|
/** Handles server auth-required prompts prior to world welcome. */
|
||||||
function handleAuthRequired(message: Extract<IncomingMessage, { type: 'auth_required' }>): void {
|
function handleAuthRequired(message: Extract<IncomingMessage, { type: 'auth_required' }>): void {
|
||||||
applyGridBranding(message.gridName, message.welcomeMessage);
|
applyGridBranding(message.gridName, message.welcomeMessage);
|
||||||
|
const expectedClientRevision = String(message.expectedClientRevision ?? '').trim();
|
||||||
|
if (!reloadScheduledForVersionMismatch && expectedClientRevision && expectedClientRevision !== APP_CLIENT_REVISION) {
|
||||||
|
reloadScheduledForVersionMismatch = true;
|
||||||
|
const serverVersion = String(message.serverVersion ?? '').trim() || 'unknown';
|
||||||
|
pushChatMessage(
|
||||||
|
`Server ${serverVersion} expects client ${expectedClientRevision}. Reloading client...`,
|
||||||
|
);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
reloadClientForVersion(expectedClientRevision);
|
||||||
|
}, 50);
|
||||||
|
return;
|
||||||
|
}
|
||||||
authController.handleAuthRequired(message);
|
authController.handleAuthRequired(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1609,22 +1617,21 @@ async function onSignalingMessage(message: IncomingMessage): Promise<void> {
|
|||||||
message.auth?.adminMenuActions;
|
message.auth?.adminMenuActions;
|
||||||
authController.applyWelcomeAuth(message.auth, uiAdminActions);
|
authController.applyWelcomeAuth(message.auth, uiAdminActions);
|
||||||
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
|
const incomingInstanceId = String(message.serverInfo?.instanceId ?? '').trim() || null;
|
||||||
const incomingVersion = String(message.serverInfo?.version ?? '').trim() || 'unknown';
|
const incomingServerVersion = String(message.serverInfo?.serverVersion ?? '').trim() || 'unknown';
|
||||||
|
const expectedClientRevision = String(message.serverInfo?.expectedClientRevision ?? '').trim();
|
||||||
connectedAnnouncement = reconnectInFlight
|
connectedAnnouncement = reconnectInFlight
|
||||||
? `Reconnected to server. Version ${incomingVersion}.`
|
? `Reconnected to server. Version ${incomingServerVersion}.`
|
||||||
: `Connected to server. Version ${incomingVersion}.`;
|
: `Connected to server. Version ${incomingServerVersion}.`;
|
||||||
playSelfLoginSound = !reconnectInFlight;
|
playSelfLoginSound = !reconnectInFlight;
|
||||||
if (
|
if (
|
||||||
!reloadScheduledForVersionMismatch &&
|
!reloadScheduledForVersionMismatch &&
|
||||||
APP_VERSION &&
|
expectedClientRevision &&
|
||||||
incomingVersion &&
|
expectedClientRevision !== APP_CLIENT_REVISION
|
||||||
incomingVersion !== 'unknown' &&
|
|
||||||
incomingVersion !== APP_VERSION
|
|
||||||
) {
|
) {
|
||||||
reloadScheduledForVersionMismatch = true;
|
reloadScheduledForVersionMismatch = true;
|
||||||
pushChatMessage(`Server version ${incomingVersion} detected. Reloading client...`);
|
pushChatMessage(`Server expects client ${expectedClientRevision}. Reloading client...`);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
reloadClientForVersion(incomingVersion);
|
reloadClientForVersion(expectedClientRevision);
|
||||||
}, 50);
|
}, 50);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export const welcomeMessageSchema = z.object({
|
|||||||
serverInfo: z
|
serverInfo: z
|
||||||
.object({
|
.object({
|
||||||
instanceId: z.string(),
|
instanceId: z.string(),
|
||||||
version: z.string().optional(),
|
releaseVersion: z.string().optional(),
|
||||||
|
serverVersion: z.string().optional(),
|
||||||
|
expectedClientRevision: z.string().optional(),
|
||||||
gridName: z.string().optional(),
|
gridName: z.string().optional(),
|
||||||
welcomeMessage: z.string().optional(),
|
welcomeMessage: z.string().optional(),
|
||||||
})
|
})
|
||||||
@@ -140,6 +142,9 @@ export const authRequiredSchema = z.object({
|
|||||||
message: z.string(),
|
message: z.string(),
|
||||||
gridName: z.string().optional(),
|
gridName: z.string().optional(),
|
||||||
welcomeMessage: z.string().optional(),
|
welcomeMessage: z.string().optional(),
|
||||||
|
releaseVersion: z.string().optional(),
|
||||||
|
expectedClientRevision: z.string().optional(),
|
||||||
|
serverVersion: z.string().optional(),
|
||||||
authPolicy: z
|
authPolicy: z
|
||||||
.object({
|
.object({
|
||||||
usernameMinLength: z.number().int().positive(),
|
usernameMinLength: z.number().int().positive(),
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
## Server -> Client
|
## Server -> Client
|
||||||
|
|
||||||
- `auth_required`: authentication challenge after websocket connect.
|
- `auth_required`: authentication challenge after websocket connect.
|
||||||
|
- includes `gridName`, `welcomeMessage`, `serverVersion`, and `expectedClientRevision`.
|
||||||
- `auth_result`: auth success/failure and session/account metadata.
|
- `auth_result`: auth success/failure and session/account metadata.
|
||||||
- `auth_permissions`: server-pushed live role/permission refresh for current session.
|
- `auth_permissions`: server-pushed live role/permission refresh for current session.
|
||||||
- `admin_roles_list`: role list response payload.
|
- `admin_roles_list`: role list response payload.
|
||||||
@@ -96,6 +97,8 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`)
|
- `policy` (`usernameMinLength`, `usernameMaxLength`, `passwordMinLength`, `passwordMaxLength`)
|
||||||
- `auth_required.authPolicy`: server auth limits advertised before login/register submit.
|
- `auth_required.authPolicy`: server auth limits advertised before login/register submit.
|
||||||
- `auth_required.gridName` / `auth_required.welcomeMessage`: server-owned pre-login branding values.
|
- `auth_required.gridName` / `auth_required.welcomeMessage`: server-owned pre-login branding values.
|
||||||
|
- `auth_required.serverVersion`: server diagnostics version text shown in connect/reconnect messaging.
|
||||||
|
- `auth_required.expectedClientRevision`: authoritative browser asset revision required by this server instance.
|
||||||
- `auth_result.authPolicy`: server auth limits echoed on auth success/failure responses.
|
- `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 <base_path>auth/session/set` (`Authorization: Bearer <sessionToken>`, `X-Chgrid-Auth-Client: 1`) so the server can issue an instance-scoped `HttpOnly` session cookie.
|
- `auth_result.sessionToken` is used by the client to call the instance-scoped HTTP endpoint `GET <base_path>auth/session/set` (`Authorization: Bearer <sessionToken>`, `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.
|
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
||||||
@@ -104,7 +107,9 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- `welcome.player`: server-assigned spawn/current self position at connect time.
|
- `welcome.player`: server-assigned spawn/current self position at connect time.
|
||||||
- `welcome.serverInfo`: server process identity/version metadata:
|
- `welcome.serverInfo`: server process identity/version metadata:
|
||||||
- `instanceId`: unique id generated at server startup
|
- `instanceId`: unique id generated at server startup
|
||||||
- `version`: server package version (or `unknown` fallback)
|
- `releaseVersion`: shared public release version
|
||||||
|
- `serverVersion`: server diagnostics version text (`release + server revision`)
|
||||||
|
- `expectedClientRevision`: browser asset revision required by this server instance
|
||||||
- `gridName`: server-owned user-facing grid name
|
- `gridName`: server-owned user-facing grid name
|
||||||
- `welcomeMessage`: server-owned pre-login welcome string
|
- `welcomeMessage`: server-owned pre-login welcome string
|
||||||
- `welcome.uiDefinitions`: server-provided item UI definitions:
|
- `welcome.uiDefinitions`: server-provided item UI definitions:
|
||||||
@@ -157,4 +162,5 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
- After reconnect, if `welcome.serverInfo.instanceId` changed, client announces `Server restarted.`
|
- After reconnect, if `welcome.serverInfo.instanceId` changed, client announces `Server restarted.`
|
||||||
- Client emits `Connected to server. Version <version>.` on initial `welcome` and
|
- Client emits `Connected to server. Version <version>.` on initial `welcome` and
|
||||||
`Reconnected to server. Version <version>.` after reconnect.
|
`Reconnected to server. Version <version>.` after reconnect.
|
||||||
- If `welcome.serverInfo.version` differs from running client version, client auto-reloads.
|
- If `auth_required.expectedClientRevision` or `welcome.serverInfo.expectedClientRevision` differs from the running client revision, client auto-reloads.
|
||||||
|
- Server-only version changes do not trigger browser reload unless `expectedClientRevision` also changes.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
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.
|
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`.
|
5. If resume does not authenticate, server sends `auth_required`.
|
||||||
- includes `gridName` and `welcomeMessage` for pre-login branding.
|
- includes `gridName` and `welcomeMessage` for pre-login branding.
|
||||||
|
- includes `serverVersion` and `expectedClientRevision` for stale-client detection before login.
|
||||||
- includes `authPolicy` limits for username/password.
|
- includes `authPolicy` limits for username/password.
|
||||||
6. Client sends `auth_login` or `auth_register` (or explicit `auth_resume` if provided by caller).
|
6. Client sends `auth_login` or `auth_register` (or explicit `auth_resume` if provided by caller).
|
||||||
7. Server sends `auth_result`.
|
7. Server sends `auth_result`.
|
||||||
@@ -20,8 +21,8 @@
|
|||||||
- applies `welcome.worldConfig.movementTickMs` as movement pacing guidance
|
- applies `welcome.worldConfig.movementTickMs` as movement pacing guidance
|
||||||
- applies `welcome.worldConfig.movementMaxStepsPerTick` for movement-rate parity
|
- applies `welcome.worldConfig.movementMaxStepsPerTick` for movement-rate parity
|
||||||
- uses `welcome.player` as authoritative starting position (restored from server-side account state when available)
|
- uses `welcome.player` as authoritative starting position (restored from server-side account state when available)
|
||||||
- records `welcome.serverInfo` (`instanceId`, `version`, `gridName`, `welcomeMessage`) for restart detection and client branding
|
- records `welcome.serverInfo` (`instanceId`, `releaseVersion`, `serverVersion`, `expectedClientRevision`, `gridName`, `welcomeMessage`) for restart detection and client branding
|
||||||
- if `welcome.serverInfo.version` differs from running client version, auto-reloads the page
|
- if `welcome.serverInfo.expectedClientRevision` differs from the running client revision, auto-reloads the page
|
||||||
- applies `welcome.uiDefinitions` for item menus/properties/options, server-backed command metadata, item-management metadata, and admin menu labels/order
|
- 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
|
- sends initial `update_position` echo from server-assigned starting tile
|
||||||
- sends initial `update_nickname`
|
- sends initial `update_nickname`
|
||||||
@@ -75,6 +76,7 @@ Core incoming message effects:
|
|||||||
- Reconnect flow waits 5 seconds and retries up to 3 times.
|
- Reconnect flow waits 5 seconds and retries up to 3 times.
|
||||||
- If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart.
|
- If reconnect lands on a different `welcome.serverInfo.instanceId`, client announces server restart.
|
||||||
- Connect/reconnect status message is emitted from `welcome` and includes server version.
|
- Connect/reconnect status message is emitted from `welcome` and includes server version.
|
||||||
|
- Server-only deploys no longer force browser reloads unless `expectedClientRevision` changes.
|
||||||
|
|
||||||
## Authorization Runtime
|
## Authorization Runtime
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,9 @@ class AuthRequiredPacket(BasePacket):
|
|||||||
authPolicy: dict | None = None
|
authPolicy: dict | None = None
|
||||||
gridName: str | None = None
|
gridName: str | None = None
|
||||||
welcomeMessage: str | None = None
|
welcomeMessage: str | None = None
|
||||||
|
releaseVersion: str | None = None
|
||||||
|
expectedClientRevision: str | None = None
|
||||||
|
serverVersion: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuthResultPacket(BasePacket):
|
class AuthResultPacket(BasePacket):
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from collections import deque
|
|||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
from importlib.metadata import PackageNotFoundError, version as package_version
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -113,6 +112,7 @@ from .ui_metadata import (
|
|||||||
ITEM_MANAGEMENT_ACTION_DEFINITIONS,
|
ITEM_MANAGEMENT_ACTION_DEFINITIONS,
|
||||||
MAIN_MODE_SERVER_COMMAND_DEFINITIONS,
|
MAIN_MODE_SERVER_COMMAND_DEFINITIONS,
|
||||||
)
|
)
|
||||||
|
from .version import format_server_version
|
||||||
|
|
||||||
LOGGER = logging.getLogger("chgrid.server")
|
LOGGER = logging.getLogger("chgrid.server")
|
||||||
PACKET_LOGGER = logging.getLogger("chgrid.server.packet")
|
PACKET_LOGGER = logging.getLogger("chgrid.server.packet")
|
||||||
@@ -196,7 +196,8 @@ class SignalingServer:
|
|||||||
self.movement_tick_ms = MOVEMENT_TICK_MS
|
self.movement_tick_ms = MOVEMENT_TICK_MS
|
||||||
self.movement_max_steps_per_tick = MOVEMENT_MAX_STEPS_PER_TICK
|
self.movement_max_steps_per_tick = MOVEMENT_MAX_STEPS_PER_TICK
|
||||||
self.instance_id = str(uuid.uuid4())
|
self.instance_id = str(uuid.uuid4())
|
||||||
self.server_version = self._resolve_server_version()
|
self.release_version, self.expected_client_revision = self._resolve_client_version_metadata()
|
||||||
|
self.server_version = self._resolve_server_version(self.release_version)
|
||||||
self.host_origin = normalize_origin(host_origin, field_name="host origin") if host_origin else None
|
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.base_path = self._normalize_base_path(base_path)
|
||||||
self.grid_name = str(grid_name).strip() or "Chat Grid"
|
self.grid_name = str(grid_name).strip() or "Chat Grid"
|
||||||
@@ -225,45 +226,36 @@ class SignalingServer:
|
|||||||
self._pending_reboot_task: asyncio.Task[None] | None = None
|
self._pending_reboot_task: asyncio.Task[None] | None = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_server_version() -> str:
|
def _resolve_server_version(release_version: str) -> str:
|
||||||
"""Resolve serverInfo version, preferring synced web version when available."""
|
"""Resolve server diagnostics version text."""
|
||||||
|
|
||||||
env_override = os.getenv("CHGRID_SERVER_VERSION", "").strip()
|
env_override = os.getenv("CHGRID_SERVER_VERSION", "").strip()
|
||||||
if env_override:
|
if env_override:
|
||||||
return env_override
|
return env_override
|
||||||
|
|
||||||
|
return format_server_version(release_version)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_client_version_metadata() -> tuple[str, str]:
|
||||||
|
"""Resolve shared release version and expected client revision from version.js."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
version_file = Path(__file__).resolve().parents[2] / "client" / "public" / "version.js"
|
version_file = Path(__file__).resolve().parents[2] / "client" / "public" / "version.js"
|
||||||
text = version_file.read_text(encoding="utf-8")
|
text = version_file.read_text(encoding="utf-8")
|
||||||
token = SignalingServer._version_from_web_version_text(text)
|
return SignalingServer._client_version_metadata_from_web_version_text(text)
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
return "", ""
|
||||||
|
|
||||||
try:
|
|
||||||
return package_version("chgrid-server")
|
|
||||||
except PackageNotFoundError:
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _version_from_web_version_text(text: str) -> str:
|
def _client_version_metadata_from_web_version_text(text: str) -> tuple[str, str]:
|
||||||
"""Parse release/build metadata from one client version.js file."""
|
"""Parse release/client revision metadata from one client version.js file."""
|
||||||
|
|
||||||
release_match = re.search(r'CHGRID_RELEASE_VERSION\s*=\s*"([^"]+)"', text)
|
release_match = re.search(r'CHGRID_RELEASE_VERSION\s*=\s*"([^"]+)"', text)
|
||||||
revision_match = re.search(r'CHGRID_BUILD_REVISION\s*=\s*"([^"]+)"', text)
|
revision_match = re.search(r'CHGRID_CLIENT_REVISION\s*=\s*"([^"]+)"', text)
|
||||||
if release_match or revision_match:
|
return (
|
||||||
parts = [
|
|
||||||
release_match.group(1).strip() if release_match else "",
|
release_match.group(1).strip() if release_match else "",
|
||||||
revision_match.group(1).strip() if revision_match else "",
|
revision_match.group(1).strip() if revision_match else "",
|
||||||
]
|
)
|
||||||
token = " ".join(part for part in parts if part)
|
|
||||||
if token:
|
|
||||||
return token
|
|
||||||
legacy_match = re.search(r'CHGRID_WEB_VERSION\s*=\s*"([^"]+)"', text)
|
|
||||||
if legacy_match:
|
|
||||||
return legacy_match.group(1).strip()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def items(self) -> dict[str, WorldItem]:
|
def items(self) -> dict[str, WorldItem]:
|
||||||
@@ -1522,6 +1514,9 @@ class SignalingServer:
|
|||||||
authPolicy=self._auth_policy(),
|
authPolicy=self._auth_policy(),
|
||||||
gridName=self.grid_name,
|
gridName=self.grid_name,
|
||||||
welcomeMessage=self.welcome_message,
|
welcomeMessage=self.welcome_message,
|
||||||
|
releaseVersion=self.release_version or None,
|
||||||
|
expectedClientRevision=self.expected_client_revision or None,
|
||||||
|
serverVersion=self.server_version,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
async for raw_message in websocket:
|
async for raw_message in websocket:
|
||||||
@@ -1580,7 +1575,9 @@ class SignalingServer:
|
|||||||
uiDefinitions=self._build_ui_definitions(client),
|
uiDefinitions=self._build_ui_definitions(client),
|
||||||
serverInfo={
|
serverInfo={
|
||||||
"instanceId": self.instance_id,
|
"instanceId": self.instance_id,
|
||||||
"version": self.server_version,
|
"releaseVersion": self.release_version,
|
||||||
|
"serverVersion": self.server_version,
|
||||||
|
"expectedClientRevision": self.expected_client_revision,
|
||||||
"gridName": self.grid_name,
|
"gridName": self.grid_name,
|
||||||
"welcomeMessage": self.welcome_message,
|
"welcomeMessage": self.welcome_message,
|
||||||
},
|
},
|
||||||
|
|||||||
22
server/app/version.py
Normal file
22
server/app/version.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""Server version metadata helpers.
|
||||||
|
|
||||||
|
This module owns the server-only revision identifier used for diagnostics and
|
||||||
|
`/version` output. The shared public release version continues to come from the
|
||||||
|
client's `public/version.js` metadata so one release number can be used across
|
||||||
|
the whole app.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
SERVER_REVISION = "S001"
|
||||||
|
|
||||||
|
|
||||||
|
def format_server_version(release_version: str) -> str:
|
||||||
|
"""Return display text for the current server build."""
|
||||||
|
|
||||||
|
release = str(release_version).strip()
|
||||||
|
revision = str(SERVER_REVISION).strip()
|
||||||
|
if release and revision:
|
||||||
|
return f"{release} {revision}"
|
||||||
|
return release or revision or "unknown"
|
||||||
@@ -65,15 +65,14 @@ def test_client_ip_ignores_forwarded_for_from_non_loopback_peer() -> None:
|
|||||||
assert server._client_ip(client) == "203.0.113.20"
|
assert server._client_ip(client) == "203.0.113.20"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_server_version_reads_release_and_revision(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_resolve_client_version_metadata_reads_release_and_revision(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
version_text = """
|
version_text = """
|
||||||
window.CHGRID_RELEASE_VERSION = "0.1.0";
|
window.CHGRID_RELEASE_VERSION = "0.1.1";
|
||||||
window.CHGRID_BUILD_REVISION = "R348";
|
window.CHGRID_CLIENT_REVISION = "R350";
|
||||||
window.CHGRID_WEB_VERSION = `${window.CHGRID_RELEASE_VERSION} ${window.CHGRID_BUILD_REVISION}`;
|
|
||||||
""".strip()
|
""".strip()
|
||||||
resolved = SignalingServer._version_from_web_version_text(version_text)
|
resolved = SignalingServer._client_version_metadata_from_web_version_text(version_text)
|
||||||
|
|
||||||
assert resolved == "0.1.0 R348"
|
assert resolved == ("0.1.1", "R350")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user