Track item updatedBy as readonly metadata and inspect field
This commit is contained in:
@@ -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 R281";
|
||||
window.CHGRID_WEB_VERSION = "2026.02.27 R282";
|
||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||
|
||||
@@ -184,6 +184,7 @@ export function getInspectItemPropertyKeys(item: WorldItem): string[] {
|
||||
'carrierId',
|
||||
'version',
|
||||
'createdBy',
|
||||
'updatedBy',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'capabilities',
|
||||
|
||||
@@ -7,6 +7,7 @@ export const itemSchema = z.object({
|
||||
x: z.number().int(),
|
||||
y: z.number().int(),
|
||||
createdBy: z.string(),
|
||||
updatedBy: z.string(),
|
||||
createdAt: z.number().int(),
|
||||
updatedAt: z.number().int(),
|
||||
version: z.number().int(),
|
||||
|
||||
@@ -11,6 +11,7 @@ export type WorldItem = {
|
||||
x: number;
|
||||
y: number;
|
||||
createdBy: string;
|
||||
updatedBy: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
version: number;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"createdBy": "user-id",
|
||||
"updatedBy": "user-id",
|
||||
"createdAt": 1735689600000,
|
||||
"updatedAt": 1735689600000,
|
||||
"version": 1,
|
||||
@@ -24,6 +25,7 @@
|
||||
- `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.
|
||||
- `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`).
|
||||
@@ -39,6 +41,7 @@
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"createdBy": "user-id",
|
||||
"updatedBy": "user-id",
|
||||
"createdAt": 1735689600000,
|
||||
"updatedAt": 1735689600000,
|
||||
"version": 1,
|
||||
|
||||
@@ -13,6 +13,7 @@ This is behavior-focused documentation for item types and their defaults.
|
||||
- `emitRange` (spatial range in squares)
|
||||
- `directional` (directional attenuation enabled)
|
||||
- Instance fields are persisted in `server/runtime/items.json`.
|
||||
- Read-only inspect fields include `createdBy` and `updatedBy` for ownership/change tracking.
|
||||
|
||||
## `radio_station`
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
||||
## Item Packet Behavior
|
||||
|
||||
- `item_upsert` is full-state replacement for one item, not partial patch.
|
||||
- `item_upsert.item.display` is server-owned display text for readonly/system properties (for example: `createdAt`, `updatedAt`, `capabilities`, `useSound`, `emitSound`).
|
||||
- `item_upsert.item.display` is server-owned display text for readonly/system properties (for example: `createdBy`, `updatedBy`, `createdAt`, `updatedAt`, `capabilities`, `useSound`, `emitSound`).
|
||||
- `item_action_result` messages are intended for direct screen-reader/user status feedback.
|
||||
- Piano runtime control no longer depends on parsing `item_action_result.message` text.
|
||||
- `item_piano_status` carries machine-readable piano events (`use_mode_entered`, record/playback transitions).
|
||||
|
||||
@@ -86,6 +86,7 @@ GLOBAL_ITEM_PROPERTY_METADATA: dict[str, dict[str, object]] = {
|
||||
"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"},
|
||||
"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"},
|
||||
|
||||
@@ -46,6 +46,7 @@ class ItemService:
|
||||
x=client.x,
|
||||
y=client.y,
|
||||
createdBy=client.username or client.nickname or client.id,
|
||||
updatedBy=client.username or client.nickname or client.id,
|
||||
createdAt=now,
|
||||
updatedAt=now,
|
||||
version=1,
|
||||
@@ -90,6 +91,7 @@ class ItemService:
|
||||
item.x = client.x
|
||||
item.y = client.y
|
||||
item.updatedAt = self.now_ms()
|
||||
item.updatedBy = "system"
|
||||
changed.append(item)
|
||||
return changed
|
||||
|
||||
@@ -115,6 +117,7 @@ class ItemService:
|
||||
x=persisted.x,
|
||||
y=persisted.y,
|
||||
createdBy=persisted.createdBy,
|
||||
updatedBy=persisted.updatedBy or persisted.createdBy,
|
||||
createdAt=persisted.createdAt,
|
||||
updatedAt=persisted.updatedAt,
|
||||
version=persisted.version,
|
||||
@@ -168,6 +171,7 @@ class ItemService:
|
||||
x=item.x,
|
||||
y=item.y,
|
||||
createdBy=item.createdBy,
|
||||
updatedBy=item.updatedBy,
|
||||
createdAt=item.createdAt,
|
||||
updatedAt=item.updatedAt,
|
||||
version=item.version,
|
||||
|
||||
@@ -243,6 +243,7 @@ class WorldItem(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
createdBy: str
|
||||
updatedBy: str
|
||||
createdAt: int
|
||||
updatedAt: int
|
||||
version: int
|
||||
@@ -262,6 +263,7 @@ class PersistedWorldItem(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
createdBy: str
|
||||
updatedBy: str | None = None
|
||||
createdAt: int
|
||||
updatedAt: int
|
||||
version: int
|
||||
|
||||
@@ -387,6 +387,7 @@ class SignalingServer:
|
||||
"carrierId": item.carrierId or "none",
|
||||
"version": str(item.version),
|
||||
"createdBy": item.createdBy,
|
||||
"updatedBy": 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",
|
||||
@@ -399,6 +400,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."""
|
||||
|
||||
return client.username or client.nickname or client.id
|
||||
|
||||
def _get_item_emit_range(self, item: WorldItem) -> int:
|
||||
"""Return effective emit range for one item with sane bounds."""
|
||||
|
||||
@@ -474,6 +481,7 @@ class SignalingServer:
|
||||
item.params["stationName"] = station_name
|
||||
item.params["nowPlaying"] = now_playing
|
||||
item.updatedAt = self.item_service.now_ms()
|
||||
item.updatedBy = "system"
|
||||
item.version += 1
|
||||
self._request_state_save()
|
||||
await self._broadcast_item(item)
|
||||
@@ -808,15 +816,16 @@ class SignalingServer:
|
||||
"events": compact_events,
|
||||
}
|
||||
self.item_service.save_piano_songs()
|
||||
owner_id = str(session.get("ownerClientId", ""))
|
||||
owner = self._get_client_by_id(owner_id) if owner_id else None
|
||||
item.params["songId"] = song_id
|
||||
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.version += 1
|
||||
self._request_state_save()
|
||||
await self._broadcast_item(item)
|
||||
owner_id = str(session.get("ownerClientId", ""))
|
||||
owner = self._get_client_by_id(owner_id) if owner_id else None
|
||||
if owner and notify_owner:
|
||||
await self._send_piano_status(
|
||||
owner,
|
||||
@@ -1514,6 +1523,7 @@ class SignalingServer:
|
||||
carried.x = client.x
|
||||
carried.y = client.y
|
||||
carried.updatedAt = self.item_service.now_ms()
|
||||
carried.updatedBy = self._item_updated_by_actor(client)
|
||||
await self._broadcast_item(carried)
|
||||
return
|
||||
|
||||
@@ -1549,6 +1559,7 @@ class SignalingServer:
|
||||
carried.x = client.x
|
||||
carried.y = client.y
|
||||
carried.updatedAt = self.item_service.now_ms()
|
||||
carried.updatedBy = self._item_updated_by_actor(client)
|
||||
await self._broadcast_item(carried)
|
||||
await self._broadcast(
|
||||
BroadcastTeleportCompletePacket(
|
||||
@@ -1725,6 +1736,7 @@ 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)
|
||||
await self._broadcast_item(item)
|
||||
self._request_state_save()
|
||||
await self._send_item_result(client, True, "pickup", f"Picked up {item.title}.", item.id)
|
||||
@@ -1745,6 +1757,7 @@ 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)
|
||||
await self._broadcast_item(item)
|
||||
self._request_state_save()
|
||||
await self._send_item_result(client, True, "drop", f"Dropped {item.title}.", item.id)
|
||||
@@ -1824,6 +1837,7 @@ 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)
|
||||
self._request_state_save()
|
||||
await self._broadcast_item(item)
|
||||
|
||||
@@ -1900,6 +1914,7 @@ 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)
|
||||
item.version += 1
|
||||
self._request_state_save()
|
||||
await self._broadcast_item(item)
|
||||
@@ -2074,6 +2089,7 @@ class SignalingServer:
|
||||
return
|
||||
item.params = next_params
|
||||
item.updatedAt = self.item_service.now_ms()
|
||||
item.updatedBy = self._item_updated_by_actor(client)
|
||||
item.version += 1
|
||||
await self._broadcast_item(item)
|
||||
self._request_state_save()
|
||||
|
||||
@@ -35,6 +35,7 @@ def test_ui_definitions_are_complete_for_all_item_types() -> None:
|
||||
"carrierId",
|
||||
"version",
|
||||
"createdBy",
|
||||
"updatedBy",
|
||||
"createdAt",
|
||||
"updatedAt",
|
||||
"capabilities",
|
||||
|
||||
Reference in New Issue
Block a user