try to fix multiuser sync
This commit is contained in:
@@ -19,3 +19,7 @@ docker/loki-data/
|
|||||||
docker/loki-wal/
|
docker/loki-wal/
|
||||||
# Local runtime logs collected by Promtail
|
# Local runtime logs collected by Promtail
|
||||||
/logs/
|
/logs/
|
||||||
|
|
||||||
|
# Compiled assets for DV3D
|
||||||
|
**/dv3d/assets/
|
||||||
|
/static/dv3d/index.js
|
||||||
|
|||||||
@@ -11,14 +11,19 @@
|
|||||||
<!-- Session Header -->
|
<!-- Session Header -->
|
||||||
<div class="row align-items-center mb-3">
|
<div class="row align-items-center mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||||
<a href="{% url 'atlas:multiuser_dashboard' %}" class="btn btn-outline-secondary btn-sm" title="Back to Dashboard">
|
<a href="{% url 'atlas:multiuser_dashboard' %}" class="btn btn-outline-secondary btn-sm" title="Back to Dashboard">
|
||||||
<i class="bi bi-arrow-left"></i>
|
<i class="bi bi-arrow-left"></i>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="h4 mb-0 text-white">
|
<h1 class="h4 mb-0 text-white">
|
||||||
<i class="bi bi-people text-info me-2"></i>{{ session.name|default:"Unnamed Session" }}
|
<i class="bi bi-people text-info me-2"></i>{{ session.name|default:"Unnamed Session" }}
|
||||||
</h1>
|
</h1>
|
||||||
<span class="badge bg-secondary font-monospace small">{{ session.id|stringformat:".8s" }}</span>
|
<span class="badge bg-secondary font-monospace small me-2">{{ session.id|stringformat:".8s" }}</span>
|
||||||
|
<!-- Debug Mode Toggle -->
|
||||||
|
<div class="form-check form-switch small text-light mb-0 d-flex align-items-center gap-2 ms-md-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="syncDebugModeCheckbox" onchange="toggleSyncDebugMode(this.checked)">
|
||||||
|
<label class="form-check-label text-muted small" for="syncDebugModeCheckbox"><i class="bi bi-bug-fill text-info me-1"></i>Debug Mode</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted mt-1">
|
<div class="small text-muted mt-1">
|
||||||
Managed by <strong>{{ session.creator.get_full_name|default:session.creator.username }}</strong>
|
Managed by <strong>{{ session.creator.get_full_name|default:session.creator.username }}</strong>
|
||||||
@@ -102,17 +107,109 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Live Debug Logs Panel -->
|
||||||
|
<div id="syncDebugLogsPanel" class="card bg-dark border-info text-white mt-4" style="display: none;">
|
||||||
|
<div class="card-header bg-info bg-opacity-25 text-info d-flex justify-content-between align-items-center py-2 px-3">
|
||||||
|
<span class="fw-semibold"><i class="bi bi-terminal me-2"></i>Live Multiuser Sync Logs</span>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<button class="btn btn-outline-info btn-xs py-0 px-2 small" style="font-size: 0.75rem;" onclick="clearDebugLogs()">Clear</button>
|
||||||
|
<span class="badge bg-info text-dark font-monospace small">DEBUG MODE ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div id="syncDebugLogsContainer" class="bg-black text-success font-monospace p-3 small" style="height: 180px; overflow-y: auto; line-height: 1.5; font-size: 0.85rem;">
|
||||||
|
<div class="text-muted">Debug mode activated. Listening for WebSocket messages...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (typeof text !== "string") {
|
||||||
|
try {
|
||||||
|
text = JSON.stringify(text);
|
||||||
|
} catch (e) {
|
||||||
|
text = String(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function logDebug(message, data) {
|
||||||
|
console.log("[Multiuser Session]", message, data || "");
|
||||||
|
|
||||||
|
// Append to on-screen console if visible
|
||||||
|
const container = document.getElementById("syncDebugLogsContainer");
|
||||||
|
if (container) {
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
const line = document.createElement("div");
|
||||||
|
line.className = "mb-1 border-bottom border-secondary pb-1 border-opacity-25";
|
||||||
|
line.innerHTML = `<span class="text-muted">[${timestamp}]</span> <strong>${escapeHtml(message)}</strong>`;
|
||||||
|
if (data) {
|
||||||
|
line.innerHTML += ` <span class="text-info">${escapeHtml(data)}</span>`;
|
||||||
|
}
|
||||||
|
container.appendChild(line);
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDebugLogs() {
|
||||||
|
const container = document.getElementById("syncDebugLogsContainer");
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<div class="text-muted">Logs cleared.</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSyncDebugMode(enabled) {
|
||||||
|
localStorage.setItem("multiuser_sync_debug_mode", enabled ? "true" : "false");
|
||||||
|
|
||||||
|
const panel = document.getElementById("syncDebugLogsPanel");
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = enabled ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkbox = document.getElementById("syncDebugModeCheckbox");
|
||||||
|
if (checkbox && checkbox.checked !== enabled) {
|
||||||
|
checkbox.checked = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.setDicomViewerLogging === "function") {
|
||||||
|
window.setDicomViewerLogging({ minLevel: enabled ? "debug" : "warn" });
|
||||||
|
logDebug("DV3D viewer log level set to " + (enabled ? "debug" : "warn"));
|
||||||
|
} else {
|
||||||
|
logDebug("DV3D viewer logging control not ready yet (will apply when viewer mounts).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set invite link input on load
|
// Set invite link input on load
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const inviteInput = document.getElementById("inviteLinkInput");
|
const inviteInput = document.getElementById("inviteLinkInput");
|
||||||
if (inviteInput) {
|
if (inviteInput) {
|
||||||
inviteInput.value = window.location.href;
|
inviteInput.value = window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize Debug Mode from LocalStorage
|
||||||
|
const debugSaved = localStorage.getItem("multiuser_sync_debug_mode") === "true";
|
||||||
|
toggleSyncDebugMode(debugSaved);
|
||||||
|
|
||||||
|
// Keep attempting to apply log level as viewer mounts
|
||||||
|
const checkInterval = setInterval(function() {
|
||||||
|
if (typeof window.setDicomViewerLogging === "function") {
|
||||||
|
const isDebug = localStorage.getItem("multiuser_sync_debug_mode") === "true";
|
||||||
|
window.setDicomViewerLogging({ minLevel: isDebug ? "debug" : "warn" });
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
setTimeout(function() { clearInterval(checkInterval); }, 15000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy to clipboard helper
|
// Copy to clipboard helper
|
||||||
@@ -147,47 +244,60 @@
|
|||||||
|
|
||||||
// Session-level WebSocket to listen for control signals
|
// Session-level WebSocket to listen for control signals
|
||||||
(function() {
|
(function() {
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
let socket = null;
|
||||||
const host = window.location.host || "localhost:8080";
|
|
||||||
const wsUrl = `${protocol}//${host}/ws/multiuser/{{ session.id }}/`;
|
function connect() {
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
console.log("Connecting session WS to", wsUrl);
|
const host = window.location.host || "localhost:8080";
|
||||||
const socket = new WebSocket(wsUrl);
|
const wsUrl = `${protocol}//${host}/ws/multiuser/{{ session.id }}/?client_type=session`;
|
||||||
|
|
||||||
socket.onmessage = function(event) {
|
logDebug("Connecting session WS to", wsUrl);
|
||||||
try {
|
socket = new WebSocket(wsUrl);
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
console.log("Session WS event:", data.type, data);
|
socket.onopen = function() {
|
||||||
|
logDebug("Session WS connected successfully");
|
||||||
if (data.type === 'change_case') {
|
};
|
||||||
console.log("Session host changed case to", data.case_id);
|
|
||||||
// Fetch new case content via HTMX
|
socket.onmessage = function(event) {
|
||||||
htmx.ajax('GET', "{% url 'atlas:multiuser_case_content_partial' session_id=session.id %}", {
|
try {
|
||||||
target: "#multiuser-case-container",
|
const data = JSON.parse(event.data);
|
||||||
swap: "innerHTML"
|
logDebug("Session WS event received (type=" + data.type + ")", data);
|
||||||
});
|
|
||||||
} else if (data.type === 'room_status') {
|
|
||||||
// Update user count
|
|
||||||
const userCount = Object.keys(data.users || {}).length;
|
|
||||||
const mgrCountBadge = document.getElementById("connectedUsersCount");
|
|
||||||
const partCountBadge = document.getElementById("connectedUsersCountParticipant");
|
|
||||||
|
|
||||||
if (mgrCountBadge) mgrCountBadge.textContent = userCount;
|
if (data.type === 'change_case') {
|
||||||
if (partCountBadge) partCountBadge.textContent = userCount;
|
logDebug("Session host changed case to " + data.case_id + ". Fetching new case via HTMX...");
|
||||||
|
// Fetch new case content via HTMX
|
||||||
|
htmx.ajax('GET', "{% url 'atlas:multiuser_case_content_partial' session_id=session.id %}", {
|
||||||
|
target: "#multiuser-case-container",
|
||||||
|
swap: "innerHTML"
|
||||||
|
});
|
||||||
|
} else if (data.type === 'room_status') {
|
||||||
|
// Update user count
|
||||||
|
const userCount = Object.keys(data.users || {}).length;
|
||||||
|
logDebug("Room status update. Active users: " + userCount, data.users);
|
||||||
|
const mgrCountBadge = document.getElementById("connectedUsersCount");
|
||||||
|
const partCountBadge = document.getElementById("connectedUsersCountParticipant");
|
||||||
|
|
||||||
|
if (mgrCountBadge) mgrCountBadge.textContent = userCount;
|
||||||
|
if (partCountBadge) partCountBadge.textContent = userCount;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logDebug("Error processing session WS message: " + err.message);
|
||||||
|
console.error("Error processing session WS message", err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
};
|
||||||
console.error("Error processing session WS message", err);
|
|
||||||
}
|
socket.onclose = function(e) {
|
||||||
};
|
logDebug("Session WS closed (code=" + e.code + "). Reconnecting in 3s...");
|
||||||
|
setTimeout(connect, 3000);
|
||||||
socket.onclose = function(e) {
|
};
|
||||||
console.warn("Session WS closed. Reconnecting in 3s...", e);
|
|
||||||
setTimeout(arguments.callee, 3000);
|
socket.onerror = function(err) {
|
||||||
};
|
logDebug("Session WS error encountered");
|
||||||
|
console.error("Session WS error:", err);
|
||||||
socket.onerror = function(err) {
|
};
|
||||||
console.error("Session WS error:", err);
|
}
|
||||||
};
|
|
||||||
|
connect();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+99
-11
@@ -1,12 +1,11 @@
|
|||||||
import logging
|
from loguru import logger
|
||||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from atlas.models import MultiuserSession
|
from atlas.models import MultiuserSession
|
||||||
|
import urllib.parse
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
@@ -24,7 +23,7 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
# Get session from DB using self.room_name
|
# Get session from DB using self.room_name
|
||||||
session = await self.get_session(self.room_name)
|
session = await self.get_session(self.room_name)
|
||||||
if not session:
|
if not session:
|
||||||
# Reject connection if session does not exist
|
logger.warning("[Multiuser WS] Rejecting connection: session {} not found", self.room_name)
|
||||||
await self.close()
|
await self.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -37,6 +36,16 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
self.username = f"Guest_{self.channel_name[-4:]}"
|
self.username = f"Guest_{self.channel_name[-4:]}"
|
||||||
self.is_staff = False
|
self.is_staff = False
|
||||||
|
|
||||||
|
# Parse query string for client_type (default to 'viewer')
|
||||||
|
query_string = self.scope.get('query_string', b'').decode('utf-8')
|
||||||
|
params = urllib.parse.parse_qs(query_string)
|
||||||
|
self.client_type = params.get('client_type', ['viewer'])[0]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Connect request: room={}, user={}, type={}, channel={}",
|
||||||
|
self.room_name, self.username, self.client_type, self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
# Add to channels group
|
# Add to channels group
|
||||||
await self.channel_layer.group_add(
|
await self.channel_layer.group_add(
|
||||||
self.room_group_name,
|
self.room_group_name,
|
||||||
@@ -48,11 +57,21 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'channel_name': self.channel_name
|
'channel_name': self.channel_name
|
||||||
})
|
})
|
||||||
|
|
||||||
# Update room state in cache & notify others
|
if self.client_type == 'viewer':
|
||||||
await self.add_user_to_room()
|
# Update room state in cache & notify others
|
||||||
await self.broadcast_room_status()
|
await self.add_user_to_room()
|
||||||
|
await self.broadcast_room_status()
|
||||||
|
else:
|
||||||
|
# Passive session listener: just broadcast the current room status to this new client
|
||||||
|
logger.info("[Multiuser WS] Passive listener connected: {}", self.channel_name)
|
||||||
|
await self.broadcast_room_status()
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Disconnect: room={}, user={}, type={}, channel={}, code={}",
|
||||||
|
self.room_name, self.username, self.client_type, self.channel_name, close_code
|
||||||
|
)
|
||||||
|
|
||||||
# Remove from channels group
|
# Remove from channels group
|
||||||
await self.channel_layer.group_discard(
|
await self.channel_layer.group_discard(
|
||||||
self.room_group_name,
|
self.room_group_name,
|
||||||
@@ -60,15 +79,28 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update room state in cache & notify others
|
# Update room state in cache & notify others
|
||||||
await self.remove_user_from_room()
|
if self.client_type == 'viewer':
|
||||||
await self.broadcast_room_status()
|
await self.remove_user_from_room()
|
||||||
|
await self.broadcast_room_status()
|
||||||
|
|
||||||
async def receive_json(self, content):
|
async def receive_json(self, content):
|
||||||
msg_type = content.get('type')
|
msg_type = content.get('type')
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Received message from user={} (type={}): {}",
|
||||||
|
self.username, msg_type, content
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.client_type != 'viewer':
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] Rejected message from passive listener user={} (type={})",
|
||||||
|
self.username, msg_type
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if msg_type == 'viewer_state':
|
if msg_type == 'viewer_state':
|
||||||
# Replicate view state changes from the active controller to others
|
# Replicate view state changes from the active controller to others
|
||||||
if await self.has_control():
|
if await self.has_control():
|
||||||
|
logger.debug("[Multiuser WS] Broadcasting viewer_state from user={}", self.username)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.room_group_name,
|
self.room_group_name,
|
||||||
{
|
{
|
||||||
@@ -77,9 +109,15 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'data': content.get('data')
|
'data': content.get('data')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] viewer_state rejected: user={} does not have control",
|
||||||
|
self.username
|
||||||
|
)
|
||||||
elif msg_type == 'annotations':
|
elif msg_type == 'annotations':
|
||||||
# Replicate annotation changes from the active controller to others
|
# Replicate annotation changes from the active controller to others
|
||||||
if await self.has_control():
|
if await self.has_control():
|
||||||
|
logger.debug("[Multiuser WS] Broadcasting annotations from user={}", self.username)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.room_group_name,
|
self.room_group_name,
|
||||||
{
|
{
|
||||||
@@ -88,8 +126,14 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'data': content.get('data')
|
'data': content.get('data')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] annotations rejected: user={} does not have control",
|
||||||
|
self.username
|
||||||
|
)
|
||||||
elif msg_type == 'request_control':
|
elif msg_type == 'request_control':
|
||||||
# Broadcast a control request to all participants (so the admin/controller can grant it)
|
# Broadcast a control request to all participants (so the admin/controller can grant it)
|
||||||
|
logger.info("[Multiuser WS] Control request from user={}", self.username)
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
self.room_group_name,
|
self.room_group_name,
|
||||||
{
|
{
|
||||||
@@ -101,25 +145,47 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
elif msg_type == 'take_control':
|
elif msg_type == 'take_control':
|
||||||
# Supervisors/admins can immediately assume control
|
# Supervisors/admins can immediately assume control
|
||||||
if self.is_staff:
|
if self.is_staff:
|
||||||
|
logger.info("[Multiuser WS] Supervisor user={} taking control", self.username)
|
||||||
await self.set_controller(self.channel_name, self.username)
|
await self.set_controller(self.channel_name, self.username)
|
||||||
await self.broadcast_room_status()
|
await self.broadcast_room_status()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] take_control rejected: user={} is not supervisor/staff",
|
||||||
|
self.username
|
||||||
|
)
|
||||||
elif msg_type == 'grant_control':
|
elif msg_type == 'grant_control':
|
||||||
# The active controller or a supervisor can hand over control to someone else
|
# The active controller or a supervisor can hand over control to someone else
|
||||||
if self.is_staff or await self.has_control():
|
if self.is_staff or await self.has_control():
|
||||||
target_channel = content.get('target_channel')
|
target_channel = content.get('target_channel')
|
||||||
target_username = content.get('target_username')
|
target_username = content.get('target_username')
|
||||||
if target_channel:
|
if target_channel:
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Control granted by user={} to target_user={} ({})",
|
||||||
|
self.username, target_username, target_channel
|
||||||
|
)
|
||||||
await self.set_controller(target_channel, target_username)
|
await self.set_controller(target_channel, target_username)
|
||||||
await self.broadcast_room_status()
|
await self.broadcast_room_status()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] grant_control rejected: user={} is not controller/staff",
|
||||||
|
self.username
|
||||||
|
)
|
||||||
elif msg_type == 'release_control':
|
elif msg_type == 'release_control':
|
||||||
# Relinquish control
|
# Relinquish control
|
||||||
if await self.has_control():
|
if await self.has_control():
|
||||||
|
logger.info("[Multiuser WS] Control released by user={}", self.username)
|
||||||
await self.set_controller(None, None)
|
await self.set_controller(None, None)
|
||||||
await self.broadcast_room_status()
|
await self.broadcast_room_status()
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"[Multiuser WS] release_control rejected: user={} is not active controller",
|
||||||
|
self.username
|
||||||
|
)
|
||||||
|
|
||||||
# Channels group event handlers
|
# Channels group event handlers
|
||||||
async def viewer_state_update(self, event):
|
async def viewer_state_update(self, event):
|
||||||
if event['sender'] != self.channel_name:
|
if event['sender'] != self.channel_name:
|
||||||
|
logger.debug("[Multiuser WS] Forwarding viewer_state_update to channel={}", self.channel_name)
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'viewer_state',
|
'type': 'viewer_state',
|
||||||
'data': event['data']
|
'data': event['data']
|
||||||
@@ -127,12 +193,14 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
|
|
||||||
async def annotations_update(self, event):
|
async def annotations_update(self, event):
|
||||||
if event['sender'] != self.channel_name:
|
if event['sender'] != self.channel_name:
|
||||||
|
logger.debug("[Multiuser WS] Forwarding annotations_update to channel={}", self.channel_name)
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'annotations',
|
'type': 'annotations',
|
||||||
'data': event['data']
|
'data': event['data']
|
||||||
})
|
})
|
||||||
|
|
||||||
async def control_request(self, event):
|
async def control_request(self, event):
|
||||||
|
logger.debug("[Multiuser WS] Forwarding control_request to channel={}", self.channel_name)
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'control_request',
|
'type': 'control_request',
|
||||||
'sender_name': event['sender_name'],
|
'sender_name': event['sender_name'],
|
||||||
@@ -140,6 +208,7 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
})
|
})
|
||||||
|
|
||||||
async def room_status_update(self, event):
|
async def room_status_update(self, event):
|
||||||
|
logger.debug("[Multiuser WS] Forwarding room_status_update to channel={}", self.channel_name)
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'room_status',
|
'type': 'room_status',
|
||||||
'users': event['users'],
|
'users': event['users'],
|
||||||
@@ -148,6 +217,7 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
})
|
})
|
||||||
|
|
||||||
async def change_case(self, event):
|
async def change_case(self, event):
|
||||||
|
logger.info("[Multiuser WS] Forwarding change_case (case_id={}) to channel={}", event['case_id'], self.channel_name)
|
||||||
await self.send_json({
|
await self.send_json({
|
||||||
'type': 'change_case',
|
'type': 'change_case',
|
||||||
'case_id': event['case_id']
|
'case_id': event['case_id']
|
||||||
@@ -174,15 +244,28 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
'controller_username': None
|
'controller_username': None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove any existing channel names for the same username to prevent duplicate presence counts on refresh
|
||||||
|
for old_channel in list(state['users'].keys()):
|
||||||
|
if state['users'][old_channel]['username'] == self.username:
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Pruning stale channel name {} for user={}",
|
||||||
|
old_channel, self.username
|
||||||
|
)
|
||||||
|
state['users'].pop(old_channel, None)
|
||||||
|
|
||||||
state['users'][self.channel_name] = {
|
state['users'][self.channel_name] = {
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
'is_staff': self.is_staff
|
'is_staff': self.is_staff
|
||||||
}
|
}
|
||||||
|
|
||||||
# Auto-assign control if no active controller exists
|
# Auto-assign control if no active controller exists or the previous controller was pruned
|
||||||
if not state['controller_channel']:
|
if not state['controller_channel'] or state['controller_channel'] not in state['users']:
|
||||||
state['controller_channel'] = self.channel_name
|
state['controller_channel'] = self.channel_name
|
||||||
state['controller_username'] = self.username
|
state['controller_username'] = self.username
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Control assigned to user={} (channel={})",
|
||||||
|
self.username, self.channel_name
|
||||||
|
)
|
||||||
|
|
||||||
await self._set_state(state)
|
await self._set_state(state)
|
||||||
|
|
||||||
@@ -196,9 +279,14 @@ class MultiuserConsumer(AsyncJsonWebsocketConsumer):
|
|||||||
next_channel = list(state['users'].keys())[0]
|
next_channel = list(state['users'].keys())[0]
|
||||||
state['controller_channel'] = next_channel
|
state['controller_channel'] = next_channel
|
||||||
state['controller_username'] = state['users'][next_channel]['username']
|
state['controller_username'] = state['users'][next_channel]['username']
|
||||||
|
logger.info(
|
||||||
|
"[Multiuser WS] Control passed to user={} (channel={})",
|
||||||
|
state['controller_username'], next_channel
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
state['controller_channel'] = None
|
state['controller_channel'] = None
|
||||||
state['controller_username'] = None
|
state['controller_username'] = None
|
||||||
|
logger.info("[Multiuser WS] Room is empty, control cleared")
|
||||||
|
|
||||||
await self._set_state(state)
|
await self._set_state(state)
|
||||||
|
|
||||||
|
|||||||
+68
-68
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user