diff --git a/client/public/version.js b/client/public/version.js index 8113647..1535e81 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,3 +1,3 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.20 R60"; +window.CHGRID_WEB_VERSION = "2026.02.20 R61"; diff --git a/deploy/README.md b/deploy/README.md index bb7c23e..4b90f3c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -114,3 +114,35 @@ sudo /usr/local/cpanel/scripts/restartsrv_httpd Usage example in Chat Grid: - `https://bestmidi.com/listen/8000/stream` + +## 8) GitHub-based update flow (`bestmidi`) + +Initial clone (one time): + +```bash +cd /home/bestmidi +git clone git@github.com:jage9/chat_grid.git chgrid +``` + +Update and redeploy: + +```bash +cd /home/bestmidi/chgrid +git fetch origin +git checkout main +git pull --ff-only origin main + +# Rebuild/publish web client +./deploy/scripts/deploy_client.sh /home/bestmidi/chgrid /home/bestmidi/public_html/chgrid /chgrid/ + +# Reconcile server env/deps (safe to rerun on updates) +./deploy/scripts/install_server.sh /home/bestmidi/chgrid + +# Restart signaling service +sudo systemctl restart chgrid-signaling.service +journalctl -u chgrid-signaling.service -n 50 --no-pager +``` + +Notes: +- Run Apache install/reload steps again only if proxy config changed. +- If your checkout has local changes, stash or commit before `git pull`. diff --git a/server/app/item_catalog.py b/server/app/item_catalog.py index f61edf3..2ed28c8 100644 --- a/server/app/item_catalog.py +++ b/server/app/item_catalog.py @@ -12,6 +12,7 @@ class ItemDefinition: capabilities: tuple[str, ...] use_sound: str | None default_params: dict + use_cooldown_ms: int = 1000 ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = { @@ -32,3 +33,11 @@ ITEM_DEFINITIONS: dict[ItemType, ItemDefinition] = { def get_item_definition(item_type: ItemType) -> ItemDefinition: return ITEM_DEFINITIONS[item_type] + + +def get_item_use_cooldown_ms(item_type: ItemType) -> int: + definition = get_item_definition(item_type) + cooldown_ms = definition.use_cooldown_ms + if isinstance(cooldown_ms, int) and cooldown_ms > 0: + return cooldown_ms + return 1000 diff --git a/server/app/server.py b/server/app/server.py index d7bc319..5283f3b 100644 --- a/server/app/server.py +++ b/server/app/server.py @@ -15,6 +15,7 @@ from websockets.asyncio.server import ServerConnection, serve from .client import ClientConnection from .config import load_config +from .item_catalog import get_item_use_cooldown_ms from .item_service import ItemService from .models import ( BroadcastChatMessagePacket, @@ -66,6 +67,7 @@ class SignalingServer: self._ssl_context = self._build_ssl_context(ssl_cert, ssl_key) self.clients: dict[ServerConnection, ClientConnection] = {} self.item_service = ItemService(state_file=state_file) + self.item_last_use_ms: dict[str, int] = {} @property def items(self) -> dict[str, WorldItem]: @@ -369,6 +371,7 @@ class SignalingServer: await self._send_item_result(client, False, "delete", "Item is not on your square.", item.id) return self.item_service.remove_item(item.id) + self.item_last_use_ms.pop(item.id, None) await self._broadcast(ItemRemovePacket(type="item_remove", itemId=item.id)) self.item_service.save_state() await self._send_item_result(client, True, "delete", f"Deleted {item.title}.", item.id) @@ -388,6 +391,20 @@ class SignalingServer: if item.type != "dice": await self._send_item_result(client, False, "use", "This item cannot be used yet.", item.id) return + now_ms = self.item_service.now_ms() + cooldown_ms = get_item_use_cooldown_ms(item.type) + last_use_ms = self.item_last_use_ms.get(item.id) + if last_use_ms is not None and now_ms - last_use_ms < cooldown_ms: + remaining_ms = cooldown_ms - (now_ms - last_use_ms) + await self._send_item_result( + client, + False, + "use", + f"Item is on cooldown for {max(1, remaining_ms)} ms.", + item.id, + ) + return + self.item_last_use_ms[item.id] = now_ms try: sides = max(1, min(100, int(item.params.get("sides", 6)))) number = max(1, min(100, int(item.params.get("number", 2)))) diff --git a/server/tests/test_item_use_cooldown.py b/server/tests/test_item_use_cooldown.py new file mode 100644 index 0000000..c24d721 --- /dev/null +++ b/server/tests/test_item_use_cooldown.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import json +from typing import cast + +import pytest +from websockets.asyncio.server import ServerConnection + +from app.server import ClientConnection, SignalingServer + + +def _fake_ws() -> ServerConnection: + return cast(ServerConnection, object()) + + +@pytest.mark.asyncio +async def test_item_use_has_global_cooldown(monkeypatch: pytest.MonkeyPatch) -> None: + server = SignalingServer("127.0.0.1", 8765, None, None) + ws = _fake_ws() + client = ClientConnection(websocket=ws, id="u1", nickname="tester", x=5, y=6) + server.clients[ws] = client + item = server.item_service.default_item(client, "dice") + server.item_service.add_item(item) + + send_payloads: list[object] = [] + now_ms = 10_000 + + async def fake_send(websocket: ServerConnection, packet: object) -> None: + send_payloads.append(packet) + + async def fake_broadcast(packet: object, exclude: ServerConnection | None = None) -> None: + return + + monkeypatch.setattr(server, "_send", fake_send) + monkeypatch.setattr(server, "_broadcast", fake_broadcast) + monkeypatch.setattr(server.item_service, "now_ms", lambda: now_ms) + + await server._handle_message(client, json.dumps({"type": "item_use", "itemId": item.id})) + assert send_payloads[-1].ok is True + + now_ms += 400 + await server._handle_message(client, json.dumps({"type": "item_use", "itemId": item.id})) + assert send_payloads[-1].ok is False + assert "cooldown" in send_payloads[-1].message.lower() + + now_ms += 700 + await server._handle_message(client, json.dumps({"type": "item_use", "itemId": item.id})) + assert send_payloads[-1].ok is True