Compare commits

...

2 Commits

Author SHA1 Message Date
9821239b39 feat: client-side portal hot-reload handler
Some checks failed
Review Approval Gate / verify-review (pull_request) Successful in 16s
CI / validate (pull_request) Failing after 1m7s
CI / test (pull_request) Failing after 1m39s
- Listens for 'portals:reload' WS message
- Removes old portals from scene
- Re-fetches portals.json with cache-bust
- Recreates all portal objects
- Announces reload in chat
2026-04-15 03:40:04 +00:00
06ece30ea9 feat: add portals.json file watcher with WS broadcast
Watches portals.json every 2 seconds for mtime changes.
Broadcasts {type: 'portals:reload'} to all connected clients.
Existing connections unaffected.
2026-04-15 03:35:43 +00:00
2 changed files with 71 additions and 1 deletions

27
app.js
View File

@@ -1534,6 +1534,27 @@ function createPortals(data) {
});
}
async function reloadPortals() {
// Remove existing portals from scene
portals.forEach(portal => {
scene.remove(portal.group);
});
portals.length = 0;
activePortal = null;
// Re-fetch and recreate
try {
const response = await fetch('./portals.json?' + Date.now()); // cache-bust
const portalData = await response.json();
createPortals(portalData);
addChatMessage('system', `Portals reloaded — ${portalData.length} worlds online.`);
console.log(`[portals] Reloaded ${portalData.length} portals`);
} catch (e) {
console.error('[portals] Reload failed:', e);
addChatMessage('error', 'Portal reload failed. Check portals.json.');
}
}
function createPortal(config) {
const group = new THREE.Group();
group.position.set(config.position.x, config.position.y, config.position.z);
@@ -2223,6 +2244,12 @@ function connectHermes() {
const data = JSON.parse(evt.data);
handleHermesMessage(data);
// Portal hot-reload: server detected portals.json change
if (data.type === 'portals:reload') {
console.log('[portals] Hot-reload triggered');
reloadPortals();
}
// Store in MemPalace
if (data.type === 'chat') {
// Store in MemPalace with AAAK compression

View File

@@ -7,8 +7,10 @@ the body (Evennia/Morrowind), and the visualization surface.
import asyncio
import json
import logging
import os
import signal
import sys
from pathlib import Path
from typing import Set
# Branch protected file - see POLICY.md
@@ -79,6 +81,42 @@ async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
clients.discard(websocket)
logger.info(f"Client disconnected {addr}. Total clients: {len(clients)}")
# Portal hot-reload state
_portals_mtime: float = 0.0
_portals_path: Path = Path(__file__).parent / "portals.json"
async def _watch_portals():
"""Watch portals.json for changes and broadcast reload to all clients."""
global _portals_mtime
try:
if _portals_path.exists():
_portals_mtime = _portals_path.stat().st_mtime
except OSError:
pass
while True:
await asyncio.sleep(2) # Check every 2 seconds
try:
if not _portals_path.exists():
continue
current_mtime = _portals_path.stat().st_mtime
if current_mtime != _portals_mtime:
_portals_mtime = current_mtime
logger.info("portals.json changed — broadcasting reload")
msg = json.dumps({"type": "portals:reload", "ts": current_mtime})
disconnected = set()
for client in clients:
if client.open:
try:
await client.send(msg)
except Exception:
disconnected.add(client)
if disconnected:
clients.difference_update(disconnected)
except OSError as e:
logger.warning(f"Portal watch error: {e}")
async def main():
"""Main server loop with graceful shutdown."""
logger.info(f"Starting Nexus WS gateway on ws://{HOST}:{PORT}")
@@ -99,8 +137,13 @@ async def main():
pass
async with websockets.serve(broadcast_handler, HOST, PORT):
logger.info("Gateway is ready and listening.")
# Start portal file watcher
watcher_task = asyncio.create_task(_watch_portals())
logger.info("Portal hot-reload watcher started.")
logger.info("Gateway is ready and listening.")
await stop
watcher_task.cancel()
logger.info("Shutting down Nexus WS gateway...")
# Close any remaining client connections (handlers may have already cleaned up)