diff --git a/client/public/version.js b/client/public/version.js index 5a7c1f0..66c3b5b 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,5 +1,5 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.27 R282"; +window.CHGRID_WEB_VERSION = "2026.02.27 R283"; // Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid. window.CHGRID_TIME_ZONE = "America/Detroit"; diff --git a/docs/item-schema.md b/docs/item-schema.md index a499748..40edab5 100644 --- a/docs/item-schema.md +++ b/docs/item-schema.md @@ -10,7 +10,9 @@ "x": 0, "y": 0, "createdBy": "user-id", + "createdByName": "username", "updatedBy": "user-id", + "updatedByName": "username", "createdAt": 1735689600000, "updatedAt": 1735689600000, "version": 1, @@ -25,7 +27,8 @@ - `useSound`: optional client-played one-shot sound when item `use` succeeds; global item field and not user-editable in V1. - `emitSound`: optional continuously-looping spatial sound emitted from the item on the grid; global item field and not user-editable in V1. - `capabilities`, `useSound`, and `emitSound` are derived from global item-type definitions at runtime (not stored per-instance in persisted state). -- `updatedBy` tracks the user account that last changed item state. +- `createdBy` / `updatedBy` are stable user IDs. +- `createdByName` / `updatedByName` are display-name snapshots used for inspect/readout text. - `useCooldownMs`: global per item type (`radio_station=1000`, `dice=1000`, `wheel=4000`, `clock=1000`, `widget=1000`, `piano=1000`), not per-instance editable. - `emitRange`: global spatial range default per item type (`radio_station=10`, `dice=15`, `wheel=15`, `clock=10`, `widget=15`, `piano=15`). - `radio_station` can override this per instance via `params.emitRange` (`5..20`). @@ -41,7 +44,9 @@ "x": 0, "y": 0, "createdBy": "user-id", + "createdByName": "username", "updatedBy": "user-id", + "updatedByName": "username", "createdAt": 1735689600000, "updatedAt": 1735689600000, "version": 1, diff --git a/server/app/item_catalog.py b/server/app/item_catalog.py index 0630fa4..0710d19 100644 --- a/server/app/item_catalog.py +++ b/server/app/item_catalog.py @@ -85,8 +85,8 @@ GLOBAL_ITEM_PROPERTY_METADATA: dict[str, dict[str, object]] = { "y": {"valueType": "number", "tooltip": "Item Y coordinate on the grid.", "label": "Y"}, "carrierId": {"valueType": "text", "tooltip": "User id currently carrying this item, or none.", "label": "Carrier"}, "version": {"valueType": "number", "tooltip": "Server-side item version incremented on each update.", "label": "Version"}, - "createdBy": {"valueType": "text", "tooltip": "Username that originally created this item.", "label": "Created by"}, - "updatedBy": {"valueType": "text", "tooltip": "Username that most recently updated this item.", "label": "Updated by"}, + "createdBy": {"valueType": "text", "tooltip": "Display name of the user who originally created this item.", "label": "Created by"}, + "updatedBy": {"valueType": "text", "tooltip": "Display name of the user who most recently updated this item.", "label": "Updated by"}, "createdAt": {"valueType": "text", "tooltip": "Creation timestamp for this item.", "label": "Created at"}, "updatedAt": {"valueType": "text", "tooltip": "Last update timestamp for this item.", "label": "Updated at"}, "capabilities": {"valueType": "text", "tooltip": "Supported actions for this item type.", "label": "Capabilities"}, diff --git a/server/app/item_service.py b/server/app/item_service.py index 43f8d50..944d5c2 100644 --- a/server/app/item_service.py +++ b/server/app/item_service.py @@ -39,14 +39,18 @@ class ItemService: item_def = get_item_definition(item_type) now = self.now_ms() + actor_id = client.user_id or client.id + actor_name = client.username or client.nickname or actor_id return WorldItem( id=str(uuid.uuid4()), type=item_type, title=item_def.default_title, x=client.x, y=client.y, - createdBy=client.username or client.nickname or client.id, - updatedBy=client.username or client.nickname or client.id, + createdBy=actor_id, + createdByName=actor_name, + updatedBy=actor_id, + updatedByName=actor_name, createdAt=now, updatedAt=now, version=1, @@ -92,6 +96,7 @@ class ItemService: item.y = client.y item.updatedAt = self.now_ms() item.updatedBy = "system" + item.updatedByName = "system" changed.append(item) return changed @@ -117,7 +122,9 @@ class ItemService: x=persisted.x, y=persisted.y, createdBy=persisted.createdBy, + createdByName=persisted.createdByName or persisted.createdBy, updatedBy=persisted.updatedBy or persisted.createdBy, + updatedByName=persisted.updatedByName or persisted.updatedBy or persisted.createdBy, createdAt=persisted.createdAt, updatedAt=persisted.updatedAt, version=persisted.version, @@ -171,7 +178,9 @@ class ItemService: x=item.x, y=item.y, createdBy=item.createdBy, + createdByName=item.createdByName, updatedBy=item.updatedBy, + updatedByName=item.updatedByName, createdAt=item.createdAt, updatedAt=item.updatedAt, version=item.version, diff --git a/server/app/models.py b/server/app/models.py index 90b6c23..d063328 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -243,7 +243,9 @@ class WorldItem(BaseModel): x: int y: int createdBy: str + createdByName: str updatedBy: str + updatedByName: str createdAt: int updatedAt: int version: int @@ -263,7 +265,9 @@ class PersistedWorldItem(BaseModel): x: int y: int createdBy: str + createdByName: str | None = None updatedBy: str | None = None + updatedByName: str | None = None createdAt: int updatedAt: int version: int diff --git a/server/app/server.py b/server/app/server.py index 9da1860..4c71f3f 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -380,14 +380,18 @@ class SignalingServer: def _build_item_display_values(self, item: WorldItem) -> dict[str, str]: """Build server-authoritative item property display values for readonly/system fields.""" + carrier_label = "none" + if item.carrierId: + carrier = self._get_client_by_id(item.carrierId) + carrier_label = carrier.nickname if carrier is not None else item.carrierId return { "type": item.type, "x": str(item.x), "y": str(item.y), - "carrierId": item.carrierId or "none", + "carrierId": carrier_label, "version": str(item.version), - "createdBy": item.createdBy, - "updatedBy": item.updatedBy, + "createdBy": item.createdByName or item.createdBy, + "updatedBy": item.updatedByName or item.updatedBy, "createdAt": self._format_display_timestamp_ms(item.createdAt), "updatedAt": self._format_display_timestamp_ms(item.updatedAt), "capabilities": ", ".join(item.capabilities) if item.capabilities else "none", @@ -401,10 +405,12 @@ class SignalingServer: return item.model_copy(update={"display": self._build_item_display_values(item)}) @staticmethod - def _item_updated_by_actor(client: ClientConnection) -> str: - """Resolve display name stored in `updatedBy` for client-initiated item mutations.""" + def _item_updated_actor(client: ClientConnection) -> tuple[str, str]: + """Resolve `(actor_id, actor_name)` used in item update tracking fields.""" - return client.username or client.nickname or client.id + actor_id = client.user_id or client.id + actor_name = client.username or client.nickname or actor_id + return actor_id, actor_name def _get_item_emit_range(self, item: WorldItem) -> int: """Return effective emit range for one item with sane bounds.""" @@ -482,6 +488,7 @@ class SignalingServer: item.params["nowPlaying"] = now_playing item.updatedAt = self.item_service.now_ms() item.updatedBy = "system" + item.updatedByName = "system" item.version += 1 self._request_state_save() await self._broadcast_item(item) @@ -822,7 +829,8 @@ class SignalingServer: item.params.pop("recording", None) item.params.pop("recordingLengthMs", None) item.updatedAt = self.item_service.now_ms() - item.updatedBy = owner.username if owner and owner.username else "system" + item.updatedBy = owner.user_id if owner and owner.user_id else "system" + item.updatedByName = owner.username if owner and owner.username else "system" item.version += 1 self._request_state_save() await self._broadcast_item(item) @@ -1520,10 +1528,12 @@ class SignalingServer: ) carried = self.item_service.find_carried_item(client.id) if carried: + actor_id, actor_name = self._item_updated_actor(client) carried.x = client.x carried.y = client.y carried.updatedAt = self.item_service.now_ms() - carried.updatedBy = self._item_updated_by_actor(client) + carried.updatedBy = actor_id + carried.updatedByName = actor_name await self._broadcast_item(carried) return @@ -1556,10 +1566,12 @@ class SignalingServer: ) carried = self.item_service.find_carried_item(client.id) if carried: + actor_id, actor_name = self._item_updated_actor(client) carried.x = client.x carried.y = client.y carried.updatedAt = self.item_service.now_ms() - carried.updatedBy = self._item_updated_by_actor(client) + carried.updatedBy = actor_id + carried.updatedByName = actor_name await self._broadcast_item(carried) await self._broadcast( BroadcastTeleportCompletePacket( @@ -1736,7 +1748,9 @@ class SignalingServer: item.x = client.x item.y = client.y item.updatedAt = self.item_service.now_ms() - item.updatedBy = self._item_updated_by_actor(client) + actor_id, actor_name = self._item_updated_actor(client) + item.updatedBy = actor_id + item.updatedByName = actor_name await self._broadcast_item(item) self._request_state_save() await self._send_item_result(client, True, "pickup", f"Picked up {item.title}.", item.id) @@ -1757,7 +1771,9 @@ class SignalingServer: item.x = packet.x item.y = packet.y item.updatedAt = self.item_service.now_ms() - item.updatedBy = self._item_updated_by_actor(client) + actor_id, actor_name = self._item_updated_actor(client) + item.updatedBy = actor_id + item.updatedByName = actor_name await self._broadcast_item(item) self._request_state_save() await self._send_item_result(client, True, "drop", f"Dropped {item.title}.", item.id) @@ -1837,7 +1853,9 @@ class SignalingServer: await self._send_item_result(client, False, "use", str(exc), item.id) return item.updatedAt = now_ms - item.updatedBy = self._item_updated_by_actor(client) + actor_id, actor_name = self._item_updated_actor(client) + item.updatedBy = actor_id + item.updatedByName = actor_name self._request_state_save() await self._broadcast_item(item) @@ -1914,7 +1932,9 @@ class SignalingServer: await self._send_item_result(client, False, "secondary_use", str(exc), item.id) return item.updatedAt = self.item_service.now_ms() - item.updatedBy = self._item_updated_by_actor(client) + actor_id, actor_name = self._item_updated_actor(client) + item.updatedBy = actor_id + item.updatedByName = actor_name item.version += 1 self._request_state_save() await self._broadcast_item(item) @@ -2089,7 +2109,9 @@ class SignalingServer: return item.params = next_params item.updatedAt = self.item_service.now_ms() - item.updatedBy = self._item_updated_by_actor(client) + actor_id, actor_name = self._item_updated_actor(client) + item.updatedBy = actor_id + item.updatedByName = actor_name item.version += 1 await self._broadcast_item(item) self._request_state_save()