Store item actor ids with display-name snapshots and nickname carrier display

This commit is contained in:
Jage9
2026-02-27 02:34:58 -05:00
parent 4fcd006856
commit 31ecb8eb5c
6 changed files with 60 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version. // Maintainer-controlled web client version.
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) // 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. // 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";

View File

@@ -10,7 +10,9 @@
"x": 0, "x": 0,
"y": 0, "y": 0,
"createdBy": "user-id", "createdBy": "user-id",
"createdByName": "username",
"updatedBy": "user-id", "updatedBy": "user-id",
"updatedByName": "username",
"createdAt": 1735689600000, "createdAt": 1735689600000,
"updatedAt": 1735689600000, "updatedAt": 1735689600000,
"version": 1, "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. - `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. - `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). - `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. - `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`). - `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`). - `radio_station` can override this per instance via `params.emitRange` (`5..20`).
@@ -41,7 +44,9 @@
"x": 0, "x": 0,
"y": 0, "y": 0,
"createdBy": "user-id", "createdBy": "user-id",
"createdByName": "username",
"updatedBy": "user-id", "updatedBy": "user-id",
"updatedByName": "username",
"createdAt": 1735689600000, "createdAt": 1735689600000,
"updatedAt": 1735689600000, "updatedAt": 1735689600000,
"version": 1, "version": 1,

View File

@@ -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"}, "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"}, "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"}, "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"}, "createdBy": {"valueType": "text", "tooltip": "Display name of the user who originally created this item.", "label": "Created by"},
"updatedBy": {"valueType": "text", "tooltip": "Username that most recently updated this item.", "label": "Updated 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"}, "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"}, "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"}, "capabilities": {"valueType": "text", "tooltip": "Supported actions for this item type.", "label": "Capabilities"},

View File

@@ -39,14 +39,18 @@ class ItemService:
item_def = get_item_definition(item_type) item_def = get_item_definition(item_type)
now = self.now_ms() now = self.now_ms()
actor_id = client.user_id or client.id
actor_name = client.username or client.nickname or actor_id
return WorldItem( return WorldItem(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
type=item_type, type=item_type,
title=item_def.default_title, title=item_def.default_title,
x=client.x, x=client.x,
y=client.y, y=client.y,
createdBy=client.username or client.nickname or client.id, createdBy=actor_id,
updatedBy=client.username or client.nickname or client.id, createdByName=actor_name,
updatedBy=actor_id,
updatedByName=actor_name,
createdAt=now, createdAt=now,
updatedAt=now, updatedAt=now,
version=1, version=1,
@@ -92,6 +96,7 @@ class ItemService:
item.y = client.y item.y = client.y
item.updatedAt = self.now_ms() item.updatedAt = self.now_ms()
item.updatedBy = "system" item.updatedBy = "system"
item.updatedByName = "system"
changed.append(item) changed.append(item)
return changed return changed
@@ -117,7 +122,9 @@ class ItemService:
x=persisted.x, x=persisted.x,
y=persisted.y, y=persisted.y,
createdBy=persisted.createdBy, createdBy=persisted.createdBy,
createdByName=persisted.createdByName or persisted.createdBy,
updatedBy=persisted.updatedBy or persisted.createdBy, updatedBy=persisted.updatedBy or persisted.createdBy,
updatedByName=persisted.updatedByName or persisted.updatedBy or persisted.createdBy,
createdAt=persisted.createdAt, createdAt=persisted.createdAt,
updatedAt=persisted.updatedAt, updatedAt=persisted.updatedAt,
version=persisted.version, version=persisted.version,
@@ -171,7 +178,9 @@ class ItemService:
x=item.x, x=item.x,
y=item.y, y=item.y,
createdBy=item.createdBy, createdBy=item.createdBy,
createdByName=item.createdByName,
updatedBy=item.updatedBy, updatedBy=item.updatedBy,
updatedByName=item.updatedByName,
createdAt=item.createdAt, createdAt=item.createdAt,
updatedAt=item.updatedAt, updatedAt=item.updatedAt,
version=item.version, version=item.version,

View File

@@ -243,7 +243,9 @@ class WorldItem(BaseModel):
x: int x: int
y: int y: int
createdBy: str createdBy: str
createdByName: str
updatedBy: str updatedBy: str
updatedByName: str
createdAt: int createdAt: int
updatedAt: int updatedAt: int
version: int version: int
@@ -263,7 +265,9 @@ class PersistedWorldItem(BaseModel):
x: int x: int
y: int y: int
createdBy: str createdBy: str
createdByName: str | None = None
updatedBy: str | None = None updatedBy: str | None = None
updatedByName: str | None = None
createdAt: int createdAt: int
updatedAt: int updatedAt: int
version: int version: int

View File

@@ -380,14 +380,18 @@ class SignalingServer:
def _build_item_display_values(self, item: WorldItem) -> dict[str, str]: def _build_item_display_values(self, item: WorldItem) -> dict[str, str]:
"""Build server-authoritative item property display values for readonly/system fields.""" """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 { return {
"type": item.type, "type": item.type,
"x": str(item.x), "x": str(item.x),
"y": str(item.y), "y": str(item.y),
"carrierId": item.carrierId or "none", "carrierId": carrier_label,
"version": str(item.version), "version": str(item.version),
"createdBy": item.createdBy, "createdBy": item.createdByName or item.createdBy,
"updatedBy": item.updatedBy, "updatedBy": item.updatedByName or item.updatedBy,
"createdAt": self._format_display_timestamp_ms(item.createdAt), "createdAt": self._format_display_timestamp_ms(item.createdAt),
"updatedAt": self._format_display_timestamp_ms(item.updatedAt), "updatedAt": self._format_display_timestamp_ms(item.updatedAt),
"capabilities": ", ".join(item.capabilities) if item.capabilities else "none", "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)}) return item.model_copy(update={"display": self._build_item_display_values(item)})
@staticmethod @staticmethod
def _item_updated_by_actor(client: ClientConnection) -> str: def _item_updated_actor(client: ClientConnection) -> tuple[str, str]:
"""Resolve display name stored in `updatedBy` for client-initiated item mutations.""" """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: def _get_item_emit_range(self, item: WorldItem) -> int:
"""Return effective emit range for one item with sane bounds.""" """Return effective emit range for one item with sane bounds."""
@@ -482,6 +488,7 @@ class SignalingServer:
item.params["nowPlaying"] = now_playing item.params["nowPlaying"] = now_playing
item.updatedAt = self.item_service.now_ms() item.updatedAt = self.item_service.now_ms()
item.updatedBy = "system" item.updatedBy = "system"
item.updatedByName = "system"
item.version += 1 item.version += 1
self._request_state_save() self._request_state_save()
await self._broadcast_item(item) await self._broadcast_item(item)
@@ -822,7 +829,8 @@ class SignalingServer:
item.params.pop("recording", None) item.params.pop("recording", None)
item.params.pop("recordingLengthMs", None) item.params.pop("recordingLengthMs", None)
item.updatedAt = self.item_service.now_ms() 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 item.version += 1
self._request_state_save() self._request_state_save()
await self._broadcast_item(item) await self._broadcast_item(item)
@@ -1520,10 +1528,12 @@ class SignalingServer:
) )
carried = self.item_service.find_carried_item(client.id) carried = self.item_service.find_carried_item(client.id)
if carried: if carried:
actor_id, actor_name = self._item_updated_actor(client)
carried.x = client.x carried.x = client.x
carried.y = client.y carried.y = client.y
carried.updatedAt = self.item_service.now_ms() 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_item(carried)
return return
@@ -1556,10 +1566,12 @@ class SignalingServer:
) )
carried = self.item_service.find_carried_item(client.id) carried = self.item_service.find_carried_item(client.id)
if carried: if carried:
actor_id, actor_name = self._item_updated_actor(client)
carried.x = client.x carried.x = client.x
carried.y = client.y carried.y = client.y
carried.updatedAt = self.item_service.now_ms() 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_item(carried)
await self._broadcast( await self._broadcast(
BroadcastTeleportCompletePacket( BroadcastTeleportCompletePacket(
@@ -1736,7 +1748,9 @@ class SignalingServer:
item.x = client.x item.x = client.x
item.y = client.y item.y = client.y
item.updatedAt = self.item_service.now_ms() 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) await self._broadcast_item(item)
self._request_state_save() self._request_state_save()
await self._send_item_result(client, True, "pickup", f"Picked up {item.title}.", item.id) 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.x = packet.x
item.y = packet.y item.y = packet.y
item.updatedAt = self.item_service.now_ms() 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) await self._broadcast_item(item)
self._request_state_save() self._request_state_save()
await self._send_item_result(client, True, "drop", f"Dropped {item.title}.", item.id) 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) await self._send_item_result(client, False, "use", str(exc), item.id)
return return
item.updatedAt = now_ms 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() self._request_state_save()
await self._broadcast_item(item) await self._broadcast_item(item)
@@ -1914,7 +1932,9 @@ class SignalingServer:
await self._send_item_result(client, False, "secondary_use", str(exc), item.id) await self._send_item_result(client, False, "secondary_use", str(exc), item.id)
return return
item.updatedAt = self.item_service.now_ms() 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 item.version += 1
self._request_state_save() self._request_state_save()
await self._broadcast_item(item) await self._broadcast_item(item)
@@ -2089,7 +2109,9 @@ class SignalingServer:
return return
item.params = next_params item.params = next_params
item.updatedAt = self.item_service.now_ms() 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 item.version += 1
await self._broadcast_item(item) await self._broadcast_item(item)
self._request_state_save() self._request_state_save()