Add audit logs, file-backed service logs, and localized timestamp display
This commit is contained in:
@@ -1,3 +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.21 R84";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R85";
|
||||||
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ const AUDIO_OUTPUT_STORAGE_KEY = 'chatGridAudioOutputDeviceId';
|
|||||||
const AUDIO_INPUT_NAME_STORAGE_KEY = 'chatGridAudioInputDeviceName';
|
const AUDIO_INPUT_NAME_STORAGE_KEY = 'chatGridAudioInputDeviceName';
|
||||||
const AUDIO_OUTPUT_NAME_STORAGE_KEY = 'chatGridAudioOutputDeviceName';
|
const AUDIO_OUTPUT_NAME_STORAGE_KEY = 'chatGridAudioOutputDeviceName';
|
||||||
const AUDIO_OUTPUT_MODE_STORAGE_KEY = 'chatGridAudioOutputMode';
|
const AUDIO_OUTPUT_MODE_STORAGE_KEY = 'chatGridAudioOutputMode';
|
||||||
|
const DEFAULT_DISPLAY_TIME_ZONE = 'America/Detroit';
|
||||||
const NICKNAME_STORAGE_KEY = 'spatialChatNickname';
|
const NICKNAME_STORAGE_KEY = 'spatialChatNickname';
|
||||||
const NICKNAME_MAX_LENGTH = 32;
|
const NICKNAME_MAX_LENGTH = 32;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CHGRID_WEB_VERSION?: string;
|
CHGRID_WEB_VERSION?: string;
|
||||||
|
CHGRID_TIME_ZONE?: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +94,7 @@ type ChangelogData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim();
|
const APP_VERSION = String(window.CHGRID_WEB_VERSION ?? '').trim();
|
||||||
|
const DISPLAY_TIME_ZONE = resolveDisplayTimeZone();
|
||||||
dom.appVersion.textContent = APP_VERSION
|
dom.appVersion.textContent = APP_VERSION
|
||||||
? `Another AI experiment with Jage. Version ${APP_VERSION}`
|
? `Another AI experiment with Jage. Version ${APP_VERSION}`
|
||||||
: 'Another AI experiment with Jage. Version unknown';
|
: 'Another AI experiment with Jage. Version unknown';
|
||||||
@@ -178,6 +181,41 @@ function requiredById<T extends HTMLElement>(id: string): T {
|
|||||||
return found as T;
|
return found as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDisplayTimeZone(): string {
|
||||||
|
const configured = String(window.CHGRID_TIME_ZONE ?? '').trim();
|
||||||
|
if (configured) {
|
||||||
|
try {
|
||||||
|
new Intl.DateTimeFormat('en-US', { timeZone: configured }).format(new Date());
|
||||||
|
return configured;
|
||||||
|
} catch {
|
||||||
|
// Fall back when configured timezone is invalid.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DEFAULT_DISPLAY_TIME_ZONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestampMs(value: unknown): string {
|
||||||
|
const raw = Number(value);
|
||||||
|
if (!Number.isFinite(raw)) {
|
||||||
|
return String(value ?? '');
|
||||||
|
}
|
||||||
|
const date = new Date(raw);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return String(value ?? '');
|
||||||
|
}
|
||||||
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: DISPLAY_TIME_ZONE,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).formatToParts(date);
|
||||||
|
const pick = (type: Intl.DateTimeFormatPartTypes): string => parts.find((part) => part.type === type)?.value ?? '00';
|
||||||
|
return `${pick('year')}-${pick('month')}-${pick('day')} ${pick('hour')}:${pick('minute')}`;
|
||||||
|
}
|
||||||
|
|
||||||
function setUpdatesExpanded(expanded: boolean): void {
|
function setUpdatesExpanded(expanded: boolean): void {
|
||||||
dom.updatesToggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
dom.updatesToggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||||
dom.updatesToggle.textContent = expanded ? 'Hide updates' : 'Show updates';
|
dom.updatesToggle.textContent = expanded ? 'Hide updates' : 'Show updates';
|
||||||
@@ -603,8 +641,8 @@ function getItemPropertyValue(item: WorldItem, key: string): string {
|
|||||||
if (key === 'carrierId') return item.carrierId ?? 'none';
|
if (key === 'carrierId') return item.carrierId ?? 'none';
|
||||||
if (key === 'version') return String(item.version);
|
if (key === 'version') return String(item.version);
|
||||||
if (key === 'createdBy') return item.createdBy;
|
if (key === 'createdBy') return item.createdBy;
|
||||||
if (key === 'createdAt') return String(item.createdAt);
|
if (key === 'createdAt') return formatTimestampMs(item.createdAt);
|
||||||
if (key === 'updatedAt') return String(item.updatedAt);
|
if (key === 'updatedAt') return formatTimestampMs(item.updatedAt);
|
||||||
if (key === 'capabilities') return item.capabilities.join(', ') || 'none';
|
if (key === 'capabilities') return item.capabilities.join(', ') || 'none';
|
||||||
if (key === 'useSound') return item.useSound ?? 'none';
|
if (key === 'useSound') return item.useSound ?? 'none';
|
||||||
if (key === 'enabled') return item.params.enabled === false ? 'off' : 'on';
|
if (key === 'enabled') return item.params.enabled === false ? 'off' : 'on';
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ Logs:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
journalctl -u chat-grid.service -f
|
journalctl -u chat-grid.service -f
|
||||||
|
tail -f /home/bestmidi/chgrid/server/runtime/server.log
|
||||||
```
|
```
|
||||||
|
|
||||||
If you previously used `chgrid-signaling.service`, migrate once:
|
If you previously used `chgrid-signaling.service`, migrate once:
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ if [[ ! -f "$SRC_UNIT" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
sudo cp "$SRC_UNIT" "$DST_UNIT"
|
sudo cp "$SRC_UNIT" "$DST_UNIT"
|
||||||
|
sudo install -d -m 0755 -o bestmidi -g bestmidi "$REPO_ROOT/server/runtime"
|
||||||
|
sudo touch "$REPO_ROOT/server/runtime/server.log"
|
||||||
|
sudo chown bestmidi:bestmidi "$REPO_ROOT/server/runtime/server.log"
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now "$UNIT_NAME"
|
sudo systemctl enable --now "$UNIT_NAME"
|
||||||
sudo systemctl restart "$UNIT_NAME"
|
sudo systemctl restart "$UNIT_NAME"
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ User=bestmidi
|
|||||||
Group=bestmidi
|
Group=bestmidi
|
||||||
WorkingDirectory=/home/bestmidi/chgrid/server
|
WorkingDirectory=/home/bestmidi/chgrid/server
|
||||||
Environment=PATH=/home/bestmidi/chgrid/server/.venv/bin:/usr/bin:/bin
|
Environment=PATH=/home/bestmidi/chgrid/server/.venv/bin:/usr/bin:/bin
|
||||||
|
ExecStartPre=/usr/bin/mkdir -p /home/bestmidi/chgrid/server/runtime
|
||||||
ExecStart=/home/bestmidi/chgrid/server/.venv/bin/python main.py --config /home/bestmidi/chgrid/server/config.toml
|
ExecStart=/home/bestmidi/chgrid/server/.venv/bin/python main.py --config /home/bestmidi/chgrid/server/config.toml
|
||||||
|
StandardOutput=append:/home/bestmidi/chgrid/server/runtime/server.log
|
||||||
|
StandardError=append:/home/bestmidi/chgrid/server/runtime/server.log
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,12 @@ class SignalingServer:
|
|||||||
for item in self.item_service.drop_carried_items_for_disconnect(disconnected):
|
for item in self.item_service.drop_carried_items_for_disconnect(disconnected):
|
||||||
await self._broadcast_item(item)
|
await self._broadcast_item(item)
|
||||||
self.item_service.save_state()
|
self.item_service.save_state()
|
||||||
LOGGER.info("client disconnected id=%s total=%d", disconnected.id, len(self.clients))
|
LOGGER.info(
|
||||||
|
"client disconnected id=%s nickname=%s total=%d",
|
||||||
|
disconnected.id,
|
||||||
|
disconnected.nickname,
|
||||||
|
len(self.clients),
|
||||||
|
)
|
||||||
await self._broadcast(UserLeftPacket(type="user_left", id=disconnected.id), exclude=websocket)
|
await self._broadcast(UserLeftPacket(type="user_left", id=disconnected.id), exclude=websocket)
|
||||||
await self._broadcast(
|
await self._broadcast(
|
||||||
BroadcastChatMessagePacket(
|
BroadcastChatMessagePacket(
|
||||||
@@ -249,6 +254,10 @@ class SignalingServer:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
client.nickname = requested_nickname
|
client.nickname = requested_nickname
|
||||||
|
if old_nickname == "user...":
|
||||||
|
LOGGER.info("user login id=%s nickname=%s", client.id, client.nickname)
|
||||||
|
else:
|
||||||
|
LOGGER.info("nickname change id=%s old=%s new=%s", client.id, old_nickname, client.nickname)
|
||||||
await self._send(
|
await self._send(
|
||||||
client.websocket,
|
client.websocket,
|
||||||
NicknameResultPacket(
|
NicknameResultPacket(
|
||||||
@@ -319,6 +328,15 @@ class SignalingServer:
|
|||||||
self.item_service.add_item(item)
|
self.item_service.add_item(item)
|
||||||
await self._broadcast_item(item)
|
await self._broadcast_item(item)
|
||||||
self.item_service.save_state()
|
self.item_service.save_state()
|
||||||
|
LOGGER.info(
|
||||||
|
"item created by=%s item_id=%s type=%s title=%s x=%d y=%d",
|
||||||
|
client.nickname,
|
||||||
|
item.id,
|
||||||
|
item.type,
|
||||||
|
item.title,
|
||||||
|
item.x,
|
||||||
|
item.y,
|
||||||
|
)
|
||||||
item_text = f"{item.title} ({self._item_type_label(item)})"
|
item_text = f"{item.title} ({self._item_type_label(item)})"
|
||||||
await self._broadcast(
|
await self._broadcast(
|
||||||
BroadcastChatMessagePacket(
|
BroadcastChatMessagePacket(
|
||||||
@@ -389,6 +407,13 @@ class SignalingServer:
|
|||||||
if item.carrierId is None and (item.x != client.x or item.y != client.y):
|
if item.carrierId is None and (item.x != client.x or item.y != client.y):
|
||||||
await self._send_item_result(client, False, "delete", "Item is not on your square.", item.id)
|
await self._send_item_result(client, False, "delete", "Item is not on your square.", item.id)
|
||||||
return
|
return
|
||||||
|
LOGGER.info(
|
||||||
|
"item deleted by=%s item_id=%s type=%s title=%s",
|
||||||
|
client.nickname,
|
||||||
|
item.id,
|
||||||
|
item.type,
|
||||||
|
item.title,
|
||||||
|
)
|
||||||
self.item_service.remove_item(item.id)
|
self.item_service.remove_item(item.id)
|
||||||
self.item_last_use_ms.pop(item.id, None)
|
self.item_last_use_ms.pop(item.id, None)
|
||||||
await self._broadcast(ItemRemovePacket(type="item_remove", itemId=item.id))
|
await self._broadcast(ItemRemovePacket(type="item_remove", itemId=item.id))
|
||||||
|
|||||||
Reference in New Issue
Block a user