@colyseus/sdk 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/build/cjs/3rd_party/discord.js +1 -1
  2. package/build/cjs/Auth.js +1 -1
  3. package/build/cjs/Client.js +1 -1
  4. package/build/cjs/Connection.js +1 -1
  5. package/build/cjs/HTTP.js +1 -1
  6. package/build/cjs/Protocol.js +1 -1
  7. package/build/cjs/Room.js +1 -1
  8. package/build/cjs/Storage.js +1 -1
  9. package/build/cjs/core/nanoevents.js +1 -1
  10. package/build/cjs/core/signal.js +1 -1
  11. package/build/cjs/core/utils.js +1 -1
  12. package/build/cjs/errors/Errors.js +1 -1
  13. package/build/cjs/index.js +1 -1
  14. package/build/cjs/legacy.js +1 -1
  15. package/build/cjs/serializer/NoneSerializer.js +1 -1
  16. package/build/cjs/serializer/SchemaSerializer.js +1 -1
  17. package/build/cjs/serializer/Serializer.js +1 -1
  18. package/build/cjs/transport/H3Transport.js +1 -1
  19. package/build/cjs/transport/WebSocketTransport.js +1 -1
  20. package/build/esm/3rd_party/discord.mjs +1 -1
  21. package/build/esm/Auth.mjs +1 -1
  22. package/build/esm/Client.mjs +1 -1
  23. package/build/esm/Connection.mjs +1 -1
  24. package/build/esm/HTTP.mjs +1 -1
  25. package/build/esm/Protocol.mjs +1 -1
  26. package/build/esm/Room.mjs +1 -1
  27. package/build/esm/Storage.mjs +1 -1
  28. package/build/esm/core/nanoevents.mjs +1 -1
  29. package/build/esm/core/signal.mjs +1 -1
  30. package/build/esm/core/utils.mjs +1 -1
  31. package/build/esm/errors/Errors.mjs +1 -1
  32. package/build/esm/index.mjs +1 -1
  33. package/build/esm/legacy.mjs +1 -1
  34. package/build/esm/serializer/NoneSerializer.mjs +1 -1
  35. package/build/esm/serializer/SchemaSerializer.mjs +1 -1
  36. package/build/esm/serializer/Serializer.mjs +1 -1
  37. package/build/esm/transport/H3Transport.mjs +1 -1
  38. package/build/esm/transport/WebSocketTransport.mjs +1 -1
  39. package/dist/colyseus-cocos-creator.js +1 -1
  40. package/dist/colyseus.js +1 -1
  41. package/dist/debug.js +1 -1
  42. package/lib/core/http_bkp.d.ts +10 -10
  43. package/package.json +7 -6
  44. package/src/3rd_party/discord.ts +48 -0
  45. package/src/Auth.ts +177 -0
  46. package/src/Client.ts +459 -0
  47. package/src/Connection.ts +51 -0
  48. package/src/HTTP.ts +545 -0
  49. package/src/HTTP_bkp.ts +67 -0
  50. package/src/Protocol.ts +25 -0
  51. package/src/Room.ts +505 -0
  52. package/src/Storage.ts +94 -0
  53. package/src/core/http_bkp.ts +358 -0
  54. package/src/core/nanoevents.ts +38 -0
  55. package/src/core/signal.ts +62 -0
  56. package/src/core/utils.ts +3 -0
  57. package/src/debug.ts +2743 -0
  58. package/src/errors/Errors.ts +29 -0
  59. package/src/index.ts +18 -0
  60. package/src/legacy.ts +29 -0
  61. package/src/serializer/FossilDeltaSerializer.ts +39 -0
  62. package/src/serializer/NoneSerializer.ts +9 -0
  63. package/src/serializer/SchemaSerializer.ts +61 -0
  64. package/src/serializer/Serializer.ts +23 -0
  65. package/src/transport/H3Transport.ts +199 -0
  66. package/src/transport/ITransport.ts +18 -0
  67. package/src/transport/WebSocketTransport.ts +53 -0
package/src/debug.ts ADDED
@@ -0,0 +1,2743 @@
1
+ import { Client } from "./Client";
2
+ import type { Room } from "./Room";
3
+ import type { WebSocketTransport } from "./transport/WebSocketTransport";
4
+
5
+ const logoIcon = `<svg viewBox="0 0 488.94 541.2" style="width: 100%; height: 100%;">
6
+ <g>
7
+ <g>
8
+ <path fill="#ffffff" d="m80.42,197.14c13.82,11.25,30.56,22.25,50.78,32.11,14.87-28.67,72.09-100.71,233.32-79.68l-14.4-55.35c-200.24-17.18-257.81,77.11-269.7,102.92Z"/>
9
+ <path fill="#ffffff" d="m44.53,167.77c22.44-40.73,99.17-124.23,290.19-105.83L310.19,1.59S109.9-21.63,8.9,109.47c3.62,10.55,13.31,33.34,35.63,58.29Z"/>
10
+ <path fill="#ffffff" d="m407.7,291.25c-32.14,3.35-62.02,4.95-89.63,4.95C123.09,296.2,36.78,219.6,0,164.95v251.98s15.77,162.98,488.94,115.66l-81.24-241.33Z"/>
11
+ </g>
12
+ </g>
13
+ </svg>`;
14
+ const envelopeUp = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4.5a.5.5 0 0 1-1 0V5.383l-7 4.2-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h5.5a.5.5 0 0 1 0 1H2a2 2 0 0 1-2-1.99zm1 7.105 4.708-2.897L1 5.383zM1 4v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1"></path><path d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m.354-5.354 1.25 1.25a.5.5 0 0 1-.708.708L13 12.207V14a.5.5 0 0 1-1 0v-1.717l-.28.305a.5.5 0 0 1-.737-.676l1.149-1.25a.5.5 0 0 1 .722-.016"></path></svg>`;
15
+ const envelopeDown = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4.5a.5.5 0 0 1-1 0V5.383l-7 4.2-1.326-.795-5.64 3.47A1 1 0 0 0 2 13h5.5a.5.5 0 0 1 0 1H2a2 2 0 0 1-2-1.99zm1 7.105 4.708-2.897L1 5.383zM1 4v.217l7 4.2 7-4.2V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1"></path><path d="M12.5 16a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7m.354-1.646a.5.5 0 0 1-.722-.016l-1.149-1.25a.5.5 0 1 1 .737-.676l.28.305V11a.5.5 0 0 1 1 0v1.793l.396-.397a.5.5 0 0 1 .708.708z"></path></svg>`;
16
+ const messageIcon = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M498.1 5.6c10.1 7 15.4 19.1 13.5 31.2l-64 416c-1.5 9.7-7.4 18.2-16 23s-18.9 5.4-28 1.6L284 427.7l-68.5 74.1c-8.9 9.7-22.9 12.9-35.2 8.1S160 493.2 160 480V396.4c0-4 1.5-7.8 4.2-10.7L331.8 202.8c5.8-6.3 5.6-16-.4-22s-15.7-6.4-22-.7L106 360.8 17.7 316.6C7.1 311.3 .3 300.7 0 288.9s5.9-22.8 16.1-28.7l448-256c10.7-6.1 23.9-5.5 34 1.4z"/></svg>`;
17
+ const treeViewIcon = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 256 256" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M160,136v-8H88v64a8,8,0,0,0,8,8h64v-8a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16v32a16,16,0,0,1-16,16H176a16,16,0,0,1-16-16v-8H96a24,24,0,0,1-24-24V80H64A16,16,0,0,1,48,64V32A16,16,0,0,1,64,16H96a16,16,0,0,1,16,16V64A16,16,0,0,1,96,80H88v32h72v-8a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16v32a16,16,0,0,1-16,16H176A16,16,0,0,1,160,136Z"></path></svg>`;
18
+ const infoIcon = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M256 48C141.2 48 48 141.2 48 256s93.2 208 208 208 208-93.2 208-208S370.8 48 256 48zm21 312h-42V235h42v125zm0-166h-42v-42h42v42z"></path></svg>`;
19
+ const settingsIcon = `<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M12.003 21c-.732 .001 -1.465 -.438 -1.678 -1.317a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c.886 .215 1.325 .957 1.318 1.694"></path><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"></path><path d="M19.001 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path><path d="M19.001 15.5v1.5"></path><path d="M19.001 21v1.5"></path><path d="M22.032 17.25l-1.299 .75"></path><path d="M17.27 20l-1.3 .75"></path><path d="M15.97 17.25l1.3 .75"></path><path d="M20.733 20l1.3 .75"></path></svg>`;
20
+ const eyeSlashIcon = `<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`;
21
+ const closeIcon = `<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M400 145.49 366.51 112 256 222.51 145.49 112 112 145.49 222.51 256 112 366.51 145.49 400 256 289.49 366.51 400 400 366.51 289.49 256 400 145.49z"></path></svg>`;
22
+
23
+ // Store debug info per room
24
+ const roomDebugInfo = new Map();
25
+
26
+ // Single interval for all panels
27
+ let globalUpdateInterval = null;
28
+
29
+ // Preferences state
30
+ const preferences = {
31
+ maxLatency: 350, // milliseconds
32
+ latencySimulation: {
33
+ enabled: false,
34
+ delay: 0 // milliseconds
35
+ },
36
+ panelPosition: {
37
+ position: 'top-right' // 'bottom-right', 'bottom-left', 'top-left', 'top-right'
38
+ }
39
+ };
40
+
41
+ // Load preferences from localStorage
42
+ function loadPreferences() {
43
+ try {
44
+ const savedPrefs = localStorage.getItem('colyseus-debug-preferences') || '{}';
45
+ const prefs = JSON.parse(savedPrefs);
46
+
47
+ // Load position
48
+ if (prefs.position && ['bottom-right', 'bottom-left', 'top-left', 'top-right'].includes(prefs.position)) {
49
+ preferences.panelPosition.position = prefs.position;
50
+ }
51
+
52
+ // Load latency
53
+ if (prefs.latency !== undefined && prefs.latency !== null) {
54
+ const latencyValue = parseInt(prefs.latency, 10);
55
+ if (!isNaN(latencyValue) && latencyValue >= 0 && latencyValue <= 500) {
56
+ preferences.latencySimulation.delay = latencyValue;
57
+ preferences.latencySimulation.enabled = latencyValue > 0;
58
+ }
59
+ }
60
+
61
+ // Load hidden state
62
+ if (prefs.hidden === true) {
63
+ panelsHidden = true;
64
+ }
65
+ } catch (e) {
66
+ // localStorage might not be available or JSON parse failed, ignore
67
+ }
68
+ }
69
+
70
+ // Save preferences to localStorage
71
+ function savePreferences() {
72
+ try {
73
+ localStorage.setItem('colyseus-debug-preferences', JSON.stringify({
74
+ position: preferences.panelPosition.position,
75
+ latency: preferences.latencySimulation.delay,
76
+ hidden: panelsHidden
77
+ }));
78
+ } catch (e) {
79
+ // localStorage might not be available, ignore
80
+ }
81
+ }
82
+
83
+ // Panel visibility state
84
+ let panelsHidden = false;
85
+
86
+ // Track open modals as an ordered stack (most recent at end)
87
+ let modalStack: any[] = [];
88
+ const BASE_MODAL_ZINDEX = 10000;
89
+
90
+ // Load preferences on script load
91
+ loadPreferences();
92
+
93
+ // Function to select a modal (bring to front)
94
+ function selectModal(modal) {
95
+ if (!modal) return;
96
+
97
+ // Remove modal from stack if already present
98
+ const index = modalStack.indexOf(modal);
99
+ if (index > -1) {
100
+ modalStack.splice(index, 1);
101
+ }
102
+
103
+ // Add to end of stack (most recent)
104
+ modalStack.push(modal);
105
+
106
+ // Update z-indexes for all modals based on their position in stack
107
+ modalStack.forEach((m, i) => {
108
+ if (document.body.contains(m)) {
109
+ m.style.zIndex = (BASE_MODAL_ZINDEX + i).toString();
110
+ }
111
+ });
112
+ }
113
+
114
+ // Function to remove modal from stack
115
+ function removeModalFromStack(modal) {
116
+ const index = modalStack.indexOf(modal);
117
+ if (index > -1) {
118
+ modalStack.splice(index, 1);
119
+ }
120
+ }
121
+
122
+ // Global ESC key handler - closes most recent modal
123
+ document.addEventListener('keydown', function(e) {
124
+ if (e.key === 'Escape' && modalStack.length > 0) {
125
+ // Get the most recent modal (top of stack)
126
+ const topModal = modalStack[modalStack.length - 1];
127
+ if (topModal && document.body.contains(topModal)) {
128
+ topModal.remove();
129
+ }
130
+ }
131
+ });
132
+
133
+ // Shared modal creation utilities
134
+ function createModalOverlay() {
135
+ var overlay = document.createElement('div');
136
+ overlay.style.position = 'fixed';
137
+ overlay.style.top = '0';
138
+ overlay.style.left = '0';
139
+ overlay.style.right = '0';
140
+ overlay.style.bottom = '0';
141
+ overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
142
+ overlay.style.zIndex = BASE_MODAL_ZINDEX.toString();
143
+ overlay.style.display = 'flex';
144
+ overlay.style.justifyContent = 'center';
145
+ overlay.style.alignItems = 'center';
146
+ return overlay;
147
+ }
148
+
149
+ function createModal(options) {
150
+ var opts = options || {};
151
+ var modal = document.createElement('div');
152
+
153
+ // Set ID if provided
154
+ if (opts.id) {
155
+ modal.id = opts.id;
156
+ }
157
+
158
+ // Base styles
159
+ modal.style.position = 'fixed';
160
+ modal.style.backgroundColor = opts.backgroundColor || '#1e1e1e';
161
+ modal.style.borderRadius = '8px';
162
+ modal.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.5)';
163
+ modal.style.color = '#fff';
164
+ modal.style.fontFamily = 'system-ui, -apple-system, sans-serif';
165
+ modal.style.zIndex = BASE_MODAL_ZINDEX.toString();
166
+ modal.style.display = 'flex';
167
+ modal.style.flexDirection = 'column';
168
+ modal.style.overflow = 'hidden';
169
+
170
+ // Size
171
+ if (opts.width) modal.style.width = opts.width;
172
+ if (opts.height) modal.style.height = opts.height;
173
+ if (opts.minWidth) modal.style.minWidth = opts.minWidth;
174
+ if (opts.minHeight) modal.style.minHeight = opts.minHeight;
175
+ if (opts.maxWidth) modal.style.maxWidth = opts.maxWidth;
176
+ if (opts.maxHeight) modal.style.maxHeight = opts.maxHeight;
177
+
178
+ // Position
179
+ modal.style.top = opts.top || '50%';
180
+ modal.style.left = opts.left || '50%';
181
+ modal.style.transform = opts.transform || 'translate(-50%, -50%)';
182
+
183
+ // Mark modal as selected when clicked
184
+ modal.addEventListener('mousedown', function(e) {
185
+ selectModal(modal);
186
+ });
187
+
188
+ // Mark modal as selected when opened
189
+ selectModal(modal);
190
+
191
+ // Override remove to cleanup modal from stack
192
+ var originalRemove = modal.remove.bind(modal);
193
+ modal.remove = function() {
194
+ // Remove from modal stack
195
+ removeModalFromStack(modal);
196
+
197
+ // Auto-cleanup room onLeave listener
198
+ if (opts.room && opts.trackOnLeave && opts.onLeaveCallback) {
199
+ var callbackToRemove = opts.onLeaveCallback.current || opts.onLeaveCallback;
200
+ opts.room.onLeave.remove(callbackToRemove);
201
+ }
202
+
203
+ if (opts.onRemove) {
204
+ opts.onRemove();
205
+ }
206
+ originalRemove();
207
+ };
208
+
209
+ return modal;
210
+ }
211
+
212
+ function createModalHeader(options) {
213
+ var opts = options || {};
214
+
215
+ var header = document.createElement('div');
216
+ header.style.display = 'flex';
217
+ header.style.justifyContent = 'space-between';
218
+ header.style.alignItems = 'center';
219
+ header.style.padding = opts.padding || '8px';
220
+ header.style.borderBottom = '1px solid rgba(255, 255, 255, 0.15)';
221
+ header.style.paddingBottom = opts.paddingBottom || '4px';
222
+ header.style.marginBottom = opts.marginBottom || '6px';
223
+ header.style.cursor = opts.draggable !== false ? 'move' : 'default';
224
+ header.style.userSelect = 'none';
225
+ header.style.flexShrink = '0';
226
+ header.style.position = 'relative';
227
+ header.style.zIndex = '1';
228
+
229
+ // Title
230
+ var title = document.createElement('div');
231
+ title.textContent = opts.title || '';
232
+ title.style.margin = '0';
233
+ title.style.fontSize = opts.titleSize || '11px';
234
+ title.style.fontWeight = 'bold';
235
+ title.style.fontFamily = opts.titleFont || 'monospace';
236
+ title.style.flex = '1';
237
+ title.style.display = 'flex';
238
+ title.style.alignItems = 'center';
239
+
240
+ // Status dot (optional)
241
+ if (opts.statusDot) {
242
+ var statusDot = document.createElement('div');
243
+ statusDot.style.width = '8px';
244
+ statusDot.style.height = '8px';
245
+ statusDot.style.borderRadius = '50%';
246
+ statusDot.style.marginRight = '8px';
247
+ statusDot.style.flexShrink = '0';
248
+ statusDot.style.transition = 'background-color 0.3s';
249
+ statusDot.style.backgroundColor = opts.statusColor || '#22c55e';
250
+ title.insertBefore(statusDot, title.firstChild);
251
+
252
+ if (opts.statusDotRef) {
253
+ opts.statusDotRef.element = statusDot;
254
+ }
255
+ }
256
+
257
+ // Close button
258
+ var closeButton = document.createElement('button');
259
+ closeButton.innerHTML = closeIcon;
260
+ closeButton.style.background = 'none';
261
+ closeButton.style.border = 'none';
262
+ closeButton.style.color = '#fff';
263
+ closeButton.style.fontSize = '18px';
264
+ closeButton.style.cursor = 'pointer';
265
+ closeButton.style.padding = '0';
266
+ closeButton.style.margin = 'auto';
267
+ closeButton.style.width = '20px';
268
+ closeButton.style.height = '20px';
269
+ closeButton.style.display = 'flex';
270
+ closeButton.style.alignItems = 'center';
271
+ closeButton.style.justifyContent = 'center';
272
+ closeButton.style.borderRadius = '4px';
273
+ closeButton.style.transition = 'background-color 0.2s';
274
+ closeButton.style.opacity = '0.6';
275
+
276
+ closeButton.addEventListener('mouseenter', function() {
277
+ closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
278
+ closeButton.style.opacity = '1';
279
+ });
280
+ closeButton.addEventListener('mouseleave', function() {
281
+ closeButton.style.backgroundColor = 'transparent';
282
+ closeButton.style.opacity = '0.6';
283
+ });
284
+ closeButton.addEventListener('click', function(e) {
285
+ e.stopPropagation();
286
+ if (opts.onClose) {
287
+ opts.onClose();
288
+ } else if (opts.modal) {
289
+ opts.modal.remove();
290
+ }
291
+ });
292
+
293
+ header.appendChild(title);
294
+ header.appendChild(closeButton);
295
+
296
+ return { header: header, title: title, closeButton: closeButton };
297
+ }
298
+
299
+ function makeDraggable(modal, dragHandle) {
300
+ var isDragging = false;
301
+ var dragOffsetX = 0;
302
+ var dragOffsetY = 0;
303
+
304
+ dragHandle.addEventListener('mousedown', function(e) {
305
+ isDragging = true;
306
+ var rect = modal.getBoundingClientRect();
307
+ dragOffsetX = e.clientX - rect.left;
308
+ dragOffsetY = e.clientY - rect.top;
309
+
310
+ // Set position to current absolute position before removing transform
311
+ modal.style.left = rect.left + 'px';
312
+ modal.style.top = rect.top + 'px';
313
+ modal.style.transform = 'none';
314
+ e.preventDefault();
315
+ });
316
+
317
+ var onMouseMove = function(e) {
318
+ if (isDragging) {
319
+ var newLeft = e.clientX - dragOffsetX;
320
+ var newTop = e.clientY - dragOffsetY;
321
+ modal.style.left = newLeft + 'px';
322
+ modal.style.top = newTop + 'px';
323
+ }
324
+ };
325
+
326
+ var onMouseUp = function() {
327
+ isDragging = false;
328
+ };
329
+
330
+ document.addEventListener('mousemove', onMouseMove);
331
+ document.addEventListener('mouseup', onMouseUp);
332
+
333
+ // Return cleanup function
334
+ return function cleanup() {
335
+ document.removeEventListener('mousemove', onMouseMove);
336
+ document.removeEventListener('mouseup', onMouseUp);
337
+ };
338
+ }
339
+
340
+ // Function to get border color based on latency simulation value
341
+ function getBorderColor(latencyValue, opacity) {
342
+ var maxLatency = preferences.maxLatency;
343
+ var percentage = latencyValue / maxLatency;
344
+ var r, g, b = 0;
345
+
346
+ if (percentage <= 0.5) {
347
+ // Green to Yellow: (0, 200, 0) -> (200, 200, 0)
348
+ var segmentPercent = percentage * 2; // 0 to 1 for this segment
349
+ r = Math.round(segmentPercent * 200);
350
+ g = 200;
351
+ } else {
352
+ // Yellow to Red: (200, 200, 0) -> (200, 0, 0)
353
+ var segmentPercent = (percentage - 0.5) * 2; // 0 to 1 for this segment
354
+ r = 200;
355
+ g = Math.round((1 - segmentPercent) * 200);
356
+ }
357
+
358
+ return 'rgba(' + r + ', ' + g + ', ' + b + ', ' + opacity + ')';
359
+ }
360
+
361
+ function initialize() {
362
+ if (panelsHidden) return;
363
+
364
+ var container = document.createElement('div');
365
+ container.id = 'debug-logo-container';
366
+ container.style.position = 'fixed';
367
+ container.style.zIndex = '1000';
368
+ container.style.width = '21px';
369
+ container.style.height = '21px';
370
+ container.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
371
+ container.style.border = '3px solid ' + getBorderColor(preferences.latencySimulation.delay, 0.7);
372
+ container.style.borderRadius = '50%';
373
+ container.style.padding = '10px';
374
+ container.style.boxSizing = 'content-box';
375
+ container.style.display = 'flex';
376
+ container.style.justifyContent = 'center';
377
+ container.style.alignItems = 'center';
378
+ container.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.3)';
379
+ container.style.transition = 'border-color 0.3s ease, background-color 0.3s ease';
380
+ container.style.cursor = 'pointer';
381
+
382
+ // Apply initial position
383
+ applyPanelPosition();
384
+
385
+ // container on hover effect
386
+ container.addEventListener('mouseenter', function() {
387
+ container.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
388
+ container.style.borderColor = getBorderColor(preferences.latencySimulation.delay, 0.9);
389
+ });
390
+ container.addEventListener('mouseleave', function() {
391
+ container.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
392
+ container.style.borderColor = getBorderColor(preferences.latencySimulation.delay, 0.7);
393
+ });
394
+
395
+ var icon = document.createElement('div');
396
+ icon.style.width = '100%';
397
+ icon.style.height = '100%';
398
+ icon.style.display = 'flex';
399
+ icon.style.justifyContent = 'center';
400
+ icon.style.alignItems = 'center';
401
+
402
+ // Use insertAdjacentHTML for better Safari compatibility with SVG
403
+ icon.insertAdjacentHTML('beforeend', logoIcon);
404
+
405
+ container.appendChild(icon);
406
+ document.body.appendChild(container);
407
+
408
+ // Create menu first
409
+ createMenu(container);
410
+
411
+ // Apply initial position after menu is created
412
+ applyPanelPosition();
413
+ }
414
+
415
+ // Create menu that opens on logo click
416
+ function createMenu(logoContainer) {
417
+ var menu = document.createElement('div');
418
+ menu.id = 'debug-menu';
419
+ menu.style.position = 'fixed';
420
+ // Position will be set by applyPanelPosition
421
+ menu.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';
422
+ menu.style.color = '#fff';
423
+ menu.style.padding = '0 0 8px 0';
424
+ menu.style.borderRadius = '6px';
425
+ menu.style.fontFamily = 'system-ui, -apple-system, sans-serif';
426
+ menu.style.fontSize = '12px';
427
+ menu.style.zIndex = '1001';
428
+ menu.style.minWidth = '200px';
429
+ menu.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.5)';
430
+ menu.style.display = 'none';
431
+ menu.style.overflow = 'hidden';
432
+
433
+ // Host name display
434
+ var hostContainer = document.createElement('div');
435
+ hostContainer.style.padding = '6px 12px';
436
+ hostContainer.style.cursor = 'default';
437
+ hostContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
438
+ hostContainer.style.borderBottom = '1px solid rgba(255, 255, 255, 0.15)';
439
+ hostContainer.style.marginBottom = '4px';
440
+ hostContainer.style.borderTopLeftRadius = '6px';
441
+ hostContainer.style.borderTopRightRadius = '6px';
442
+
443
+ var hostValue = document.createElement('div');
444
+ hostValue.id = 'debug-menu-host';
445
+ hostValue.style.fontSize = '11px';
446
+ hostValue.style.color = '#fff';
447
+ hostValue.style.fontFamily = 'monospace';
448
+ hostValue.style.whiteSpace = 'nowrap';
449
+ hostValue.style.overflow = 'hidden';
450
+ hostValue.style.textOverflow = 'ellipsis';
451
+ hostValue.style.textAlign = 'center';
452
+ hostValue.style.fontWeight = '500';
453
+
454
+ // Function to update host display
455
+ function updateHostDisplay() {
456
+ var hostText = 'N/A';
457
+ if (roomDebugInfo.size > 0) {
458
+ // Get host from first room
459
+ var firstRoom = roomDebugInfo.values().next().value;
460
+ if (firstRoom && firstRoom.host) {
461
+ hostText = firstRoom.host;
462
+ }
463
+ }
464
+ hostValue.textContent = hostText;
465
+ }
466
+
467
+ // Update host display initially
468
+ updateHostDisplay();
469
+
470
+ hostContainer.appendChild(hostValue);
471
+ menu.appendChild(hostContainer);
472
+
473
+ // Simulate latency option
474
+ var latencyContainer = document.createElement('div');
475
+ latencyContainer.style.padding = '8px 12px';
476
+ latencyContainer.style.cursor = 'default';
477
+
478
+ var latencyLabel = document.createElement('div');
479
+ latencyLabel.style.marginBottom = '8px';
480
+ latencyLabel.style.display = 'flex';
481
+ latencyLabel.style.alignItems = 'center';
482
+ latencyLabel.style.justifyContent = 'space-between';
483
+ var latencyValueSpan = document.createElement('span');
484
+ latencyValueSpan.id = 'latency-value';
485
+ latencyValueSpan.style.color = '#888';
486
+ latencyValueSpan.style.fontSize = '11px';
487
+ latencyValueSpan.textContent = preferences.latencySimulation.delay + 'ms';
488
+
489
+ var latencyTextSpan = document.createElement('span');
490
+ latencyTextSpan.textContent = 'Simulate latency';
491
+
492
+ latencyLabel.appendChild(latencyTextSpan);
493
+ latencyLabel.appendChild(latencyValueSpan);
494
+
495
+ var latencySlider = document.createElement('input');
496
+ latencySlider.type = 'range';
497
+ latencySlider.min = '0';
498
+ latencySlider.max = preferences.maxLatency.toString();
499
+ latencySlider.value = preferences.latencySimulation.delay.toString();
500
+ latencySlider.style.border = 'none';
501
+ latencySlider.style.width = '100%';
502
+ latencySlider.style.height = '20px';
503
+ latencySlider.style.padding = '0';
504
+ latencySlider.style.margin = '0';
505
+ latencySlider.style.outline = 'none';
506
+ latencySlider.style.cursor = 'pointer';
507
+ latencySlider.style.webkitAppearance = 'none';
508
+ latencySlider.style.appearance = 'none';
509
+ latencySlider.style.background = 'transparent';
510
+ latencySlider.id = 'latency-slider';
511
+
512
+ // Function to calculate color from green (0) -> yellow (250) -> red (500)
513
+ function getSliderColor(value, min, max) {
514
+ var percentage = (value - min) / (max - min);
515
+ var r, g, b = 0;
516
+
517
+ if (percentage <= 0.5) {
518
+ // Green to Yellow: (0, 200, 0) -> (200, 200, 0)
519
+ var segmentPercent = percentage * 2; // 0 to 1 for this segment
520
+ r = Math.round(segmentPercent * 200);
521
+ g = 200;
522
+ } else {
523
+ // Yellow to Red: (200, 200, 0) -> (200, 0, 0)
524
+ var segmentPercent = (percentage - 0.5) * 2; // 0 to 1 for this segment
525
+ r = 200;
526
+ g = Math.round((1 - segmentPercent) * 200);
527
+ }
528
+
529
+ return 'rgb(' + r + ', ' + g + ', ' + b + ')';
530
+ }
531
+
532
+ // Function to update slider track color
533
+ function updateSliderColor(value) {
534
+ var color = getSliderColor(value, 0, preferences.maxLatency);
535
+ var valuePercent = (value / preferences.maxLatency) * 100;
536
+ var yellowPercent = 50; // Yellow at 250ms (50% of 500ms)
537
+ var gradient;
538
+
539
+ if (value <= preferences.maxLatency / 2) {
540
+ // Value is in green->yellow range: show green -> yellow
541
+ var yellowColor = getSliderColor(preferences.maxLatency / 2, 0, preferences.maxLatency);
542
+ gradient = `linear-gradient(to right, #00c800 0%, ${yellowColor} ${valuePercent}%, #333 ${valuePercent}%, #333 100%)`;
543
+ } else {
544
+ // Value is in yellow->red range: show green -> yellow -> current color
545
+ var yellowColor = getSliderColor(preferences.maxLatency / 2, 0, preferences.maxLatency);
546
+ gradient = `linear-gradient(to right, #00c800 0%, ${yellowColor} ${yellowPercent}%, ${color} ${valuePercent}%, #333 ${valuePercent}%, #333 100%)`;
547
+ }
548
+
549
+ var styleId = 'latency-slider-style';
550
+ var existingStyle = document.getElementById(styleId);
551
+ if (existingStyle) {
552
+ existingStyle.remove();
553
+ }
554
+ var style = document.createElement('style');
555
+ style.id = styleId;
556
+ style.textContent = `
557
+ #latency-slider::-webkit-slider-runnable-track {
558
+ background: ${gradient};
559
+ height: 6px;
560
+ border-radius: 3px;
561
+ border: none;
562
+ }
563
+ #latency-slider::-moz-range-track {
564
+ background: ${gradient};
565
+ height: 6px;
566
+ border-radius: 3px;
567
+ border: none;
568
+ }
569
+ `;
570
+ document.head.appendChild(style);
571
+ }
572
+
573
+ // Initialize slider color
574
+ updateSliderColor(parseInt(latencySlider.value));
575
+
576
+ // Add custom styling via CSS (inline style limitations)
577
+ var style = document.createElement('style');
578
+ style.textContent = `
579
+ #latency-slider {
580
+ background: transparent !important;
581
+ background-color: transparent !important;
582
+ }
583
+ #latency-slider::-webkit-slider-thumb {
584
+ -webkit-appearance: none;
585
+ appearance: none;
586
+ width: 16px;
587
+ height: 16px;
588
+ border-radius: 50%;
589
+ background: #fff;
590
+ background-color: #fff;
591
+ cursor: pointer;
592
+ border: 2px solid #888;
593
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
594
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
595
+ margin-top: -5px;
596
+ }
597
+ #latency-slider::-webkit-slider-thumb:hover {
598
+ transform: scale(1.1);
599
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
600
+ }
601
+ #latency-slider::-moz-range-thumb {
602
+ width: 16px;
603
+ height: 16px;
604
+ border-radius: 50%;
605
+ background: #fff;
606
+ cursor: pointer;
607
+ border: 2px solid #888;
608
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
609
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
610
+ }
611
+ #latency-slider::-moz-range-thumb:hover {
612
+ transform: scale(1.1);
613
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
614
+ }
615
+ `;
616
+ document.head.appendChild(style);
617
+
618
+ // Function to update container border color
619
+ function updateContainerBackgroundColor() {
620
+ var container = document.getElementById('debug-logo-container');
621
+ if (container) {
622
+ // Update to normal state (hover handlers will update on hover)
623
+ container.style.borderColor = getBorderColor(preferences.latencySimulation.delay, 0.7);
624
+ }
625
+ }
626
+
627
+ // Update latency value display
628
+ latencySlider.addEventListener('input', function() {
629
+ var value = parseInt(latencySlider.value);
630
+ latencyValueSpan.textContent = value + 'ms';
631
+ preferences.latencySimulation.delay = value;
632
+ preferences.latencySimulation.enabled = value > 0;
633
+ updateSliderColor(value);
634
+ updateContainerBackgroundColor();
635
+ savePreferences();
636
+ });
637
+
638
+ latencyContainer.appendChild(latencyLabel);
639
+ latencyContainer.appendChild(latencySlider);
640
+ menu.appendChild(latencyContainer);
641
+
642
+ // Separator
643
+ var separator = document.createElement('div');
644
+ separator.style.height = '1px';
645
+ separator.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
646
+ separator.style.margin = '4px 0';
647
+ menu.appendChild(separator);
648
+
649
+ // Settings option
650
+ var settingsOption = document.createElement('div');
651
+ settingsOption.style.padding = '8px 12px';
652
+ settingsOption.style.cursor = 'pointer';
653
+ settingsOption.style.transition = 'background-color 0.2s';
654
+ settingsOption.style.display = 'flex';
655
+ settingsOption.style.alignItems = 'center';
656
+ settingsOption.style.gap = '8px';
657
+
658
+ var settingsIconWrapper = document.createElement('span');
659
+ settingsIconWrapper.style.display = 'inline-flex';
660
+ settingsIconWrapper.style.alignItems = 'center';
661
+ settingsIconWrapper.style.width = '16px';
662
+ settingsIconWrapper.style.height = '16px';
663
+ settingsIconWrapper.innerHTML = settingsIcon.replace('height="200px" width="200px"', 'height="16" width="16"');
664
+
665
+ var settingsText = document.createElement('span');
666
+ settingsText.textContent = 'Preferences';
667
+
668
+ settingsOption.appendChild(settingsIconWrapper);
669
+ settingsOption.appendChild(settingsText);
670
+
671
+ settingsOption.addEventListener('mouseenter', function() {
672
+ settingsOption.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
673
+ });
674
+ settingsOption.addEventListener('mouseleave', function() {
675
+ settingsOption.style.backgroundColor = 'transparent';
676
+ });
677
+ settingsOption.addEventListener('click', function(e) {
678
+ e.stopPropagation();
679
+ menuVisible = false;
680
+ menu.style.display = 'none';
681
+ if (hostUpdateInterval) {
682
+ clearInterval(hostUpdateInterval);
683
+ hostUpdateInterval = null;
684
+ }
685
+ openSettingsModal();
686
+ });
687
+ menu.appendChild(settingsOption);
688
+
689
+ document.body.appendChild(menu);
690
+
691
+ // Toggle menu on logo click
692
+ var menuVisible = false;
693
+ var hostUpdateInterval = null;
694
+ logoContainer.addEventListener('click', function(e) {
695
+ e.stopPropagation();
696
+ menuVisible = !menuVisible;
697
+ menu.style.display = menuVisible ? 'block' : 'none';
698
+
699
+ if (menuVisible) {
700
+ updateHostDisplay();
701
+ // Update host every second while menu is visible
702
+ hostUpdateInterval = setInterval(updateHostDisplay, 1000);
703
+ } else {
704
+ if (hostUpdateInterval) {
705
+ clearInterval(hostUpdateInterval);
706
+ hostUpdateInterval = null;
707
+ }
708
+ }
709
+ });
710
+
711
+ // Close menu when clicking outside
712
+ document.addEventListener('click', function(e) {
713
+ if (menuVisible && !menu.contains(e.target as Node) && !logoContainer.contains(e.target as Node)) {
714
+ menuVisible = false;
715
+ menu.style.display = 'none';
716
+ if (hostUpdateInterval) {
717
+ clearInterval(hostUpdateInterval);
718
+ hostUpdateInterval = null;
719
+ }
720
+ }
721
+ });
722
+ }
723
+
724
+ // Create and open Settings modal
725
+ function openSettingsModal() {
726
+ // Remove existing modal if present
727
+ var existingModal = document.getElementById('debug-settings-modal');
728
+ if (existingModal) {
729
+ existingModal.remove();
730
+ }
731
+
732
+ // Create overlay using shared utility
733
+ var overlay = createModalOverlay();
734
+ overlay.id = 'debug-settings-overlay';
735
+
736
+ // Create modal (non-fixed positioning for overlay)
737
+ var modal = document.createElement('div');
738
+ modal.id = 'debug-settings-modal';
739
+ modal.style.position = 'relative'; // relative position for centered overlay content
740
+ modal.style.backgroundColor = 'rgba(30, 30, 30, 0.98)';
741
+ modal.style.borderRadius = '8px';
742
+ modal.style.width = '90%';
743
+ modal.style.maxWidth = '500px';
744
+ modal.style.maxHeight = '90vh';
745
+ modal.style.overflowY = 'auto';
746
+ modal.style.boxShadow = '0 8px 32px rgba(0, 0, 0, 0.5)';
747
+ modal.style.color = '#fff';
748
+ modal.style.fontFamily = 'system-ui, -apple-system, sans-serif';
749
+
750
+ // Modal header
751
+ var header = document.createElement('div');
752
+ header.style.display = 'flex';
753
+ header.style.justifyContent = 'space-between';
754
+ header.style.alignItems = 'center';
755
+ header.style.padding = '20px 24px';
756
+ header.style.borderBottom = '1px solid rgba(255, 255, 255, 0.1)';
757
+
758
+ var title = document.createElement('h2');
759
+ title.textContent = 'Preferences';
760
+ title.style.margin = '0';
761
+ title.style.fontSize = '18px';
762
+ title.style.fontWeight = '600';
763
+
764
+ var closeButton = document.createElement('button');
765
+ closeButton.innerHTML = '×';
766
+ closeButton.style.background = 'none';
767
+ closeButton.style.border = 'none';
768
+ closeButton.style.color = '#fff';
769
+ closeButton.style.fontSize = '24px';
770
+ closeButton.style.cursor = 'pointer';
771
+ closeButton.style.padding = '0';
772
+ closeButton.style.width = '32px';
773
+ closeButton.style.height = '32px';
774
+ closeButton.style.display = 'flex';
775
+ closeButton.style.alignItems = 'center';
776
+ closeButton.style.justifyContent = 'center';
777
+ closeButton.style.borderRadius = '4px';
778
+ closeButton.style.transition = 'background-color 0.2s';
779
+ closeButton.addEventListener('mouseenter', function() {
780
+ closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
781
+ });
782
+ closeButton.addEventListener('mouseleave', function() {
783
+ closeButton.style.backgroundColor = 'transparent';
784
+ });
785
+ closeButton.addEventListener('click', function() {
786
+ overlay.remove();
787
+ });
788
+
789
+ header.appendChild(title);
790
+ header.appendChild(closeButton);
791
+ modal.appendChild(header);
792
+
793
+ // Position option
794
+ var positionContainer = document.createElement('div');
795
+ positionContainer.style.padding = '20px 24px';
796
+ positionContainer.style.borderBottom = '1px solid rgba(255, 255, 255, 0.1)';
797
+ positionContainer.style.display = 'flex';
798
+ positionContainer.style.justifyContent = 'space-between';
799
+ positionContainer.style.alignItems = 'center';
800
+ positionContainer.style.gap = '16px';
801
+
802
+ var positionTextContainer = document.createElement('div');
803
+ positionTextContainer.style.flex = '1';
804
+
805
+ var positionTitle = document.createElement('div');
806
+ positionTitle.style.fontSize = '14px';
807
+ positionTitle.style.fontWeight = '600';
808
+ positionTitle.style.marginBottom = '4px';
809
+ positionTitle.textContent = 'Position';
810
+
811
+ var positionDescription = document.createElement('div');
812
+ positionDescription.style.fontSize = '12px';
813
+ positionDescription.style.color = 'rgba(255, 255, 255, 0.7)';
814
+ positionDescription.textContent = 'Adjust the placement of the panels.';
815
+
816
+ positionTextContainer.appendChild(positionTitle);
817
+ positionTextContainer.appendChild(positionDescription);
818
+
819
+ var positionSelect = document.createElement('select');
820
+ positionSelect.style.minWidth = '150px';
821
+ positionSelect.style.padding = '8px 12px';
822
+ positionSelect.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
823
+ positionSelect.style.border = '1px solid rgba(255, 255, 255, 0.2)';
824
+ positionSelect.style.borderRadius = '4px';
825
+ positionSelect.style.color = '#fff';
826
+ positionSelect.style.fontSize = '14px';
827
+ positionSelect.style.cursor = 'pointer';
828
+ positionSelect.style.outline = 'none';
829
+
830
+ var positions = [
831
+ { value: 'bottom-left', label: 'Bottom Left' },
832
+ { value: 'bottom-right', label: 'Bottom Right' },
833
+ { value: 'top-left', label: 'Top Left' },
834
+ { value: 'top-right', label: 'Top Right' }
835
+ ];
836
+
837
+ positions.forEach(function(pos) {
838
+ var option = document.createElement('option');
839
+ option.value = pos.value;
840
+ option.textContent = pos.label;
841
+ if (preferences.panelPosition.position === pos.value) {
842
+ option.selected = true;
843
+ }
844
+ positionSelect.appendChild(option);
845
+ });
846
+
847
+ positionSelect.addEventListener('change', function() {
848
+ preferences.panelPosition.position = positionSelect.value;
849
+ applyPanelPosition();
850
+ savePreferences();
851
+ });
852
+
853
+ positionContainer.appendChild(positionTextContainer);
854
+ positionContainer.appendChild(positionSelect);
855
+ modal.appendChild(positionContainer);
856
+
857
+ // Disable instruction
858
+ var disableContainer = document.createElement('div');
859
+ disableContainer.style.padding = '20px 24px';
860
+ disableContainer.style.borderBottom = '1px solid rgba(255, 255, 255, 0.1)';
861
+
862
+ var disableTitle = document.createElement('div');
863
+ disableTitle.style.fontSize = '14px';
864
+ disableTitle.style.fontWeight = '600';
865
+ disableTitle.style.marginBottom = '4px';
866
+ disableTitle.textContent = 'Disable Dev Tools';
867
+
868
+ var disableDescription = document.createElement('div');
869
+ disableDescription.style.fontSize = '12px';
870
+ disableDescription.style.color = 'rgba(255, 255, 255, 0.7)';
871
+ disableDescription.style.marginBottom = '8px';
872
+ disableDescription.innerHTML = 'To disable this UI completely, remove the <code style="background: rgba(255, 255, 255, 0.1); padding: 2px 6px; border-radius: 3px; font-family: monospace; font-size: 11px;">debug.js</code> script from your HTML file.';
873
+
874
+ disableContainer.appendChild(disableTitle);
875
+ disableContainer.appendChild(disableDescription);
876
+ modal.appendChild(disableContainer);
877
+
878
+ // Hide panels button
879
+ var hideContainer = document.createElement('div');
880
+ hideContainer.style.padding = '20px 24px';
881
+ hideContainer.style.display = 'flex';
882
+ hideContainer.style.justifyContent = 'space-between';
883
+ hideContainer.style.alignItems = 'center';
884
+ hideContainer.style.gap = '16px';
885
+
886
+ var hideTextContainer = document.createElement('div');
887
+ hideTextContainer.style.flex = '1';
888
+
889
+ var hideTitle = document.createElement('div');
890
+ hideTitle.style.fontSize = '14px';
891
+ hideTitle.style.fontWeight = '600';
892
+ hideTitle.style.marginBottom = '4px';
893
+ hideTitle.textContent = 'Hide Dev Tools for this session';
894
+
895
+ var hideDescription = document.createElement('div');
896
+ hideDescription.style.fontSize = '12px';
897
+ hideDescription.style.color = 'rgba(255, 255, 255, 0.7)';
898
+ hideDescription.textContent = 'Hide Dev Tools until you refresh the page.';
899
+
900
+ hideTextContainer.appendChild(hideTitle);
901
+ hideTextContainer.appendChild(hideDescription);
902
+
903
+ var hideButton = document.createElement('button');
904
+ hideButton.textContent = 'Hide';
905
+ hideButton.style.padding = '8px 16px';
906
+ hideButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
907
+ hideButton.style.border = '1px solid rgba(255, 255, 255, 0.2)';
908
+ hideButton.style.borderRadius = '4px';
909
+ hideButton.style.color = '#fff';
910
+ hideButton.style.fontSize = '14px';
911
+ hideButton.style.cursor = 'pointer';
912
+ hideButton.style.transition = 'background-color 0.2s';
913
+ hideButton.style.display = 'flex';
914
+ hideButton.style.alignItems = 'center';
915
+ hideButton.style.gap = '8px';
916
+ hideButton.style.flexShrink = '0';
917
+
918
+ hideButton.insertAdjacentHTML('afterbegin', eyeSlashIcon);
919
+
920
+ hideButton.addEventListener('mouseenter', function() {
921
+ hideButton.style.backgroundColor = 'rgba(255, 255, 255, 0.15)';
922
+ });
923
+ hideButton.addEventListener('mouseleave', function() {
924
+ hideButton.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
925
+ });
926
+ hideButton.addEventListener('click', function() {
927
+ hidePanelsForSession();
928
+ overlay.remove();
929
+ });
930
+
931
+ hideContainer.appendChild(hideTextContainer);
932
+ hideContainer.appendChild(hideButton);
933
+ modal.appendChild(hideContainer);
934
+
935
+ overlay.appendChild(modal);
936
+ document.body.appendChild(overlay);
937
+
938
+ // Mark as selected modal when opened
939
+ selectModal(overlay);
940
+
941
+ // Update close button to cleanup from modal stack
942
+ var originalOverlayRemove = overlay.remove.bind(overlay);
943
+ overlay.remove = function() {
944
+ removeModalFromStack(overlay);
945
+ originalOverlayRemove();
946
+ };
947
+
948
+ // Mark modal as selected when clicked
949
+ modal.addEventListener('mousedown', function(e) {
950
+ selectModal(overlay);
951
+ });
952
+
953
+ // Close on overlay click
954
+ overlay.addEventListener('click', function(e) {
955
+ if (e.target === overlay) {
956
+ overlay.remove();
957
+ }
958
+ });
959
+ }
960
+
961
+ // Create and open Send Messages modal
962
+ function openSendMessagesModal(uniquePanelId) {
963
+ var debugInfo = roomDebugInfo.get(uniquePanelId);
964
+ if (!debugInfo || !debugInfo.room) {
965
+ console.warn('Room not found for panel:', uniquePanelId);
966
+ return;
967
+ }
968
+
969
+ var room = debugInfo.room;
970
+ var messageTypes = debugInfo.messageTypes;
971
+
972
+ if (!messageTypes) {
973
+ console.warn('No message types available for this room');
974
+ return;
975
+ }
976
+
977
+ // Remove existing modal if present
978
+ var existingModal = document.getElementById('debug-send-messages-modal');
979
+ if (existingModal) {
980
+ existingModal.remove();
981
+ }
982
+
983
+ // Status dot reference
984
+ var statusDotRef: any = {};
985
+ var updateConnectionStatus: any = null;
986
+ var onLeaveCallbackRef: any = { current: null };
987
+
988
+ // Function to update status dot color
989
+ const updateSendMsgStatusDot = () => {
990
+ if (statusDotRef.element) {
991
+ statusDotRef.element.style.backgroundColor = room.connection?.isOpen ? '#22c55e' : '#ef4444';
992
+ }
993
+ };
994
+
995
+ // Initial callback
996
+ onLeaveCallbackRef.current = updateSendMsgStatusDot;
997
+ room.onLeave(updateSendMsgStatusDot);
998
+
999
+ // Create modal using shared utility
1000
+ const modal = createModal({
1001
+ id: 'debug-send-messages-modal',
1002
+ width: '400px',
1003
+ minWidth: '300px',
1004
+ maxWidth: '90vw',
1005
+ maxHeight: '90vh',
1006
+ room: room,
1007
+ trackOnLeave: true,
1008
+ onLeaveCallback: onLeaveCallbackRef
1009
+ });
1010
+
1011
+ // Create header using shared utility
1012
+ const headerComponents = createModalHeader({
1013
+ title: debugInfo.roomName + ' - Send Message',
1014
+ modal: modal,
1015
+ statusDot: true,
1016
+ statusColor: room.connection?.isOpen ? '#22c55e' : '#ef4444',
1017
+ statusDotRef: statusDotRef
1018
+ });
1019
+
1020
+ modal.appendChild(headerComponents.header);
1021
+
1022
+ // Make modal draggable
1023
+ makeDraggable(modal, headerComponents.header);
1024
+
1025
+ // Update status dot initially
1026
+ updateSendMsgStatusDot();
1027
+
1028
+ // Form content container (scrollable)
1029
+ var formContainer = document.createElement('div');
1030
+ formContainer.style.padding = '8px';
1031
+ formContainer.style.overflowY = 'auto';
1032
+ formContainer.style.backgroundColor = '#1e1e1e';
1033
+
1034
+ // Message Type Selector
1035
+ var typeLabel = document.createElement('label');
1036
+ typeLabel.textContent = 'Message Type';
1037
+ typeLabel.style.display = 'block';
1038
+ typeLabel.style.fontSize = '11px';
1039
+ typeLabel.style.fontWeight = '600';
1040
+ typeLabel.style.marginBottom = '4px';
1041
+ typeLabel.style.color = 'rgba(255, 255, 255, 0.9)';
1042
+
1043
+ var typeSelect = document.createElement('select');
1044
+ typeSelect.style.width = '100%';
1045
+ typeSelect.style.padding = '6px 8px';
1046
+ typeSelect.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
1047
+ typeSelect.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1048
+ typeSelect.style.borderRadius = '4px';
1049
+ typeSelect.style.color = '#fff';
1050
+ typeSelect.style.fontSize = '12px';
1051
+ typeSelect.style.cursor = 'pointer';
1052
+ typeSelect.style.outline = 'none';
1053
+ typeSelect.style.marginBottom = '12px';
1054
+
1055
+ // Add default option
1056
+ var defaultOption = document.createElement('option');
1057
+ defaultOption.value = '';
1058
+ defaultOption.textContent = 'Select a message type';
1059
+ defaultOption.disabled = true;
1060
+ defaultOption.selected = true;
1061
+ typeSelect.appendChild(defaultOption);
1062
+
1063
+ // Add message types
1064
+ Object.keys(messageTypes).forEach(function(msgType) {
1065
+ var option = document.createElement('option');
1066
+ option.value = msgType;
1067
+ option.textContent = msgType;
1068
+ typeSelect.appendChild(option);
1069
+ });
1070
+
1071
+ // Add wildcard option for custom message types
1072
+ var wildcardOption = document.createElement('option');
1073
+ wildcardOption.value = '*';
1074
+ wildcardOption.textContent = '* (Custom)';
1075
+ typeSelect.appendChild(wildcardOption);
1076
+
1077
+ formContainer.appendChild(typeLabel);
1078
+ formContainer.appendChild(typeSelect);
1079
+
1080
+ // Custom Message Type Input Container (shown when "*" is selected)
1081
+ var customTypeContainer = document.createElement('div');
1082
+ customTypeContainer.style.display = 'none';
1083
+ customTypeContainer.style.marginBottom = '12px';
1084
+
1085
+ var customTypeLabel = document.createElement('label');
1086
+ customTypeLabel.textContent = 'Message Type';
1087
+ customTypeLabel.style.display = 'block';
1088
+ customTypeLabel.style.fontSize = '11px';
1089
+ customTypeLabel.style.fontWeight = '600';
1090
+ customTypeLabel.style.marginBottom = '4px';
1091
+ customTypeLabel.style.color = 'rgba(255, 255, 255, 0.9)';
1092
+
1093
+ var customTypeInput = document.createElement('input');
1094
+ customTypeInput.type = 'text';
1095
+ customTypeInput.placeholder = 'Enter message type name';
1096
+ customTypeInput.style.width = '100%';
1097
+ customTypeInput.style.padding = '6px 8px';
1098
+ customTypeInput.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
1099
+ customTypeInput.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1100
+ customTypeInput.style.borderRadius = '4px';
1101
+ customTypeInput.style.color = '#fff';
1102
+ customTypeInput.style.fontSize = '11px';
1103
+ customTypeInput.style.fontFamily = 'monospace';
1104
+ customTypeInput.style.outline = 'none';
1105
+
1106
+ customTypeContainer.appendChild(customTypeLabel);
1107
+ customTypeContainer.appendChild(customTypeInput);
1108
+ formContainer.appendChild(customTypeContainer);
1109
+
1110
+ // Message Payload Input Container
1111
+ var payloadContainer = document.createElement('div');
1112
+ payloadContainer.style.display = 'none';
1113
+ payloadContainer.style.marginBottom = '12px';
1114
+
1115
+ var payloadLabel = document.createElement('label');
1116
+ payloadLabel.textContent = 'Payload';
1117
+ payloadLabel.style.display = 'block';
1118
+ payloadLabel.style.fontSize = '11px';
1119
+ payloadLabel.style.fontWeight = '600';
1120
+ payloadLabel.style.marginBottom = '4px';
1121
+ payloadLabel.style.color = 'rgba(255, 255, 255, 0.9)';
1122
+
1123
+ var payloadFieldsContainer = document.createElement('div');
1124
+ payloadFieldsContainer.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
1125
+ payloadFieldsContainer.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1126
+ payloadFieldsContainer.style.borderRadius = '4px';
1127
+ payloadFieldsContainer.style.padding = '8px';
1128
+ payloadFieldsContainer.style.fontFamily = 'monospace';
1129
+ payloadFieldsContainer.style.fontSize = '11px';
1130
+
1131
+ var payloadTextarea = document.createElement('textarea');
1132
+ payloadTextarea.style.width = '100%';
1133
+ payloadTextarea.style.minHeight = '80px';
1134
+ payloadTextarea.style.padding = '6px 8px';
1135
+ payloadTextarea.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
1136
+ payloadTextarea.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1137
+ payloadTextarea.style.borderRadius = '4px';
1138
+ payloadTextarea.style.color = '#fff';
1139
+ payloadTextarea.style.fontSize = '11px';
1140
+ payloadTextarea.style.fontFamily = 'monospace';
1141
+ payloadTextarea.style.outline = 'none';
1142
+ payloadTextarea.style.resize = 'vertical';
1143
+ payloadTextarea.placeholder = '{}';
1144
+ payloadTextarea.value = '{}';
1145
+
1146
+ payloadContainer.appendChild(payloadLabel);
1147
+ payloadContainer.appendChild(payloadFieldsContainer);
1148
+ payloadContainer.appendChild(payloadTextarea);
1149
+ formContainer.appendChild(payloadContainer);
1150
+
1151
+ // Error message container
1152
+ var errorContainer = document.createElement('div');
1153
+ errorContainer.style.display = 'none';
1154
+ errorContainer.style.padding = '6px 8px';
1155
+ errorContainer.style.backgroundColor = 'rgba(220, 38, 38, 0.2)';
1156
+ errorContainer.style.border = '1px solid rgba(220, 38, 38, 0.4)';
1157
+ errorContainer.style.borderRadius = '4px';
1158
+ errorContainer.style.marginBottom = '8px';
1159
+ errorContainer.style.fontSize = '11px';
1160
+ errorContainer.style.color = '#fca5a5';
1161
+
1162
+ formContainer.appendChild(errorContainer);
1163
+
1164
+ // Variables to store current message type and its schema
1165
+ var currentFormInputs: any = {};
1166
+ var currentMessageType = '';
1167
+
1168
+ // Update payload fields based on selected message type
1169
+ typeSelect.addEventListener('change', function() {
1170
+ var selectedType = typeSelect.value;
1171
+ currentMessageType = selectedType;
1172
+
1173
+ if (selectedType) {
1174
+ // Show/hide custom type input based on selection
1175
+ if (selectedType === '*') {
1176
+ customTypeContainer.style.display = 'block';
1177
+ customTypeInput.focus();
1178
+ } else {
1179
+ customTypeContainer.style.display = 'none';
1180
+ }
1181
+
1182
+ payloadContainer.style.display = 'block';
1183
+ errorContainer.style.display = 'none';
1184
+ currentFormInputs = {};
1185
+
1186
+ // Clear previous fields
1187
+ payloadFieldsContainer.innerHTML = '';
1188
+
1189
+ var schema = messageTypes[selectedType];
1190
+
1191
+ // If schema exists and has properties, create form inputs
1192
+ if (schema && schema.properties && Object.keys(schema.properties).length > 0) {
1193
+ payloadTextarea.style.display = 'none';
1194
+ payloadFieldsContainer.style.display = 'block';
1195
+
1196
+ // Generate form fields based on schema
1197
+ Object.keys(schema.properties).forEach(function(fieldName) {
1198
+ var fieldSchema = schema.properties[fieldName];
1199
+ var fieldContainer = document.createElement('div');
1200
+ fieldContainer.style.marginBottom = '8px';
1201
+
1202
+ var fieldLabel = document.createElement('label');
1203
+ fieldLabel.textContent = fieldName;
1204
+ if (schema.required && schema.required.includes(fieldName)) {
1205
+ fieldLabel.textContent += ' *';
1206
+ }
1207
+ fieldLabel.style.display = 'block';
1208
+ fieldLabel.style.fontSize = '10px';
1209
+ fieldLabel.style.marginBottom = '3px';
1210
+ fieldLabel.style.color = 'rgba(255, 255, 255, 0.8)';
1211
+
1212
+ var fieldInput;
1213
+
1214
+ if (fieldSchema.type === 'boolean') {
1215
+ fieldInput = document.createElement('input');
1216
+ fieldInput.type = 'checkbox';
1217
+ fieldInput.style.width = '16px';
1218
+ fieldInput.style.height = '16px';
1219
+ fieldInput.style.cursor = 'pointer';
1220
+ } else if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
1221
+ fieldInput = document.createElement('input');
1222
+ fieldInput.type = 'number';
1223
+ if (fieldSchema.type === 'integer') {
1224
+ fieldInput.step = '1';
1225
+ }
1226
+ fieldInput.style.width = '100%';
1227
+ fieldInput.style.padding = '4px 6px';
1228
+ fieldInput.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
1229
+ fieldInput.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1230
+ fieldInput.style.borderRadius = '3px';
1231
+ fieldInput.style.color = '#fff';
1232
+ fieldInput.style.fontSize = '11px';
1233
+ fieldInput.style.outline = 'none';
1234
+ } else {
1235
+ fieldInput = document.createElement('input');
1236
+ fieldInput.type = 'text';
1237
+ fieldInput.style.width = '100%';
1238
+ fieldInput.style.padding = '4px 6px';
1239
+ fieldInput.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
1240
+ fieldInput.style.border = '1px solid rgba(255, 255, 255, 0.2)';
1241
+ fieldInput.style.borderRadius = '3px';
1242
+ fieldInput.style.color = '#fff';
1243
+ fieldInput.style.fontSize = '11px';
1244
+ fieldInput.style.outline = 'none';
1245
+ }
1246
+
1247
+ if (fieldSchema.description) {
1248
+ var fieldDesc = document.createElement('div');
1249
+ fieldDesc.textContent = fieldSchema.description;
1250
+ fieldDesc.style.fontSize = '9px';
1251
+ fieldDesc.style.color = 'rgba(255, 255, 255, 0.5)';
1252
+ fieldDesc.style.marginTop = '2px';
1253
+ fieldContainer.appendChild(fieldLabel);
1254
+ fieldContainer.appendChild(fieldInput);
1255
+ fieldContainer.appendChild(fieldDesc);
1256
+ } else {
1257
+ fieldContainer.appendChild(fieldLabel);
1258
+ fieldContainer.appendChild(fieldInput);
1259
+ }
1260
+
1261
+ payloadFieldsContainer.appendChild(fieldContainer);
1262
+ currentFormInputs[fieldName] = { input: fieldInput, schema: fieldSchema };
1263
+ });
1264
+ } else {
1265
+ // Use JSON textarea for free-form input (no schema or empty schema)
1266
+ payloadTextarea.style.display = 'block';
1267
+ payloadFieldsContainer.style.display = 'none';
1268
+ payloadTextarea.value = '{}';
1269
+
1270
+ // Update placeholder based on whether schema exists
1271
+ if (!schema) {
1272
+ payloadTextarea.placeholder = 'Enter JSON payload (no message format defined)\n\nExample:\n{\n "key": "value"\n}';
1273
+ } else {
1274
+ payloadTextarea.placeholder = '{}';
1275
+ }
1276
+ }
1277
+ } else {
1278
+ payloadContainer.style.display = 'none';
1279
+ }
1280
+ });
1281
+
1282
+ // Send Button
1283
+ var sendButton = document.createElement('button');
1284
+ sendButton.textContent = 'Send';
1285
+ sendButton.style.width = '100%';
1286
+ sendButton.style.padding = '8px 12px';
1287
+ sendButton.style.backgroundColor = '#8b5cf6';
1288
+ sendButton.style.border = 'none';
1289
+ sendButton.style.borderRadius = '4px';
1290
+ sendButton.style.color = '#fff';
1291
+ sendButton.style.fontSize = '12px';
1292
+ sendButton.style.fontWeight = '600';
1293
+ sendButton.style.cursor = 'pointer';
1294
+ sendButton.style.transition = 'background-color 0.2s';
1295
+
1296
+ var isButtonInSuccessState = false;
1297
+ var hoverColor = '#7c3aed';
1298
+ var normalColor = '#8b5cf6';
1299
+
1300
+ sendButton.addEventListener('mouseenter', function() {
1301
+ if (!isButtonInSuccessState && !sendButton.disabled) {
1302
+ sendButton.style.backgroundColor = hoverColor;
1303
+ }
1304
+ });
1305
+ sendButton.addEventListener('mouseleave', function() {
1306
+ if (!isButtonInSuccessState && !sendButton.disabled) {
1307
+ sendButton.style.backgroundColor = normalColor;
1308
+ }
1309
+ });
1310
+
1311
+ // Update the connection status function to also manage button state
1312
+ updateConnectionStatus = function() {
1313
+ const isConnected = room.connection?.isOpen;
1314
+ if (statusDotRef.element) {
1315
+ statusDotRef.element.style.backgroundColor = isConnected ? '#22c55e' : '#ef4444';
1316
+ }
1317
+
1318
+ // Update button disabled state
1319
+ sendButton.disabled = !isConnected;
1320
+ if (!isConnected) {
1321
+ sendButton.style.backgroundColor = '#6b7280';
1322
+ sendButton.style.cursor = 'not-allowed';
1323
+ sendButton.style.opacity = '0.5';
1324
+ } else if (!isButtonInSuccessState) {
1325
+ sendButton.style.backgroundColor = normalColor;
1326
+ sendButton.style.cursor = 'pointer';
1327
+ sendButton.style.opacity = '1';
1328
+ }
1329
+ };
1330
+
1331
+ // Swap out the onLeave callback to use the combined update function
1332
+ room.onLeave.remove(onLeaveCallbackRef.current);
1333
+ room.onLeave(updateConnectionStatus);
1334
+ onLeaveCallbackRef.current = updateConnectionStatus;
1335
+
1336
+ updateConnectionStatus();
1337
+
1338
+ sendButton.addEventListener('click', function() {
1339
+ errorContainer.style.display = 'none';
1340
+
1341
+ // Check if room is connected
1342
+ if (!room.connection?.isOpen) {
1343
+ errorContainer.textContent = 'Cannot send message: Room is not connected';
1344
+ errorContainer.style.display = 'block';
1345
+ return;
1346
+ }
1347
+
1348
+ if (!currentMessageType) {
1349
+ errorContainer.textContent = 'Please select a message type';
1350
+ errorContainer.style.display = 'block';
1351
+ return;
1352
+ }
1353
+
1354
+ // Determine actual message type to send
1355
+ var actualMessageType = currentMessageType;
1356
+ if (currentMessageType === '*') {
1357
+ actualMessageType = customTypeInput.value.trim();
1358
+ if (!actualMessageType) {
1359
+ errorContainer.textContent = 'Please enter a message type name';
1360
+ errorContainer.style.display = 'block';
1361
+ return;
1362
+ }
1363
+ }
1364
+
1365
+ try {
1366
+ var payload;
1367
+
1368
+ // Build payload from form inputs or textarea
1369
+ if (Object.keys(currentFormInputs).length > 0) {
1370
+ payload = {};
1371
+ var schema = messageTypes[currentMessageType];
1372
+
1373
+ for (var fieldName in currentFormInputs) {
1374
+ var fieldData = currentFormInputs[fieldName];
1375
+ var input = fieldData.input;
1376
+ var fieldSchema = fieldData.schema;
1377
+ var value;
1378
+
1379
+ if (fieldSchema.type === 'boolean') {
1380
+ value = input.checked;
1381
+ } else if (fieldSchema.type === 'number' || fieldSchema.type === 'integer') {
1382
+ value = input.value ? parseFloat(input.value) : undefined;
1383
+ } else {
1384
+ value = input.value || undefined;
1385
+ }
1386
+
1387
+ // Only include required fields or fields with values
1388
+ if (value !== undefined || (schema.required && schema.required.includes(fieldName))) {
1389
+ payload[fieldName] = value;
1390
+ }
1391
+ }
1392
+ } else {
1393
+ payload = JSON.parse(payloadTextarea.value);
1394
+ }
1395
+
1396
+ // Send the message
1397
+ room.send(actualMessageType, payload);
1398
+
1399
+ // Change button to success state
1400
+ isButtonInSuccessState = true;
1401
+ sendButton.textContent = 'Message sent!';
1402
+ sendButton.style.backgroundColor = '#22c55e';
1403
+ sendButton.style.cursor = 'default';
1404
+
1405
+ // Restore button after 1.5 seconds
1406
+ setTimeout(function() {
1407
+ isButtonInSuccessState = false;
1408
+ sendButton.textContent = 'Send';
1409
+ sendButton.style.backgroundColor = normalColor;
1410
+ sendButton.style.cursor = 'pointer';
1411
+ }, 800);
1412
+
1413
+ } catch (e) {
1414
+ errorContainer.textContent = 'Error: ' + e.message;
1415
+ errorContainer.style.display = 'block';
1416
+ }
1417
+ });
1418
+
1419
+ formContainer.appendChild(sendButton);
1420
+
1421
+ modal.appendChild(formContainer);
1422
+ document.body.appendChild(modal);
1423
+ }
1424
+
1425
+ // Create and open State Inspector modal
1426
+ function openStateInspectorModal(uniquePanelId) {
1427
+ var debugInfo = roomDebugInfo.get(uniquePanelId);
1428
+ if (!debugInfo || !debugInfo.room) {
1429
+ console.warn('Room not found for panel:', uniquePanelId);
1430
+ return;
1431
+ }
1432
+
1433
+ var room = debugInfo.room;
1434
+ var refIds = room.serializer.decoder.root.refIds;
1435
+
1436
+ // Remove existing modal if present
1437
+ var existingModal = document.getElementById('debug-state-inspector-modal');
1438
+ if (existingModal) {
1439
+ existingModal.remove();
1440
+ }
1441
+
1442
+ // Load saved position and size from localStorage
1443
+ var savedStateInspectorPrefs = null;
1444
+ try {
1445
+ var saved = localStorage.getItem('colyseus-state-inspector-preferences');
1446
+ if (saved) {
1447
+ savedStateInspectorPrefs = JSON.parse(saved);
1448
+ }
1449
+ } catch (e) {
1450
+ // Ignore localStorage errors
1451
+ }
1452
+
1453
+ // Default values
1454
+ var defaultWidth = 600;
1455
+ var defaultHeight = 500;
1456
+ var defaultLeft = '50%';
1457
+ var defaultTop = '50%';
1458
+ var defaultTransform = 'translate(-50%, -50%)';
1459
+
1460
+ // Use saved preferences if available
1461
+ if (savedStateInspectorPrefs) {
1462
+ if (savedStateInspectorPrefs.width && savedStateInspectorPrefs.width >= 300) {
1463
+ defaultWidth = savedStateInspectorPrefs.width;
1464
+ }
1465
+ if (savedStateInspectorPrefs.height && savedStateInspectorPrefs.height >= 200) {
1466
+ defaultHeight = savedStateInspectorPrefs.height;
1467
+ }
1468
+ if (savedStateInspectorPrefs.left !== undefined && savedStateInspectorPrefs.top !== undefined) {
1469
+ // Constrain position to window boundaries
1470
+ var maxLeft = window.innerWidth - defaultWidth;
1471
+ var maxTop = window.innerHeight - defaultHeight;
1472
+
1473
+ var constrainedLeft = Math.max(0, Math.min(savedStateInspectorPrefs.left, maxLeft));
1474
+ var constrainedTop = Math.max(0, Math.min(savedStateInspectorPrefs.top, maxTop));
1475
+
1476
+ defaultLeft = constrainedLeft + 'px';
1477
+ defaultTop = constrainedTop + 'px';
1478
+ defaultTransform = 'none';
1479
+ }
1480
+ }
1481
+
1482
+ // Function to save state inspector preferences
1483
+ function saveStateInspectorPreferences() {
1484
+ try {
1485
+ var rect = modal.getBoundingClientRect();
1486
+ var prefs = {
1487
+ width: rect.width,
1488
+ height: rect.height,
1489
+ left: rect.left,
1490
+ top: rect.top
1491
+ };
1492
+ localStorage.setItem('colyseus-state-inspector-preferences', JSON.stringify(prefs));
1493
+ } catch (e) {
1494
+ // Ignore localStorage errors
1495
+ }
1496
+ }
1497
+
1498
+ // Status dot reference
1499
+ var statusDotRef: any = {};
1500
+
1501
+ // Function to update status dot color
1502
+ const updateStateViewerStatusDot = () => {
1503
+ if (statusDotRef.element) {
1504
+ statusDotRef.element.style.backgroundColor = room.connection?.isOpen ? '#22c55e' : '#ef4444';
1505
+ }
1506
+ };
1507
+
1508
+ // Register the onLeave callback
1509
+ room.onLeave(updateStateViewerStatusDot);
1510
+
1511
+ // Create modal using shared utility with automatic onLeave tracking
1512
+ const modal = createModal({
1513
+ id: 'debug-state-inspector-modal',
1514
+ width: defaultWidth + 'px',
1515
+ height: defaultHeight + 'px',
1516
+ minWidth: '300px',
1517
+ minHeight: '200px',
1518
+ maxWidth: '90vw',
1519
+ maxHeight: '90vh',
1520
+ top: defaultTop,
1521
+ left: defaultLeft,
1522
+ transform: defaultTransform,
1523
+ room: room,
1524
+ trackOnLeave: true,
1525
+ onLeaveCallback: updateStateViewerStatusDot
1526
+ });
1527
+
1528
+ // Create header using shared utility
1529
+ const headerComponents = createModalHeader({
1530
+ title: `${debugInfo.roomName} - State Viewer`,
1531
+ modal: modal,
1532
+ statusDot: true,
1533
+ statusColor: room.connection?.isOpen ? '#22c55e' : '#ef4444',
1534
+ statusDotRef: statusDotRef
1535
+ });
1536
+ const header = headerComponents.header;
1537
+ const closeButton = headerComponents.closeButton;
1538
+
1539
+ modal.appendChild(header);
1540
+
1541
+ // Update status dot initially
1542
+ updateStateViewerStatusDot();
1543
+
1544
+ // State content container
1545
+ var contentContainer = document.createElement('div');
1546
+ contentContainer.style.padding = '8px';
1547
+ contentContainer.style.overflowY = 'auto';
1548
+ contentContainer.style.flex = '1';
1549
+ contentContainer.style.minHeight = '0';
1550
+ contentContainer.style.backgroundColor = '#1e1e1e';
1551
+ contentContainer.id = 'debug-state-content';
1552
+
1553
+ // Single event listener for all expand buttons (event delegation)
1554
+ contentContainer.addEventListener('click', function(e: MouseEvent) {
1555
+ var expandButton = (e.target as HTMLElement).closest('[data-expand-button]');
1556
+ if (expandButton) {
1557
+ e.stopPropagation();
1558
+ toggleExpand(expandButton);
1559
+ }
1560
+ });
1561
+
1562
+ // // Counter for unique IDs
1563
+ // var stateNodeCounter = 0;
1564
+
1565
+ // Track expanded paths across re-renders
1566
+ var expandedPaths = new Set();
1567
+
1568
+ // Helper function to escape HTML
1569
+ function escapeHtml(text) {
1570
+ var div = document.createElement('div');
1571
+ div.textContent = text;
1572
+ return div.innerHTML;
1573
+ }
1574
+
1575
+ // Helper function to check if a value is expandable (object, array, or collection)
1576
+ function isExpandable(value) {
1577
+ if (value === null || value === undefined) {
1578
+ return false;
1579
+ }
1580
+ var valueType = typeof value;
1581
+ var isObject = valueType === 'object' && !(value instanceof Array);
1582
+ var hasForEach = valueType === 'object' && typeof value.forEach === 'function';
1583
+ var isArray = value instanceof Array;
1584
+ return isObject || isArray || hasForEach;
1585
+ }
1586
+
1587
+ // Helper function to get label color based on field name
1588
+ function getLabelColor(fieldName) {
1589
+ return typeof fieldName === 'string' && fieldName.startsWith('"') ? '#CE9178' : '#DCDCAA';
1590
+ }
1591
+
1592
+ // Helper function to get display state for expandable nodes
1593
+ function getDisplayState(depth, isPathExpanded) {
1594
+ var isRoot = depth === 0;
1595
+ return {
1596
+ isRoot: isRoot,
1597
+ initialDisplay: isRoot ? 'block' : (isPathExpanded ? 'block' : 'none'),
1598
+ initialIcon: (isRoot || isPathExpanded) ? '▼' : '▶',
1599
+ rootClass: isRoot ? ' data-root-node="true"' : ''
1600
+ };
1601
+ }
1602
+
1603
+ // Helper function to get refId display string
1604
+ function getRefIdDisplay(refId) {
1605
+ return refId !== null && refId !== undefined ? '<span style="color: #909090; font-size: 11px; margin-left: 4px;">(ref: ' + refId + ')</span>' : '';
1606
+ }
1607
+
1608
+ // Helper function to render expand button HTML
1609
+ function renderExpandButton(displayState, displayLabel, refIdDisplay, size) {
1610
+ var html = '<div data-expand-button' + displayState.rootClass + ' style="display: flex; align-items: center; cursor: ' + (displayState.isRoot ? 'default' : 'pointer') + '; user-select: none; margin: 0; padding: 1px 0;" onmouseover="' + (displayState.isRoot ? '' : 'this.style.backgroundColor=\'rgba(255,255,255,0.05)\'') + '" onmouseout="' + (displayState.isRoot ? '' : 'this.style.backgroundColor=\'transparent\'') + '">';
1611
+ html += '<span data-expand-icon style="margin-right: 4px; color: #858585; font-size: 10px; width: 10px; display: inline-block; text-align: center; user-select: none;">' + displayState.initialIcon + '</span>';
1612
+ if (displayLabel) {
1613
+ html += '<span style="color: ' + getLabelColor(displayLabel) + ';">' + escapeHtml(String(displayLabel)) + '</span>';
1614
+ }
1615
+ if (size !== null && size !== undefined && size !== '?') {
1616
+ // ×
1617
+ html += ' <span style="color: #858585;">(' + size + ') </span>';
1618
+ }
1619
+ html += refIdDisplay;
1620
+ html += '</div>';
1621
+ return html;
1622
+ }
1623
+
1624
+ // Helper function to render a key-value pair
1625
+ function renderKeyValue(key, value, depth, currentPath, keyDisplay, isKeyString) {
1626
+ if (isExpandable(value)) {
1627
+ // For expandable values, renderState handles its own container with proper indentation
1628
+ // Pass the fieldName so it displays the key as the label
1629
+ if (keyDisplay !== null) {
1630
+ return renderState(value, depth, String(key), currentPath, keyDisplay);
1631
+ } else {
1632
+ return renderState(value, depth, String(key), currentPath);
1633
+ }
1634
+ } else {
1635
+ // For primitive values, wrap in a div with key-value formatting
1636
+ // Add padding-left to align with expandable items (icon width 10px + margin-right 4px = 14px)
1637
+ var indent = depth * 6;
1638
+ var html = '<div style="margin-left: ' + indent + 'px; padding-left: 14px;">';
1639
+ if (keyDisplay !== null) {
1640
+ var keyColor = isKeyString ? '#CE9178' : '#DCDCAA';
1641
+ html += '<span style="color: ' + keyColor + ';">' + (isKeyString ? '"' + escapeHtml(String(keyDisplay)) + '"' : escapeHtml(String(keyDisplay))) + '</span><span style="color: #858585;">: </span>';
1642
+ } else {
1643
+ html += '<span style="color: #DCDCAA;">' + escapeHtml(String(key)) + '</span><span style="color: #858585;">: </span>';
1644
+ }
1645
+ html += renderState(value, depth + 1, String(key), currentPath);
1646
+ html += '</div>';
1647
+ return html;
1648
+ }
1649
+ }
1650
+
1651
+ // Function to render state recursively
1652
+ function renderState(
1653
+ obj: any,
1654
+ depth: number = 0,
1655
+ parentKey: string = '',
1656
+ path: string = '',
1657
+ fieldName: string = null
1658
+ ) {
1659
+ var maxDepth = 10;
1660
+ if (depth > maxDepth) {
1661
+ return '<span style="color: #858585; font-style: italic;">[Max depth reached]</span>';
1662
+ }
1663
+
1664
+ if (obj === null) {
1665
+ return '<span style="color: #858585;">null</span>';
1666
+ }
1667
+
1668
+ if (obj === undefined) {
1669
+ return '<span style="color: #858585;">undefined</span>';
1670
+ }
1671
+
1672
+ var type = typeof obj;
1673
+ var indent = depth * 6;
1674
+ var refId = refIds.get(obj);
1675
+ var nodeId = 'state-node-' + refId;
1676
+ var currentPath = path ? path + '.' + (parentKey || '') : (parentKey || 'root');
1677
+ var isPathExpanded = expandedPaths.has(currentPath);
1678
+ var refIdDisplay = getRefIdDisplay(refId);
1679
+
1680
+ if (type === 'string') {
1681
+ return '<span style="color: #CE9178;">"' + escapeHtml(String(obj)) + '"</span>';
1682
+ }
1683
+
1684
+ if (type === 'number') {
1685
+ return '<span style="color: #B5CEA8;">' + obj + '</span>';
1686
+ }
1687
+
1688
+ if (type === 'boolean') {
1689
+ return '<span style="color: #569CD6;">' + obj + '</span>';
1690
+ }
1691
+
1692
+ // Check if object has forEach method
1693
+ var hasForEach = typeof obj.forEach === 'function';
1694
+ var collectionSize = (hasForEach) && (obj.size || obj.length) || null;
1695
+
1696
+ if (hasForEach) {
1697
+ var size = (collectionSize !== null ? collectionSize : '?');
1698
+ var contentId = nodeId + '-content';
1699
+ var displayState = getDisplayState(depth, isPathExpanded);
1700
+ var displayLabel = fieldName !== null ? fieldName : (displayState.isRoot ? 'State' : '');
1701
+
1702
+ var html = '<div style="margin-left: ' + indent + 'px;" data-path="' + escapeHtml(currentPath) + '">';
1703
+ html += renderExpandButton(displayState, displayLabel, refIdDisplay, size);
1704
+ html += '<div id="' + contentId + '" style="display: ' + displayState.initialDisplay + ';">';
1705
+
1706
+ // Handle forEach collections (Map, Set, etc.)
1707
+ var isSet = obj instanceof Set || (obj.constructor && obj.constructor.name === 'Set');
1708
+ try {
1709
+ obj.forEach(function (value, key) {
1710
+ if (isSet) {
1711
+ // For Sets, forEach passes (value, value, set), so we only use the first parameter
1712
+ html += renderState(value, depth + 1, String(key), currentPath);
1713
+ } else {
1714
+ // For Maps, format key and use renderKeyValue to handle expandable values and proper formatting
1715
+ var isKeyNumber = typeof key === 'number';
1716
+ var keyStr = isKeyNumber ? '[' + String(key) + ']' : String(key);
1717
+ html += renderKeyValue(key, value, depth + 1, currentPath, keyStr, typeof key === 'string');
1718
+ }
1719
+ });
1720
+ } catch (e) {
1721
+ var errorIndent = (depth + 1) * 6;
1722
+ html += '<div style="margin-left: ' + errorIndent + 'px; color: #e74856;">Error iterating: ' + escapeHtml(e.message) + '</div>';
1723
+ }
1724
+
1725
+ html += '</div>';
1726
+ html += '</div>';
1727
+ return html;
1728
+ }
1729
+
1730
+ if (type === 'object') {
1731
+ var keys = Object.keys(obj);
1732
+ var contentId = nodeId + '-content';
1733
+ var displayState = getDisplayState(depth, isPathExpanded);
1734
+ var displayLabel = fieldName !== null ? fieldName : (displayState.isRoot ? 'state' : '');
1735
+
1736
+ var html = '<div style="margin-left: ' + indent + 'px;" data-path="' + escapeHtml(currentPath) + '">';
1737
+
1738
+ if (keys.length === 0) {
1739
+ if (displayLabel) {
1740
+ html += '<span style="color: ' + getLabelColor(displayLabel) + ';">' + escapeHtml(String(displayLabel)) + '</span><span style="color: #858585;"> {}</span>' + refIdDisplay;
1741
+ } else {
1742
+ html += '<span style="color: #858585;">{}</span>' + refIdDisplay;
1743
+ }
1744
+ } else {
1745
+ html += renderExpandButton(displayState, displayLabel, refIdDisplay, null);
1746
+ html += '<div id="' + contentId + '" style="display: ' + displayState.initialDisplay + ';">';
1747
+
1748
+ for (var i = 0; i < keys.length; i++) {
1749
+ var key = keys[i];
1750
+ html += renderKeyValue(key, obj[key], depth + 1, currentPath, key, false);
1751
+ }
1752
+
1753
+ html += '</div>';
1754
+ }
1755
+
1756
+ html += '</div>';
1757
+ return html;
1758
+ }
1759
+
1760
+ return '<span style="color: #858585;">' + escapeHtml(String(obj)) + '</span>';
1761
+ }
1762
+
1763
+ // Function to toggle expand/collapse
1764
+ function toggleExpand(expandButton) {
1765
+ // Don't allow collapsing root node
1766
+ var content = expandButton.nextElementSibling;
1767
+ var icon = expandButton.querySelector('[data-expand-icon]');
1768
+ if (content && content.style) {
1769
+ var isHidden = content.style.display === 'none';
1770
+ content.style.display = isHidden ? 'block' : 'none';
1771
+ if (icon) {
1772
+ icon.textContent = isHidden ? '▼' : '▶';
1773
+ }
1774
+
1775
+ // Track expanded state by path
1776
+ var pathElement = expandButton.closest('[data-path]');
1777
+ if (pathElement) {
1778
+ var path = pathElement.getAttribute('data-path');
1779
+ if (isHidden) {
1780
+ expandedPaths.add(path);
1781
+ } else {
1782
+ expandedPaths.delete(path);
1783
+ }
1784
+ }
1785
+ }
1786
+ }
1787
+
1788
+ // Function to update state display
1789
+ function updateStateDisplay() {
1790
+ try {
1791
+ // Save currently expanded paths before clearing
1792
+ var currentExpandedPaths = new Set();
1793
+ var existingExpandButtons = contentContainer.querySelectorAll('[data-expand-button]');
1794
+ for (var i = 0; i < existingExpandButtons.length; i++) {
1795
+ var button = existingExpandButtons[i];
1796
+ var pathElement = button.closest('[data-path]');
1797
+ if (pathElement) {
1798
+ var path = pathElement.getAttribute('data-path');
1799
+ var content = button.nextElementSibling as HTMLElement;
1800
+ if (content && content.style && content.style.display !== 'none') {
1801
+ currentExpandedPaths.add(path);
1802
+ }
1803
+ }
1804
+ }
1805
+
1806
+ // Merge with previously tracked expanded paths
1807
+ // Keep paths that were expanded before, or are currently expanded
1808
+ var pathsToKeep = new Set();
1809
+ expandedPaths.forEach(function(path) {
1810
+ pathsToKeep.add(path);
1811
+ });
1812
+ currentExpandedPaths.forEach(function(path) {
1813
+ pathsToKeep.add(path);
1814
+ });
1815
+ expandedPaths = pathsToKeep;
1816
+
1817
+ // stateNodeCounter = 0; // Reset counter for new render
1818
+ var state = room.state || {};
1819
+ contentContainer.innerHTML = '<div style="font-family: \'Consolas\', \'Monaco\', \'Courier New\', monospace; font-size: 12px; line-height: 1.5; color: #d4d4d4; padding: 8px;">' + renderState(state) + '</div>';
1820
+
1821
+ // Event delegation: single click listener handles all expand buttons
1822
+ } catch (e) {
1823
+ contentContainer.innerHTML = '<div style="color: #e74856; padding: 20px;">Error accessing room state: ' + escapeHtml(e.message) + '</div>';
1824
+ }
1825
+ }
1826
+
1827
+ // Throttle function to prevent excessive re-renders
1828
+ // TODO: prevent re-renders of unchanged refIds instead of just throttling
1829
+ function throttle(func, wait) {
1830
+ var timeout;
1831
+ var previous = 0;
1832
+ return function executedFunction() {
1833
+ var context = this;
1834
+ var args = arguments;
1835
+ var now = Date.now();
1836
+ var remaining = wait - (now - previous);
1837
+
1838
+ if (remaining <= 0 || remaining > wait) {
1839
+ if (timeout) {
1840
+ clearTimeout(timeout);
1841
+ timeout = null;
1842
+ }
1843
+ previous = now;
1844
+ func.apply(context, args);
1845
+ } else if (!timeout) {
1846
+ timeout = setTimeout(function() {
1847
+ previous = Date.now();
1848
+ timeout = null;
1849
+ func.apply(context, args);
1850
+ }, remaining);
1851
+ }
1852
+ };
1853
+ }
1854
+
1855
+ // Create throttled version of updateStateDisplay
1856
+ var throttledUpdateStateDisplay = throttle(updateStateDisplay, 200);
1857
+
1858
+ // Initial render - root is always expanded
1859
+ expandedPaths.add('root');
1860
+ updateStateDisplay();
1861
+
1862
+ // Update state when it changes
1863
+ const originalTriggerChanges = room.serializer.decoder.triggerChanges;
1864
+ room.serializer.decoder.triggerChanges = function(changes) {
1865
+ originalTriggerChanges?.apply(this, arguments);
1866
+ throttledUpdateStateDisplay();
1867
+ /**
1868
+ * TODO: keep track of which refIds have changed and only re-render those
1869
+ */
1870
+ }
1871
+
1872
+ modal.appendChild(contentContainer);
1873
+ document.body.appendChild(modal);
1874
+
1875
+ // Drag and resize state variables
1876
+ var isDragging = false;
1877
+ var dragStartX = 0;
1878
+ var dragStartY = 0;
1879
+ var modalStartX = 0;
1880
+ var modalStartY = 0;
1881
+ var isResizing = false;
1882
+ var resizeHandle = null;
1883
+ var resizeStartX = 0;
1884
+ var resizeStartY = 0;
1885
+ var resizeStartWidth = 0;
1886
+ var resizeStartHeight = 0;
1887
+ var resizeStartLeft = 0;
1888
+ var resizeStartTop = 0;
1889
+
1890
+ header.addEventListener('mousedown', function(e) {
1891
+ const target = e.target as HTMLElement;
1892
+ // Don't drag if clicking on a resize handle
1893
+ if (target.classList && target.classList.contains('resize-handle')) {
1894
+ return;
1895
+ }
1896
+ if (target === closeButton || closeButton.contains(target)) {
1897
+ return; // Don't drag when clicking close button
1898
+ }
1899
+ isDragging = true;
1900
+ dragStartX = e.clientX;
1901
+ dragStartY = e.clientY;
1902
+ var rect = modal.getBoundingClientRect();
1903
+ modalStartX = rect.left;
1904
+ modalStartY = rect.top;
1905
+ modal.style.cursor = 'move';
1906
+ e.preventDefault();
1907
+ });
1908
+
1909
+ var handleMouseMove = function(e) {
1910
+ if (isResizing && resizeHandle) {
1911
+ // Handle resize
1912
+ var deltaX = e.clientX - resizeStartX;
1913
+ var deltaY = e.clientY - resizeStartY;
1914
+ var newWidth = resizeStartWidth;
1915
+ var newHeight = resizeStartHeight;
1916
+ var newLeft = resizeStartLeft;
1917
+ var newTop = resizeStartTop;
1918
+
1919
+ if (resizeHandle.includes('e')) {
1920
+ newWidth = resizeStartWidth + deltaX;
1921
+ }
1922
+ if (resizeHandle.includes('w')) {
1923
+ newWidth = resizeStartWidth - deltaX;
1924
+ newLeft = resizeStartLeft + deltaX;
1925
+ }
1926
+ if (resizeHandle.includes('s')) {
1927
+ newHeight = resizeStartHeight + deltaY;
1928
+ }
1929
+ if (resizeHandle.includes('n')) {
1930
+ newHeight = resizeStartHeight - deltaY;
1931
+ newTop = resizeStartTop + deltaY;
1932
+ }
1933
+
1934
+ // Apply constraints
1935
+ newWidth = Math.max(parseInt(modal.style.minWidth) || 300, Math.min(newWidth, window.innerWidth - newLeft));
1936
+ newHeight = Math.max(parseInt(modal.style.minHeight) || 200, Math.min(newHeight, window.innerHeight - newTop));
1937
+
1938
+ modal.style.width = newWidth + 'px';
1939
+ modal.style.height = newHeight + 'px';
1940
+ modal.style.left = newLeft + 'px';
1941
+ modal.style.top = newTop + 'px';
1942
+ modal.style.transform = 'none';
1943
+ } else if (isDragging) {
1944
+ // Handle drag
1945
+ var deltaX = e.clientX - dragStartX;
1946
+ var deltaY = e.clientY - dragStartY;
1947
+ var newX = modalStartX + deltaX;
1948
+ var newY = modalStartY + deltaY;
1949
+
1950
+ // Constrain to viewport
1951
+ var maxX = window.innerWidth - modal.offsetWidth;
1952
+ var maxY = window.innerHeight - modal.offsetHeight;
1953
+ newX = Math.max(0, Math.min(newX, maxX));
1954
+ newY = Math.max(0, Math.min(newY, maxY));
1955
+
1956
+ modal.style.left = newX + 'px';
1957
+ modal.style.top = newY + 'px';
1958
+ modal.style.transform = 'none';
1959
+ }
1960
+ };
1961
+
1962
+ document.addEventListener('mousemove', handleMouseMove);
1963
+
1964
+ var handleMouseUp = function() {
1965
+ if (isDragging) {
1966
+ isDragging = false;
1967
+ modal.style.cursor = '';
1968
+ saveStateInspectorPreferences();
1969
+ }
1970
+ if (isResizing) {
1971
+ isResizing = false;
1972
+ resizeHandle = null;
1973
+ saveStateInspectorPreferences();
1974
+ }
1975
+ };
1976
+
1977
+ document.addEventListener('mouseup', handleMouseUp);
1978
+
1979
+ // Resize functionality
1980
+ var resizeHandleSize = 8;
1981
+ var cornerHandleSize = 12; // Larger handles for corners to make them easier to grab
1982
+
1983
+ // Create edge handles first, then corner handles (so corners are on top)
1984
+ var edgeHandles = ['n', 's', 'e', 'w'];
1985
+ var cornerHandles = ['nw', 'ne', 'sw', 'se'];
1986
+
1987
+ // Create edge handles (leaving space for corners)
1988
+ edgeHandles.forEach(function(handle) {
1989
+ var handleEl = document.createElement('div');
1990
+ handleEl.className = 'resize-handle resize-' + handle;
1991
+ handleEl.style.position = 'absolute';
1992
+ handleEl.style.backgroundColor = 'transparent';
1993
+ handleEl.style.zIndex = '10000';
1994
+ handleEl.style.pointerEvents = 'auto';
1995
+
1996
+ if (handle === 'n' || handle === 's') {
1997
+ handleEl.style.height = resizeHandleSize + 'px';
1998
+ handleEl.style.left = cornerHandleSize + 'px';
1999
+ handleEl.style.right = cornerHandleSize + 'px';
2000
+ if (handle === 'n') {
2001
+ handleEl.style.top = '0';
2002
+ handleEl.style.cursor = 'n-resize';
2003
+ } else {
2004
+ handleEl.style.bottom = '0';
2005
+ handleEl.style.cursor = 's-resize';
2006
+ }
2007
+ } else {
2008
+ handleEl.style.width = resizeHandleSize + 'px';
2009
+ handleEl.style.top = cornerHandleSize + 'px';
2010
+ handleEl.style.bottom = cornerHandleSize + 'px';
2011
+ if (handle === 'e') {
2012
+ handleEl.style.right = '0';
2013
+ handleEl.style.cursor = 'e-resize';
2014
+ } else {
2015
+ handleEl.style.left = '0';
2016
+ handleEl.style.cursor = 'w-resize';
2017
+ }
2018
+ }
2019
+
2020
+ handleEl.addEventListener('mousedown', function(e) {
2021
+ e.stopPropagation();
2022
+ e.preventDefault();
2023
+ isResizing = true;
2024
+ resizeHandle = handle;
2025
+ resizeStartX = e.clientX;
2026
+ resizeStartY = e.clientY;
2027
+ var rect = modal.getBoundingClientRect();
2028
+ resizeStartWidth = rect.width;
2029
+ resizeStartHeight = rect.height;
2030
+ resizeStartLeft = rect.left;
2031
+ resizeStartTop = rect.top;
2032
+ });
2033
+
2034
+ modal.appendChild(handleEl);
2035
+ });
2036
+
2037
+ // Create corner handles (higher z-index so they're on top)
2038
+ cornerHandles.forEach(function(handle) {
2039
+ var handleEl = document.createElement('div');
2040
+ handleEl.className = 'resize-handle resize-' + handle;
2041
+ handleEl.style.position = 'absolute';
2042
+ handleEl.style.backgroundColor = 'transparent';
2043
+ handleEl.style.zIndex = '10002';
2044
+ handleEl.style.pointerEvents = 'auto';
2045
+ handleEl.style.width = cornerHandleSize + 'px';
2046
+ handleEl.style.height = cornerHandleSize + 'px';
2047
+
2048
+ if (handle === 'nw') {
2049
+ handleEl.style.top = '0';
2050
+ handleEl.style.left = '0';
2051
+ handleEl.style.cursor = 'nw-resize';
2052
+ } else if (handle === 'ne') {
2053
+ handleEl.style.top = '0';
2054
+ handleEl.style.right = '0';
2055
+ handleEl.style.cursor = 'ne-resize';
2056
+ } else if (handle === 'sw') {
2057
+ handleEl.style.bottom = '0';
2058
+ handleEl.style.left = '0';
2059
+ handleEl.style.cursor = 'sw-resize';
2060
+ } else if (handle === 'se') {
2061
+ handleEl.style.bottom = '0';
2062
+ handleEl.style.right = '0';
2063
+ handleEl.style.cursor = 'se-resize';
2064
+ }
2065
+
2066
+ handleEl.addEventListener('mousedown', function(e) {
2067
+ e.stopPropagation();
2068
+ e.preventDefault();
2069
+ isResizing = true;
2070
+ resizeHandle = handle;
2071
+ resizeStartX = e.clientX;
2072
+ resizeStartY = e.clientY;
2073
+ var rect = modal.getBoundingClientRect();
2074
+ resizeStartWidth = rect.width;
2075
+ resizeStartHeight = rect.height;
2076
+ resizeStartLeft = rect.left;
2077
+ resizeStartTop = rect.top;
2078
+ });
2079
+
2080
+ modal.appendChild(handleEl);
2081
+ });
2082
+
2083
+
2084
+ // Remove state change listener when modal is closed
2085
+ var originalRemove = modal.remove;
2086
+ modal.remove = function() {
2087
+ // Restore original trigger changes
2088
+ room.serializer.decoder.triggerChanges = originalTriggerChanges;
2089
+ originalRemove.call(this);
2090
+ };
2091
+ }
2092
+
2093
+ // Apply panel position based on current setting
2094
+ function applyPanelPosition() {
2095
+ var logoContainer = document.getElementById('debug-logo-container');
2096
+ var menu = document.getElementById('debug-menu');
2097
+ var panels = document.querySelectorAll('[id^="debug-panel-"]');
2098
+
2099
+ var positions = {
2100
+ 'bottom-right': { bottom: '14px', right: '14px', top: 'auto', left: 'auto' },
2101
+ 'bottom-left': { bottom: '14px', left: '14px', top: 'auto', right: 'auto' },
2102
+ 'top-left': { top: '14px', left: '14px', bottom: 'auto', right: 'auto' },
2103
+ 'top-right': { top: '14px', right: '14px', bottom: 'auto', left: 'auto' }
2104
+ };
2105
+
2106
+ var pos = positions[preferences.panelPosition.position] || positions['bottom-right'];
2107
+
2108
+ // Update logo container
2109
+ if (logoContainer) {
2110
+ logoContainer.style.bottom = pos.bottom;
2111
+ logoContainer.style.right = pos.right;
2112
+ logoContainer.style.top = pos.top;
2113
+ logoContainer.style.left = pos.left;
2114
+ }
2115
+
2116
+ // Update menu position
2117
+ if (menu) {
2118
+ if (preferences.panelPosition.position.startsWith('bottom')) {
2119
+ menu.style.bottom = '60px';
2120
+ menu.style.top = 'auto';
2121
+ } else {
2122
+ // For top positions, menu appears below the logo
2123
+ menu.style.top = '60px';
2124
+ menu.style.bottom = 'auto';
2125
+ }
2126
+ menu.style.right = pos.right;
2127
+ menu.style.left = pos.left;
2128
+ }
2129
+
2130
+ // Update panels
2131
+ repositionDebugPanels();
2132
+ }
2133
+
2134
+ // Hide panels for this session
2135
+ function hidePanelsForSession() {
2136
+ panelsHidden = true;
2137
+ savePreferences(); // Save the hidden state
2138
+
2139
+ var logoContainer = document.getElementById('debug-logo-container');
2140
+ var menu = document.getElementById('debug-menu');
2141
+ var panels = document.querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>;
2142
+
2143
+ if (logoContainer) {
2144
+ logoContainer.style.display = 'none';
2145
+ }
2146
+ if (menu) {
2147
+ menu.style.display = 'none';
2148
+ }
2149
+ panels.forEach(function(panel) {
2150
+ panel.style.display = 'none';
2151
+ });
2152
+ }
2153
+
2154
+ // Helper function to format bytes
2155
+ function formatBytes(bytes) {
2156
+ if (bytes === 0) return '0 B';
2157
+ var k = 1024;
2158
+ var sizes = ['B', 'KB', 'MB', 'GB'];
2159
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
2160
+ return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
2161
+ }
2162
+
2163
+ // Helper function to create debug panel for a room
2164
+ function createDebugPanel(uniquePanelId, debugInfo) {
2165
+ // Check if panel already exists
2166
+ var existingPanel = document.getElementById('debug-panel-' + uniquePanelId);
2167
+ if (existingPanel) {
2168
+ return existingPanel;
2169
+ }
2170
+
2171
+ var panel = document.createElement('div');
2172
+ panel.id = 'debug-panel-' + uniquePanelId;
2173
+ panel.style.position = 'fixed';
2174
+ // Position will be set by repositionDebugPanels
2175
+ panel.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
2176
+ panel.style.color = '#fff';
2177
+ panel.style.padding = '8px';
2178
+ panel.style.borderRadius = '6px';
2179
+ panel.style.fontFamily = 'monospace';
2180
+ panel.style.fontSize = '11px';
2181
+ panel.style.zIndex = '999';
2182
+ panel.style.minWidth = '180px';
2183
+ panel.style.marginRight = '6px';
2184
+ panel.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.5)';
2185
+ panel.style.display = panelsHidden ? 'none' : 'block';
2186
+
2187
+ var title = document.createElement('div');
2188
+ title.id = 'debug-title-' + uniquePanelId;
2189
+ title.style.fontWeight = 'bold';
2190
+ title.style.marginBottom = '6px';
2191
+ title.style.borderBottom = '1px solid rgba(255, 255, 255, 0.15)';
2192
+ title.style.paddingBottom = '4px';
2193
+ title.style.display = 'flex';
2194
+ title.style.alignItems = 'center';
2195
+ title.style.gap = '4px';
2196
+ title.style.position = 'relative';
2197
+ title.innerHTML = '<span style="display: inline-flex; align-items: center;"></span><span id="debug-title-text-' + uniquePanelId + '"></span><span id="debug-message-icon-' + uniquePanelId + '" style="display: none; align-items: center; margin-left: auto; cursor: pointer; opacity: 0.6; transition: opacity 0.2s; margin-right: 4px; width: 16px; height: 16px;">' + messageIcon.replace('height="200px" width="200px"', 'height="16" width="16"') + '</span><span id="debug-hamburger-icon-' + uniquePanelId + '" style="display: inline-flex; align-items: center; cursor: pointer; opacity: 0.6; transition: opacity 0.2s; margin-right: 4px; width: 16px; height: 16px;">' + treeViewIcon.replace('height="200px" width="200px"', 'height="16" width="16"') + '</span><span id="debug-info-icon-' + uniquePanelId + '" style="display: inline-flex; align-items: center; cursor: pointer; opacity: 0.6; transition: opacity 0.2s; width: 16px; height: 16px;">' + infoIcon + '</span>';
2198
+
2199
+ // Create tooltip for info icon
2200
+ var tooltip = document.createElement('div');
2201
+ tooltip.id = 'debug-tooltip-' + uniquePanelId;
2202
+ tooltip.style.position = 'absolute';
2203
+ tooltip.style.top = '100%';
2204
+ tooltip.style.right = '0';
2205
+ tooltip.style.marginTop = '4px';
2206
+ tooltip.style.padding = '6px 8px';
2207
+ tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.95)';
2208
+ tooltip.style.color = '#fff';
2209
+ tooltip.style.borderRadius = '4px';
2210
+ tooltip.style.fontSize = '10px';
2211
+ tooltip.style.fontFamily = 'monospace';
2212
+ tooltip.style.zIndex = '1000';
2213
+ tooltip.style.display = 'none';
2214
+ tooltip.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)';
2215
+ tooltip.style.lineHeight = '1.4';
2216
+ tooltip.innerHTML = '<div><strong>Room ID:</strong> ' + debugInfo.roomId + '</div><div><strong>Session ID:</strong> N/A</div><div><strong>Host:</strong> N/A</div>';
2217
+ title.appendChild(tooltip);
2218
+
2219
+ // Add hover handlers - use a small delay to ensure element exists
2220
+ setTimeout(function() {
2221
+ var infoIconElement = document.getElementById('debug-info-icon-' + uniquePanelId);
2222
+ if (infoIconElement) {
2223
+ var showTooltip = function() {
2224
+ tooltip.style.display = 'block';
2225
+ infoIconElement.style.opacity = '1';
2226
+ };
2227
+ var hideTooltip = function() {
2228
+ tooltip.style.display = 'none';
2229
+ infoIconElement.style.opacity = '0.6';
2230
+ };
2231
+
2232
+ infoIconElement.addEventListener('mouseenter', showTooltip);
2233
+ infoIconElement.addEventListener('mouseleave', hideTooltip);
2234
+
2235
+ // Also handle tooltip hover to keep it visible
2236
+ tooltip.style.pointerEvents = 'auto';
2237
+ tooltip.addEventListener('mouseenter', showTooltip);
2238
+ tooltip.addEventListener('mouseleave', hideTooltip);
2239
+ }
2240
+
2241
+ // Add click handler for hamburger icon
2242
+ var hamburgerIconElement = document.getElementById('debug-hamburger-icon-' + uniquePanelId);
2243
+ if (hamburgerIconElement) {
2244
+ hamburgerIconElement.addEventListener('mouseenter', function() {
2245
+ hamburgerIconElement.style.opacity = '1';
2246
+ });
2247
+ hamburgerIconElement.addEventListener('mouseleave', function() {
2248
+ hamburgerIconElement.style.opacity = '0.6';
2249
+ });
2250
+ hamburgerIconElement.addEventListener('click', function(e) {
2251
+ e.stopPropagation();
2252
+ openStateInspectorModal(uniquePanelId);
2253
+ });
2254
+ }
2255
+
2256
+ // Add click handler for message icon
2257
+ var messageIconElement = document.getElementById('debug-message-icon-' + uniquePanelId);
2258
+ if (messageIconElement) {
2259
+ messageIconElement.addEventListener('mouseenter', function() {
2260
+ messageIconElement.style.opacity = '1';
2261
+ });
2262
+ messageIconElement.addEventListener('mouseleave', function() {
2263
+ messageIconElement.style.opacity = '0.6';
2264
+ });
2265
+ messageIconElement.addEventListener('click', function(e) {
2266
+ e.stopPropagation();
2267
+ openSendMessagesModal(uniquePanelId);
2268
+ });
2269
+ }
2270
+ }, 0);
2271
+
2272
+ var content = document.createElement('div');
2273
+ content.id = 'debug-content-' + uniquePanelId;
2274
+
2275
+ panel.appendChild(title);
2276
+ panel.appendChild(content);
2277
+
2278
+ // Prepend panel to body so new panels appear first
2279
+ if (document.body.firstChild) {
2280
+ document.body.insertBefore(panel, document.body.firstChild);
2281
+ } else {
2282
+ document.body.appendChild(panel);
2283
+ }
2284
+
2285
+ return panel;
2286
+ }
2287
+
2288
+ // Reposition all debug panels to stack vertically
2289
+ function repositionDebugPanels() {
2290
+ if (panelsHidden) return;
2291
+
2292
+ var panels = Array.from(document.querySelectorAll('[id^="debug-panel-"]') as NodeListOf<HTMLElement>)
2293
+ .filter(function(panel: HTMLElement) { return panel.style.display !== 'none'; })
2294
+ .reverse(); // Reverse to get oldest first (since new panels are prepended)
2295
+
2296
+ // Calculate logoIcon container width: 22px width + 10px padding on each side = 42px
2297
+ // Add 6px margin to prevent overlap
2298
+ var logoIconOffset = 42 + 6;
2299
+
2300
+ var positions = {
2301
+ 'bottom-right': {
2302
+ start: { bottom: '14px', right: '14px', top: 'auto', left: 'auto' },
2303
+ offset: function(panel, currentRight) { return { right: currentRight + 'px', left: 'auto' }; }
2304
+ },
2305
+ 'bottom-left': {
2306
+ start: { bottom: '14px', left: '14px', top: 'auto', right: 'auto' },
2307
+ offset: function(panel, currentLeft) { return { left: currentLeft + 'px', right: 'auto' }; }
2308
+ },
2309
+ 'top-left': {
2310
+ start: { top: '14px', left: '14px', bottom: 'auto', right: 'auto' },
2311
+ offset: function(panel, currentLeft) { return { left: currentLeft + 'px', right: 'auto' }; }
2312
+ },
2313
+ 'top-right': {
2314
+ start: { top: '14px', right: '14px', bottom: 'auto', left: 'auto' },
2315
+ offset: function(panel, currentRight) { return { right: currentRight + 'px', left: 'auto' }; }
2316
+ }
2317
+ };
2318
+
2319
+ var pos = positions[preferences.panelPosition.position] || positions['bottom-right'];
2320
+ var baseOffset = 14 + logoIconOffset;
2321
+ var currentOffset = baseOffset;
2322
+
2323
+ panels.forEach(function(panel) {
2324
+ // Set base position
2325
+ Object.keys(pos.start).forEach(function(key) {
2326
+ panel.style[key] = pos.start[key];
2327
+ });
2328
+
2329
+ // Apply offset
2330
+ var offset = pos.offset(panel, currentOffset);
2331
+ Object.keys(offset).forEach(function(key) {
2332
+ panel.style[key] = offset[key];
2333
+ });
2334
+
2335
+ currentOffset += panel.offsetWidth + 6;
2336
+ });
2337
+ }
2338
+
2339
+ // Update debug panel content
2340
+ function updateDebugPanel(uniquePanelId, debugInfo) {
2341
+ var contentId = 'debug-content-' + uniquePanelId;
2342
+ var panelId = 'debug-panel-' + uniquePanelId;
2343
+ var titleId = 'debug-title-' + uniquePanelId;
2344
+ var content = document.getElementById(contentId);
2345
+ var panel = document.getElementById(panelId);
2346
+ var title = document.getElementById(titleId);
2347
+
2348
+ if (!content || !panel) {
2349
+ // Only create if panel doesn't exist
2350
+ if (!panel) {
2351
+ createDebugPanel(uniquePanelId, debugInfo);
2352
+ content = document.getElementById(contentId);
2353
+ title = document.getElementById(titleId);
2354
+ repositionDebugPanels();
2355
+ } else {
2356
+ content = document.getElementById(contentId);
2357
+ title = document.getElementById(titleId);
2358
+ }
2359
+ }
2360
+
2361
+ // Update title with room name only (roomId, sessionId, and Host are in tooltip)
2362
+ document.getElementById('debug-title-text-' + uniquePanelId).textContent = debugInfo.roomName;
2363
+ document.getElementById('debug-tooltip-' + uniquePanelId).innerHTML = '<div><strong>Room ID:</strong> ' + debugInfo.roomId + '</div><div><strong>Session ID:</strong> ' + debugInfo.sessionId + '</div><div><strong>Host:</strong> ' + debugInfo.host + '</div>';
2364
+
2365
+ var html = '<div style="line-height: 1.3;">';
2366
+ html += '<div style="font-size: 10px; display: flex; gap: 8px;">';
2367
+ html += '<div style="flex: 1;">';
2368
+ html += '<div style="margin-bottom: 4px;"><div style="display: flex; align-items: center; gap: 6px;"><span style="display: inline-flex; align-items: center; width: 18px; height: 18px; color: #FF9800;">' + envelopeUp + '</span><span style="color: #FF9800;">' + formatBytes(debugInfo.bytesSentPerSec) + '/s</span></div><div style="margin-left: 24px; opacity: 0.7; font-size: 9px;">' + debugInfo.messagesSentPerSec.toFixed(0) + ' messages</div></div>';
2369
+ html += '<div><div style="display: flex; align-items: center; gap: 6px;"><span style="display: inline-flex; align-items: center; width: 18px; height: 18px; color: #2196F3;">' + envelopeDown + '</span><span style="color: #2196F3;">' + formatBytes(debugInfo.bytesReceivedPerSec) + '/s</span></div><div style="margin-left: 24px; opacity: 0.7; font-size: 9px;">' + debugInfo.messagesReceivedPerSec.toFixed(0) + ' messages</div></div>';
2370
+ html += '</div>';
2371
+ html += '<div style="display: flex; flex-direction: column; gap: 4px;">';
2372
+ html += '<canvas id="graph-sent-' + uniquePanelId + '" width="80" height="30" style="display: block;"></canvas>';
2373
+ html += '<canvas id="graph-received-' + uniquePanelId + '" width="80" height="30" style="display: block;"></canvas>';
2374
+ html += '</div>';
2375
+ html += '</div>';
2376
+ html += '</div>';
2377
+
2378
+ content.innerHTML = html;
2379
+
2380
+ // Draw graphs after a short delay to ensure canvas elements are rendered
2381
+ setTimeout(function() {
2382
+ drawGraph('graph-sent-' + uniquePanelId, debugInfo.bytesSentHistory, '#FF9800');
2383
+ drawGraph('graph-received-' + uniquePanelId, debugInfo.bytesReceivedHistory, '#2196F3');
2384
+ }, 10);
2385
+ }
2386
+
2387
+ // Draw graph on canvas
2388
+ function drawGraph(canvasId, data, color) {
2389
+ var canvas = document.getElementById(canvasId) as HTMLCanvasElement;
2390
+ if (!canvas) return;
2391
+
2392
+ var ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
2393
+ var width = canvas.width;
2394
+ var height = canvas.height;
2395
+
2396
+ // Clear canvas
2397
+ ctx.clearRect(0, 0, width, height);
2398
+
2399
+ if (!data || data.length === 0) return;
2400
+
2401
+ // Find min and max values
2402
+ var maxValue = Math.max.apply(Math, data);
2403
+ var minValue = Math.min.apply(Math, data);
2404
+ var range = maxValue - minValue || 1; // Avoid division by zero
2405
+
2406
+ // Padding
2407
+ var padding = 2;
2408
+ var graphWidth = width - padding * 2;
2409
+ var graphHeight = height - padding * 2;
2410
+
2411
+ // Draw grid lines
2412
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
2413
+ ctx.lineWidth = 0.5;
2414
+ for (var i = 0; i <= 4; i++) {
2415
+ var y = padding + (graphHeight / 4) * i;
2416
+ ctx.beginPath();
2417
+ ctx.moveTo(padding, y);
2418
+ ctx.lineTo(width - padding, y);
2419
+ ctx.stroke();
2420
+ }
2421
+
2422
+ // Draw the line
2423
+ ctx.strokeStyle = color;
2424
+ ctx.lineWidth = 1.5;
2425
+ ctx.beginPath();
2426
+
2427
+ for (var i = 0; i < data.length; i++) {
2428
+ var x = padding + (graphWidth / (data.length - 1 || 1)) * i;
2429
+ var normalizedValue = (data[i] - minValue) / range;
2430
+ var y = padding + graphHeight - (normalizedValue * graphHeight);
2431
+
2432
+ if (i === 0) {
2433
+ ctx.moveTo(x, y);
2434
+ } else {
2435
+ ctx.lineTo(x, y);
2436
+ }
2437
+ }
2438
+
2439
+ ctx.stroke();
2440
+
2441
+ // Fill area under the line
2442
+ if (data.length > 0) {
2443
+ ctx.lineTo(width - padding, height - padding);
2444
+ ctx.lineTo(padding, height - padding);
2445
+ ctx.closePath();
2446
+ ctx.fillStyle = color;
2447
+ ctx.globalAlpha = 0.2;
2448
+ ctx.fill();
2449
+ ctx.globalAlpha = 1.0;
2450
+ }
2451
+ }
2452
+
2453
+ // Calculate per-second rates
2454
+ function calculateRates(debugInfo) {
2455
+ var now = Date.now();
2456
+ var elapsed = (now - debugInfo.lastUpdate) / 1000; // seconds
2457
+
2458
+ if (elapsed > 0) {
2459
+ debugInfo.bytesSentPerSec = debugInfo.bytesSentDelta / elapsed;
2460
+ debugInfo.bytesReceivedPerSec = debugInfo.bytesReceivedDelta / elapsed;
2461
+ debugInfo.messagesSentPerSec = debugInfo.messagesSentDelta / elapsed;
2462
+ debugInfo.messagesReceivedPerSec = debugInfo.messagesReceivedDelta / elapsed;
2463
+
2464
+ // Add to history
2465
+ debugInfo.bytesSentHistory.push(debugInfo.bytesSentPerSec);
2466
+ debugInfo.bytesReceivedHistory.push(debugInfo.bytesReceivedPerSec);
2467
+ // debugInfo.historyTimestamps.push(now);
2468
+
2469
+ // Limit history length
2470
+ var maxLen = debugInfo.maxHistoryLength || 60;
2471
+ if (debugInfo.bytesSentHistory.length > maxLen) {
2472
+ debugInfo.bytesSentHistory.shift();
2473
+ debugInfo.bytesReceivedHistory.shift();
2474
+ // debugInfo.historyTimestamps.shift();
2475
+ }
2476
+
2477
+ // Reset deltas
2478
+ debugInfo.bytesSentDelta = 0;
2479
+ debugInfo.bytesReceivedDelta = 0;
2480
+ debugInfo.messagesSentDelta = 0;
2481
+ debugInfo.messagesReceivedDelta = 0;
2482
+ debugInfo.lastUpdate = now;
2483
+ }
2484
+
2485
+ // Update panel
2486
+ updateDebugPanel(debugInfo.uniquePanelId, debugInfo);
2487
+ }
2488
+
2489
+ // Start global update interval if not already running
2490
+ function ensureGlobalUpdateInterval() {
2491
+ if (globalUpdateInterval === null) {
2492
+ globalUpdateInterval = setInterval(function() {
2493
+ // Loop through all panels and calculate rates
2494
+ roomDebugInfo.forEach(function(debugInfo, uniquePanelId) {
2495
+ calculateRates(debugInfo);
2496
+ });
2497
+
2498
+ // Clean up interval if no more panels
2499
+ if (roomDebugInfo.size === 0) {
2500
+ clearInterval(globalUpdateInterval);
2501
+ globalUpdateInterval = null;
2502
+ }
2503
+ }, 1000);
2504
+ }
2505
+ }
2506
+
2507
+ function applyMonkeyPatches() {
2508
+
2509
+ // Helper function to patch a room
2510
+ function patchRoom(room: Room) {
2511
+ if (!room) { return room; }
2512
+
2513
+ // Generate a consistent room ID
2514
+ const roomId = room.roomId;
2515
+ const sessionId = room.sessionId;
2516
+
2517
+ // Generate unique panel ID: use sessionId if available, otherwise use roomId + timestamp
2518
+ const uniquePanelId = sessionId && sessionId !== 'N/A' && sessionId !== ''
2519
+ ? sessionId
2520
+ : roomId + '-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9);
2521
+
2522
+ const transport = room.connection?.transport as WebSocketTransport;
2523
+ const endpoint = transport.ws?.url || 'N/A';
2524
+
2525
+ const debugInfo = {
2526
+ uniquePanelId: uniquePanelId,
2527
+ roomId: roomId,
2528
+ roomName: room.name || 'N/A',
2529
+ sessionId: sessionId || 'N/A',
2530
+ endpoint,
2531
+ host: new URL(endpoint).host,
2532
+ room, // Store room reference for state inspector
2533
+ bytesSent: 0,
2534
+ bytesReceived: 0,
2535
+ messagesSent: 0,
2536
+ messagesReceived: 0,
2537
+ bytesSentDelta: 0,
2538
+ bytesReceivedDelta: 0,
2539
+ messagesSentDelta: 0,
2540
+ messagesReceivedDelta: 0,
2541
+ bytesSentPerSec: 0,
2542
+ bytesReceivedPerSec: 0,
2543
+ messagesSentPerSec: 0,
2544
+ messagesReceivedPerSec: 0,
2545
+ lastUpdate: Date.now(),
2546
+ bytesSentHistory: [],
2547
+ bytesReceivedHistory: [],
2548
+ // historyTimestamps: [],
2549
+ maxHistoryLength: 60, // Keep last 60 data points (1 minute at 1 second intervals)
2550
+ messageTypes: null // Will store message types from __playground_message_types
2551
+ };
2552
+
2553
+ roomDebugInfo.set(uniquePanelId, debugInfo);
2554
+
2555
+ // Listen for __playground_message_types message
2556
+ room.onMessage('__playground_message_types', (messageTypes: any) => {
2557
+ debugInfo.messageTypes = messageTypes;
2558
+
2559
+ // Show/hide message icon based on message types availability
2560
+ var messageIconElement = document.getElementById('debug-message-icon-' + uniquePanelId);
2561
+ if (messageIconElement) {
2562
+ messageIconElement.style.display = messageTypes ? 'inline-flex' : 'none';
2563
+ }
2564
+ });
2565
+
2566
+ // Helper function to track received message/bytes
2567
+ function trackReceivedMessage(data) {
2568
+ // Calculate bytes received
2569
+ var bytes = 0;
2570
+ if (data instanceof Blob) {
2571
+ bytes = data.size;
2572
+ } else if (data instanceof ArrayBuffer) {
2573
+ bytes = data.byteLength;
2574
+ } else if (typeof data === 'string') {
2575
+ bytes = new Blob([data]).size;
2576
+ } else if (data) {
2577
+ try {
2578
+ bytes = new Blob([JSON.stringify(data)]).size;
2579
+ } catch (e) {
2580
+ bytes = new Blob([String(data)]).size;
2581
+ }
2582
+ }
2583
+
2584
+ //
2585
+ // TODO: avoid trackig __playground_message_types messages in the stats
2586
+ //
2587
+ debugInfo.messagesReceived++;
2588
+ debugInfo.messagesReceivedDelta++;
2589
+ debugInfo.bytesReceived += bytes;
2590
+ debugInfo.bytesReceivedDelta += bytes;
2591
+ }
2592
+
2593
+ function trackSentMessage(data) {
2594
+ var bytes = 0;
2595
+ if (data instanceof Blob) {
2596
+ bytes = data.size;
2597
+ } else if (data instanceof ArrayBuffer) {
2598
+ bytes = data.byteLength;
2599
+ } else if (typeof data === 'string') {
2600
+ bytes = new Blob([data]).size;
2601
+ }
2602
+ debugInfo.messagesSent++;
2603
+ debugInfo.messagesSentDelta++;
2604
+ debugInfo.bytesSent += data.length;
2605
+ debugInfo.bytesSentDelta += data.length;
2606
+ }
2607
+
2608
+ // Monkey-patch: WebSocket transport
2609
+ if (transport.ws) {
2610
+ const originalOnMessage = transport.ws.onmessage;
2611
+ const ws = transport.ws;
2612
+
2613
+ transport.ws.onmessage = function(event) {
2614
+ // Clone event data to avoid issues with delayed processing
2615
+ var eventData = event.data;
2616
+ if (eventData instanceof Blob) {
2617
+ eventData = eventData.slice();
2618
+ } else if (eventData instanceof ArrayBuffer) {
2619
+ eventData = eventData.slice(0);
2620
+ } else if (typeof eventData === 'string') {
2621
+ eventData = eventData;
2622
+ }
2623
+
2624
+ trackReceivedMessage(eventData);
2625
+
2626
+ // Apply latency simulation for received messages
2627
+ if (preferences.latencySimulation.enabled && preferences.latencySimulation.delay > 0) {
2628
+ setTimeout(function() {
2629
+ // Create a synthetic event-like object
2630
+ var syntheticEvent = {
2631
+ data: eventData,
2632
+ target: ws,
2633
+ currentTarget: ws,
2634
+ type: 'message'
2635
+ };
2636
+ originalOnMessage.call(ws, syntheticEvent);
2637
+ }, preferences.latencySimulation.delay);
2638
+ } else {
2639
+ return originalOnMessage.apply(this, arguments);
2640
+ }
2641
+ };
2642
+ }
2643
+
2644
+
2645
+ // Monkey-patch: sending messages through room connection
2646
+ const originalSend = room.connection.send.bind(room.connection);
2647
+ room.connection.send = function(data: any) {
2648
+ trackSentMessage(data);
2649
+
2650
+ // Apply latency simulation for sent messages
2651
+ if (preferences.latencySimulation.enabled && preferences.latencySimulation.delay > 0) {
2652
+ var clonedData = data;
2653
+ if (data instanceof ArrayBuffer) {
2654
+ clonedData = data.slice(0);
2655
+ } else if (data instanceof Blob) {
2656
+ clonedData = data.slice(0);
2657
+ } else if (data instanceof Uint8Array || data instanceof DataView || (data.buffer && data.buffer instanceof ArrayBuffer)) {
2658
+ clonedData = new Uint8Array(data).buffer;
2659
+ }
2660
+
2661
+ setTimeout(function() {
2662
+ originalSend(clonedData);
2663
+ }, preferences.latencySimulation.delay / 2);
2664
+ } else {
2665
+ return originalSend(data);
2666
+ }
2667
+ };
2668
+
2669
+ updateDebugPanel(uniquePanelId, debugInfo);
2670
+
2671
+ // Ensure global update interval is running
2672
+ ensureGlobalUpdateInterval();
2673
+
2674
+ // Clean up on room leave
2675
+ room.onLeave.once(() => {
2676
+ roomDebugInfo.delete(uniquePanelId);
2677
+ var panel = document.getElementById('debug-panel-' + uniquePanelId);
2678
+ if (panel) {
2679
+ panel.remove();
2680
+ repositionDebugPanels();
2681
+ }
2682
+ // Clean up interval if no more panels
2683
+ if (roomDebugInfo.size === 0 && globalUpdateInterval !== null) {
2684
+ clearInterval(globalUpdateInterval);
2685
+ globalUpdateInterval = null;
2686
+ }
2687
+ });
2688
+
2689
+
2690
+ return room;
2691
+ }
2692
+
2693
+ // Store original methods that return rooms
2694
+ var originalJoinOrCreate = Client.prototype.joinOrCreate;
2695
+ var originalJoin = Client.prototype.join;
2696
+ var originalCreate = Client.prototype.create;
2697
+ var originalReconnect = Client.prototype.reconnect;
2698
+
2699
+ // Patch joinOrCreate
2700
+ Client.prototype.joinOrCreate = function() {
2701
+ var promise = originalJoinOrCreate.apply(this, arguments);
2702
+ return promise.then(function(room) {
2703
+ return patchRoom(room);
2704
+ });
2705
+ };
2706
+
2707
+ // Patch join
2708
+ Client.prototype.join = function() {
2709
+ var promise = originalJoin.apply(this, arguments);
2710
+ return promise.then(function(room) {
2711
+ return patchRoom(room);
2712
+ });
2713
+ };
2714
+
2715
+ // Patch create
2716
+ Client.prototype.create = function() {
2717
+ var promise = originalCreate.apply(this, arguments);
2718
+ return promise.then(function(room) {
2719
+ return patchRoom(room);
2720
+ });
2721
+ };
2722
+
2723
+ // Patch reconnect
2724
+ if (originalReconnect) {
2725
+ Client.prototype.reconnect = function() {
2726
+ var promise = originalReconnect.apply(this, arguments);
2727
+ return promise.then(function(room) {
2728
+ return patchRoom(room);
2729
+ });
2730
+ };
2731
+ }
2732
+ }
2733
+
2734
+ applyMonkeyPatches();
2735
+
2736
+ // Initialize only after DOM is ready
2737
+ // (in case script is loaded in HEAD tag)
2738
+ if (document.readyState === 'loading') {
2739
+ document.addEventListener('DOMContentLoaded', initialize);
2740
+ } else {
2741
+ // DOM is already ready
2742
+ initialize();
2743
+ }