@mooncompany/uplink-chat 0.5.0

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.

Potentially problematic release.


This version of @mooncompany/uplink-chat might be problematic. Click here for more details.

Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/uplink.js +279 -0
  4. package/middleware/error-handler.js +69 -0
  5. package/package.json +93 -0
  6. package/public/css/agents.36b98c0f.css +1469 -0
  7. package/public/css/agents.css +1469 -0
  8. package/public/css/app.a6a7f8f5.css +2731 -0
  9. package/public/css/app.css +2731 -0
  10. package/public/css/artifacts.css +444 -0
  11. package/public/css/commands.css +55 -0
  12. package/public/css/connection.css +131 -0
  13. package/public/css/dashboard.css +233 -0
  14. package/public/css/developer.css +328 -0
  15. package/public/css/files.css +123 -0
  16. package/public/css/markdown.css +156 -0
  17. package/public/css/message-actions.css +278 -0
  18. package/public/css/mobile.css +614 -0
  19. package/public/css/panels-unified.css +483 -0
  20. package/public/css/premium.css +415 -0
  21. package/public/css/realtime.css +189 -0
  22. package/public/css/satellites.css +401 -0
  23. package/public/css/shortcuts.css +185 -0
  24. package/public/css/split-view.4def0262.css +673 -0
  25. package/public/css/split-view.css +673 -0
  26. package/public/css/theme-generator.css +391 -0
  27. package/public/css/themes.css +387 -0
  28. package/public/css/timestamps.css +54 -0
  29. package/public/css/variables.css +78 -0
  30. package/public/dist/bundle.b55050c4.js +15757 -0
  31. package/public/favicon.svg +24 -0
  32. package/public/img/agents/ada.png +0 -0
  33. package/public/img/agents/clarice.png +0 -0
  34. package/public/img/agents/dennis-nedry.png +0 -0
  35. package/public/img/agents/elliot-alderson.png +0 -0
  36. package/public/img/agents/main.png +0 -0
  37. package/public/img/agents/scotty.png +0 -0
  38. package/public/img/agents/top-flight-security.png +0 -0
  39. package/public/index.html +1083 -0
  40. package/public/js/agents-data.js +234 -0
  41. package/public/js/agents-ui.js +72 -0
  42. package/public/js/agents.js +1525 -0
  43. package/public/js/app.js +79 -0
  44. package/public/js/appearance-settings.js +111 -0
  45. package/public/js/artifacts.js +432 -0
  46. package/public/js/audio-queue.js +168 -0
  47. package/public/js/bootstrap.js +54 -0
  48. package/public/js/chat.js +1211 -0
  49. package/public/js/commands.js +581 -0
  50. package/public/js/connection-api.js +121 -0
  51. package/public/js/connection.js +1231 -0
  52. package/public/js/context-tracker.js +271 -0
  53. package/public/js/core.js +172 -0
  54. package/public/js/dashboard.js +452 -0
  55. package/public/js/developer.js +432 -0
  56. package/public/js/encryption.js +124 -0
  57. package/public/js/errors.js +122 -0
  58. package/public/js/event-bus.js +77 -0
  59. package/public/js/fetch-utils.js +171 -0
  60. package/public/js/file-handler.js +229 -0
  61. package/public/js/files.js +352 -0
  62. package/public/js/gateway-chat.js +538 -0
  63. package/public/js/logger.js +112 -0
  64. package/public/js/markdown.js +190 -0
  65. package/public/js/message-actions.js +431 -0
  66. package/public/js/message-renderer.js +288 -0
  67. package/public/js/missed-messages.js +235 -0
  68. package/public/js/mobile-debug.js +95 -0
  69. package/public/js/notifications.js +367 -0
  70. package/public/js/offline-queue.js +178 -0
  71. package/public/js/onboarding.js +543 -0
  72. package/public/js/panels.js +156 -0
  73. package/public/js/premium.js +412 -0
  74. package/public/js/realtime-voice.js +844 -0
  75. package/public/js/satellite-sync.js +256 -0
  76. package/public/js/satellite-ui.js +175 -0
  77. package/public/js/satellites.js +1516 -0
  78. package/public/js/settings.js +1087 -0
  79. package/public/js/shortcuts.js +381 -0
  80. package/public/js/split-chat.js +1234 -0
  81. package/public/js/split-resize.js +211 -0
  82. package/public/js/splitview.js +340 -0
  83. package/public/js/storage.js +408 -0
  84. package/public/js/streaming-handler.js +324 -0
  85. package/public/js/stt-settings.js +316 -0
  86. package/public/js/theme-generator.js +661 -0
  87. package/public/js/themes.js +164 -0
  88. package/public/js/timestamps.js +198 -0
  89. package/public/js/tts-settings.js +575 -0
  90. package/public/js/ui.js +267 -0
  91. package/public/js/update-notifier.js +143 -0
  92. package/public/js/utils/constants.js +165 -0
  93. package/public/js/utils/sanitize.js +93 -0
  94. package/public/js/utils/sse-parser.js +195 -0
  95. package/public/js/voice.js +883 -0
  96. package/public/manifest.json +58 -0
  97. package/public/moon_texture.jpg +0 -0
  98. package/public/sw.js +221 -0
  99. package/public/three.min.js +6 -0
  100. package/server/channel.js +529 -0
  101. package/server/chat.js +270 -0
  102. package/server/config-store.js +362 -0
  103. package/server/config.js +159 -0
  104. package/server/context.js +131 -0
  105. package/server/gateway-commands.js +211 -0
  106. package/server/gateway-proxy.js +318 -0
  107. package/server/index.js +22 -0
  108. package/server/logger.js +89 -0
  109. package/server/middleware/auth.js +188 -0
  110. package/server/middleware.js +218 -0
  111. package/server/openclaw-discover.js +308 -0
  112. package/server/premium/index.js +156 -0
  113. package/server/premium/license.js +140 -0
  114. package/server/realtime/bridge.js +837 -0
  115. package/server/realtime/index.js +349 -0
  116. package/server/realtime/tts-stream.js +446 -0
  117. package/server/routes/agents.js +564 -0
  118. package/server/routes/artifacts.js +174 -0
  119. package/server/routes/chat.js +311 -0
  120. package/server/routes/config-settings.js +345 -0
  121. package/server/routes/config.js +603 -0
  122. package/server/routes/files.js +307 -0
  123. package/server/routes/index.js +18 -0
  124. package/server/routes/media.js +451 -0
  125. package/server/routes/missed-messages.js +107 -0
  126. package/server/routes/premium.js +75 -0
  127. package/server/routes/push.js +156 -0
  128. package/server/routes/satellite.js +406 -0
  129. package/server/routes/status.js +251 -0
  130. package/server/routes/stt.js +35 -0
  131. package/server/routes/voice.js +260 -0
  132. package/server/routes/webhooks.js +203 -0
  133. package/server/routes.js +206 -0
  134. package/server/runtime-config.js +336 -0
  135. package/server/share.js +305 -0
  136. package/server/stt/faster-whisper.js +72 -0
  137. package/server/stt/groq.js +51 -0
  138. package/server/stt/index.js +196 -0
  139. package/server/stt/openai.js +49 -0
  140. package/server/sync.js +244 -0
  141. package/server/tailscale-https.js +175 -0
  142. package/server/tts.js +646 -0
  143. package/server/update-checker.js +172 -0
  144. package/server/utils/filename.js +129 -0
  145. package/server/utils.js +147 -0
  146. package/server/watchdog.js +318 -0
  147. package/server/websocket/broadcast.js +359 -0
  148. package/server/websocket/connections.js +339 -0
  149. package/server/websocket/index.js +215 -0
  150. package/server/websocket/routing.js +277 -0
  151. package/server/websocket/sync.js +102 -0
  152. package/server.js +404 -0
  153. package/utils/detect-tool-usage.js +93 -0
  154. package/utils/errors.js +158 -0
  155. package/utils/html-escape.js +84 -0
  156. package/utils/id-sanitize.js +94 -0
  157. package/utils/response.js +130 -0
  158. package/utils/with-retry.js +105 -0
@@ -0,0 +1,288 @@
1
+ // ============================================
2
+ // MESSAGE RENDERER MODULE
3
+ // Shared message rendering, avatars, and formatting
4
+ // Used by both chat.js and split-chat.js
5
+ // ============================================
6
+
7
+ import { UplinkMarkdown } from './markdown.js';
8
+
9
+ // ============================================
10
+ // AVATAR SYSTEM
11
+ // ============================================
12
+
13
+ // Cache for avatar availability checks (agentId -> boolean|undefined)
14
+ const avatarCache = {};
15
+ // Session-stable cache buster (changes on each page load, not per message)
16
+ const avatarCacheBust = Date.now();
17
+
18
+ /**
19
+ * Build an agent avatar element for the given agent ID.
20
+ */
21
+ function buildAgentAvatar(agentId) {
22
+ // Resolve agentId from current satellite if not provided
23
+ if (!agentId) {
24
+ const currentSat = window.UplinkSatellites?.getCurrentId?.() || 'main';
25
+ const satellites = window.UplinkSatellites?.getSatellites?.() || {};
26
+ const satellite = satellites[currentSat];
27
+ agentId = satellite?.agentId || 'main';
28
+ }
29
+
30
+ const container = document.createElement('div');
31
+ container.className = 'agent-avatar';
32
+ container.setAttribute('aria-hidden', 'true');
33
+
34
+ const avatarUrl = `/img/agents/${agentId}.png?t=${avatarCacheBust}`;
35
+
36
+ // Check cache first
37
+ if (avatarCache[agentId] === true) {
38
+ const img = document.createElement('img');
39
+ img.src = avatarUrl;
40
+ img.alt = '';
41
+ img.className = 'agent-avatar-img';
42
+ container.appendChild(img);
43
+ return container;
44
+ } else if (avatarCache[agentId] === false) {
45
+ return null;
46
+ }
47
+
48
+ // First time — try image, fallback to null
49
+ const img = document.createElement('img');
50
+ img.src = avatarUrl;
51
+ img.alt = '';
52
+ img.className = 'agent-avatar-img';
53
+
54
+ img.onload = () => { avatarCache[agentId] = true; };
55
+ img.onerror = () => {
56
+ avatarCache[agentId] = false;
57
+ container.remove();
58
+ };
59
+ container.appendChild(img);
60
+ return container;
61
+ }
62
+
63
+ /**
64
+ * Get agent emoji from the agents module.
65
+ */
66
+ function getAgentEmoji(agentId) {
67
+ const agents = window.UplinkAgents?.getAgents?.() || [];
68
+ const agent = agents.find(a => a.id === agentId);
69
+ return agent?.identity?.emoji || '🤖';
70
+ }
71
+
72
+ // ============================================
73
+ // TEXT FORMATTING
74
+ // ============================================
75
+
76
+ function isValidHttpUrl(str) {
77
+ try {
78
+ const url = new URL(str);
79
+ return url.protocol === 'http:' || url.protocol === 'https:';
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Format message text with markdown.
87
+ */
88
+ function formatMessage(text) {
89
+ if (UplinkMarkdown?.render) {
90
+ const html = UplinkMarkdown.render(text);
91
+ if (UplinkMarkdown.highlightCode) {
92
+ const temp = document.createElement('div');
93
+ temp.innerHTML = html;
94
+ UplinkMarkdown.highlightCode(temp);
95
+ return temp.innerHTML;
96
+ }
97
+ return html;
98
+ }
99
+
100
+ // Fallback basic formatting
101
+ if (!text) return '';
102
+
103
+ let formatted = text
104
+ .replace(/&/g, '&')
105
+ .replace(/</g, '&lt;')
106
+ .replace(/>/g, '&gt;');
107
+
108
+ formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
109
+ formatted = formatted.replace(/`([^`]+)`/g, '<code>$1</code>');
110
+ formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
111
+ formatted = formatted.replace(/__(.+?)__/g, '<strong>$1</strong>');
112
+ formatted = formatted.replace(/\*([^*]+)\*/g, '<em>$1</em>');
113
+ formatted = formatted.replace(/_([^_]+)_/g, '<em>$1</em>');
114
+ formatted = formatted.replace(
115
+ /(https?:\/\/[^\s<]+)/g,
116
+ (match) => isValidHttpUrl(match)
117
+ ? `<a href="${match}" target="_blank" rel="noopener noreferrer">${match}</a>`
118
+ : match
119
+ );
120
+ formatted = formatted.replace(/\n/g, '<br>');
121
+
122
+ return formatted;
123
+ }
124
+
125
+ /**
126
+ * Render markdown text to HTML (alias for formatMessage, used by split-chat).
127
+ */
128
+ function renderMarkdown(text) {
129
+ if (UplinkMarkdown?.render) {
130
+ return UplinkMarkdown.render(text);
131
+ }
132
+ return formatMessage(text);
133
+ }
134
+
135
+ // ============================================
136
+ // MESSAGE RENDERING
137
+ // ============================================
138
+
139
+ /**
140
+ * Add a message to a container element.
141
+ */
142
+ function addMessageToContainer(options) {
143
+ const {
144
+ container,
145
+ text,
146
+ type,
147
+ imageUrl = null,
148
+ showAvatar = false,
149
+ agentId = null,
150
+ timestamp = null,
151
+ scroll = {}
152
+ } = options;
153
+
154
+ if (!container) return null;
155
+
156
+ const div = document.createElement('div');
157
+ div.className = `message ${type}`;
158
+ div.dataset.time = timestamp || Date.now();
159
+
160
+ if (type === 'system') {
161
+ div.setAttribute('role', 'alert');
162
+ div.setAttribute('aria-live', 'polite');
163
+ }
164
+
165
+ // Add agent avatar
166
+ if (type === 'assistant' && showAvatar) {
167
+ const avatar = buildAgentAvatar(agentId);
168
+ if (avatar) {
169
+ div.prepend(avatar);
170
+ }
171
+ }
172
+
173
+ // Image handling
174
+ if (imageUrl && imageUrl !== '__pending_upload__') {
175
+ const img = document.createElement('img');
176
+ img.src = imageUrl;
177
+ img.alt = type === 'user' ? 'Image shared by you' : 'Image from assistant';
178
+ img.loading = 'lazy'; // Lazy load to reduce unnecessary 404s on scroll
179
+ img.onerror = () => {
180
+ // Silently hide broken images — console 404 unavoidable but UX is clean
181
+ img.remove();
182
+ // Only show placeholder if this is a user-uploaded image (not old uploads)
183
+ if (imageUrl.startsWith('/uploads/') && !imageUrl.includes('upload-')) {
184
+ const placeholder = document.createElement('div');
185
+ placeholder.className = 'message-image-expired';
186
+ placeholder.textContent = '🖼️ Image no longer available';
187
+ placeholder.style.opacity = '0.5';
188
+ placeholder.style.fontSize = '0.85em';
189
+ div.insertBefore(placeholder, div.firstChild);
190
+ }
191
+ };
192
+ div.appendChild(img);
193
+ } else if (imageUrl === '__pending_upload__') {
194
+ const placeholder = document.createElement('div');
195
+ placeholder.className = 'message-image-expired';
196
+ placeholder.textContent = '🖼️ Image (upload incomplete)';
197
+ div.appendChild(placeholder);
198
+ }
199
+
200
+ // Text content
201
+ if (text) {
202
+ const textSpan = document.createElement('span');
203
+ textSpan.className = 'message-text';
204
+ textSpan.innerHTML = formatMessage(text);
205
+ div.appendChild(textSpan);
206
+
207
+ if (UplinkMarkdown?.highlightCode) {
208
+ UplinkMarkdown.highlightCode(textSpan);
209
+ }
210
+ }
211
+
212
+ container.appendChild(div);
213
+
214
+ // Scroll management
215
+ const isNearBottom = scroll.isNearBottom !== undefined ? scroll.isNearBottom : true;
216
+ if (isNearBottom) {
217
+ container.scrollTop = container.scrollHeight;
218
+ } else if (type !== 'system' && scroll.onNewMessage) {
219
+ scroll.onNewMessage();
220
+ }
221
+
222
+ // Auto-dismiss system messages after 15 seconds
223
+ if (type === 'system') {
224
+ setTimeout(() => {
225
+ if (div.parentNode) {
226
+ div.style.transition = 'opacity 0.3s, transform 0.3s';
227
+ div.style.opacity = '0';
228
+ div.style.transform = 'translateY(-10px)';
229
+ setTimeout(() => div.remove(), 300);
230
+ }
231
+ }, 15000);
232
+ }
233
+
234
+ return div;
235
+ }
236
+
237
+ // ============================================
238
+ // HTML ESCAPING UTILITIES
239
+ // ============================================
240
+
241
+ function escapeHtml(text) {
242
+ const div = document.createElement('div');
243
+ div.textContent = text || '';
244
+ return div.innerHTML;
245
+ }
246
+
247
+ function escapeAttr(text) {
248
+ return (text || '')
249
+ .replace(/&/g, '&amp;')
250
+ .replace(/"/g, '&quot;')
251
+ .replace(/'/g, '&#39;')
252
+ .replace(/</g, '&lt;')
253
+ .replace(/>/g, '&gt;');
254
+ }
255
+
256
+ // ============================================
257
+ // PUBLIC API
258
+ // ============================================
259
+
260
+ export const UplinkMessageRenderer = {
261
+ buildAgentAvatar,
262
+ getAgentEmoji,
263
+ avatarCache,
264
+ avatarCacheBust,
265
+ formatMessage,
266
+ renderMarkdown,
267
+ isValidHttpUrl,
268
+ addMessageToContainer,
269
+ escapeHtml,
270
+ escapeAttr
271
+ };
272
+
273
+ export {
274
+ buildAgentAvatar,
275
+ getAgentEmoji,
276
+ formatMessage,
277
+ renderMarkdown,
278
+ addMessageToContainer,
279
+ escapeHtml,
280
+ escapeAttr
281
+ };
282
+
283
+ // Backward compat: assign to window
284
+ window.UplinkMessageRenderer = UplinkMessageRenderer;
285
+
286
+ if (typeof logger !== 'undefined') {
287
+ logger.debug('MessageRenderer: Module loaded');
288
+ }
@@ -0,0 +1,235 @@
1
+ // ============================================
2
+ // MISSED MESSAGES MODULE
3
+ // Polls for messages when tab was closed
4
+ // ============================================
5
+
6
+ import { UplinkCore } from './core.js';
7
+
8
+ let pollingInterval = null;
9
+ let isPolling = false;
10
+
11
+ function init() {
12
+ if (window.UplinkLogger?.debug) {
13
+ window.UplinkLogger.debug('[Missed Messages] Initialized');
14
+ }
15
+
16
+ // Check for missed messages immediately on load
17
+ setTimeout(checkForMissedMessages, 1000);
18
+
19
+ // Start periodic polling every 30 seconds
20
+ startPolling();
21
+ }
22
+
23
+ function startPolling() {
24
+ if (pollingInterval) return; // Already polling
25
+
26
+ isPolling = true;
27
+ pollingInterval = setInterval(checkForMissedMessages, 30000); // 30 seconds
28
+ if (window.UplinkLogger?.debug) {
29
+ window.UplinkLogger.debug('[Missed Messages] Started polling every 30 seconds');
30
+ }
31
+ }
32
+
33
+ function stopPolling() {
34
+ if (pollingInterval) {
35
+ clearInterval(pollingInterval);
36
+ pollingInterval = null;
37
+ isPolling = false;
38
+ if (window.UplinkLogger?.debug) {
39
+ window.UplinkLogger.debug('[Missed Messages] Stopped polling');
40
+ }
41
+ }
42
+ }
43
+
44
+ function getAuthHeaders() {
45
+ const settings = JSON.parse(localStorage.getItem('uplink-settings') || '{}');
46
+ const token = settings.gatewayToken || '';
47
+ const headers = {};
48
+ if (token) headers['Authorization'] = `Bearer ${token}`;
49
+ return headers;
50
+ }
51
+
52
+ async function checkForMissedMessages() {
53
+ // M-27: Skip polling if offline or WebSocket disconnected
54
+ if (!navigator.onLine) {
55
+ if (window.UplinkLogger?.debug) {
56
+ window.UplinkLogger.debug('[Missed Messages] Skipping check - browser offline');
57
+ }
58
+ return;
59
+ }
60
+
61
+ // Skip if WebSocket is not connected
62
+ if (window.UplinkConnection && !window.UplinkConnection.isConnected()) {
63
+ if (window.UplinkLogger?.debug) {
64
+ window.UplinkLogger.debug('[Missed Messages] Skipping check - WebSocket disconnected');
65
+ }
66
+ return;
67
+ }
68
+
69
+ try {
70
+ const response = await fetch('/api/missed-messages', {
71
+ headers: getAuthHeaders(),
72
+ });
73
+ if (!response.ok) {
74
+ if (window.UplinkLogger?.warn) {
75
+ window.UplinkLogger.warn('[Missed Messages] Failed to fetch:', response.status);
76
+ }
77
+ return;
78
+ }
79
+
80
+ const data = await response.json();
81
+
82
+ if (data.messages && data.messages.length > 0) {
83
+ if (window.UplinkLogger?.debug) {
84
+ window.UplinkLogger.debug(`[Missed Messages] Retrieved ${data.messages.length} missed messages`);
85
+ }
86
+
87
+ // Display missed messages
88
+ displayMissedMessages(data.messages);
89
+ }
90
+ } catch (error) {
91
+ if (window.UplinkLogger?.warn) {
92
+ window.UplinkLogger.warn('[Missed Messages] Error fetching:', error.message);
93
+ }
94
+ }
95
+ }
96
+
97
+ // Track displayed message hashes to prevent duplicates (H-45)
98
+ const displayedHashes = new Set();
99
+ const MAX_DISPLAYED_HASHES = 500;
100
+
101
+ function hashMessage(text) {
102
+ let hash = 5381;
103
+ for (let i = 0; i < text.length; i++) {
104
+ hash = ((hash << 5) + hash + text.charCodeAt(i)) | 0;
105
+ }
106
+ return hash;
107
+ }
108
+
109
+ function displayMissedMessages(messages) {
110
+ // Sort messages by timestamp
111
+ messages.sort((a, b) => a.timestamp - b.timestamp);
112
+
113
+ // Trim hash set if too large
114
+ if (displayedHashes.size > MAX_DISPLAYED_HASHES) {
115
+ const iter = displayedHashes.values();
116
+ const toRemove = displayedHashes.size - MAX_DISPLAYED_HASHES;
117
+ for (let i = 0; i < toRemove; i++) {
118
+ displayedHashes.delete(iter.next().value);
119
+ }
120
+ }
121
+
122
+ let addedCount = 0;
123
+ for (const msg of messages) {
124
+ // Add each message to the chat
125
+ const messageText = typeof msg.message === 'string' ? msg.message : String(msg.message || '');
126
+
127
+ if (messageText) {
128
+ // Deduplicate: skip messages we've already displayed (H-45)
129
+ const hash = hashMessage(messageText);
130
+ if (displayedHashes.has(hash)) {
131
+ if (window.UplinkLogger?.debug) {
132
+ window.UplinkLogger.debug(`[Missed Messages] Skipping duplicate message (hash: ${hash})`);
133
+ }
134
+ continue;
135
+ }
136
+
137
+ // Also check if message is already visible in the chat DOM
138
+ const existingMessages = document.querySelectorAll('.message.assistant');
139
+ let alreadyVisible = false;
140
+ for (const el of existingMessages) {
141
+ if (el.dataset.originalText === messageText) {
142
+ alreadyVisible = true;
143
+ break;
144
+ }
145
+ }
146
+ if (alreadyVisible) {
147
+ if (window.UplinkLogger?.debug) {
148
+ window.UplinkLogger.debug(`[Missed Messages] Skipping already-visible message`);
149
+ }
150
+ displayedHashes.add(hash);
151
+ continue;
152
+ }
153
+
154
+ try {
155
+ // Use the chat module's addMessage if available
156
+ // Signature: addMessage(text, type, imageUrl, save)
157
+ if (window.UplinkChat && window.UplinkChat.addMessage) {
158
+ window.UplinkChat.addMessage(messageText, 'assistant');
159
+ } else if (window.addMessage) {
160
+ window.addMessage(messageText, 'assistant');
161
+ } else {
162
+ if (window.UplinkLogger?.warn) {
163
+ window.UplinkLogger.warn('[Missed Messages] No addMessage function available');
164
+ }
165
+ }
166
+
167
+ displayedHashes.add(hash);
168
+ addedCount++;
169
+ if (window.UplinkLogger?.debug) {
170
+ window.UplinkLogger.debug(`[Missed Messages] Displayed missed message for satellite ${msg.satelliteId}`);
171
+ }
172
+ } catch (err) {
173
+ if (window.UplinkLogger?.error) {
174
+ window.UplinkLogger.error('[Missed Messages] Error displaying message:', err.message);
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ if (addedCount > 0) {
181
+ // Show a notification that we recovered missed messages
182
+ showMissedMessageNotification(addedCount);
183
+
184
+ // Scroll to bottom to show new messages
185
+ if (window.scrollToBottom) {
186
+ setTimeout(window.scrollToBottom, 100);
187
+ }
188
+ }
189
+ }
190
+
191
+ function showMissedMessageNotification(count) {
192
+ // Create a temporary notification (CSP-safe — no inline styles)
193
+ const notification = document.createElement('div');
194
+ notification.className = 'missed-message-notification';
195
+
196
+ const inner = document.createElement('div');
197
+ inner.className = 'missed-message-banner';
198
+ inner.textContent = `📨 Retrieved ${count} missed message${count > 1 ? 's' : ''} from while you were away`;
199
+ notification.appendChild(inner);
200
+
201
+ // Insert at top of chat area
202
+ const chatContainer = document.getElementById('chatMessages') || document.body;
203
+ chatContainer.insertBefore(notification, chatContainer.firstChild);
204
+
205
+ // Remove after 4 seconds
206
+ setTimeout(() => {
207
+ if (notification.parentNode) {
208
+ notification.parentNode.removeChild(notification);
209
+ }
210
+ }, 4000);
211
+ }
212
+
213
+ // Check for missed messages on tab resume — but only if connected (avoid fetch errors during reconnect)
214
+ document.addEventListener('visibilitychange', () => {
215
+ if (!document.hidden && window.UplinkConnection?.isConnected?.()) {
216
+ if (window.UplinkLogger?.debug) {
217
+ window.UplinkLogger.debug('[Missed Messages] Tab became visible, checking for missed messages');
218
+ }
219
+ checkForMissedMessages();
220
+ }
221
+ });
222
+
223
+ // Expose API
224
+ export const MissedMessages = {
225
+ check: checkForMissedMessages,
226
+ startPolling,
227
+ stopPolling,
228
+ isPolling: () => isPolling
229
+ };
230
+
231
+ // Backward compat: assign to window
232
+ window.MissedMessages = MissedMessages;
233
+
234
+ // Register and init
235
+ UplinkCore.registerModule('missed-messages', init);
@@ -0,0 +1,95 @@
1
+ // Mobile Debug Panel
2
+ // Only loads on non-localhost (mobile via tunnel)
3
+
4
+ const isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1';
5
+
6
+ if (!isLocal) {
7
+ // Only activate on remote/tunnel connections
8
+ const logs = [];
9
+
10
+ function initDebug() {
11
+ const container = document.createElement('div');
12
+ container.id = 'mobileDebugContainer';
13
+ container.innerHTML = `
14
+ <div id="debugToggle" style="
15
+ position: fixed; bottom: 80px; right: 15px; width: 50px; height: 50px;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ color: #fff; border-radius: 50%; display: flex; align-items: center;
18
+ justify-content: center; font-size: 22px; z-index: 99999;
19
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); cursor: pointer;
20
+ user-select: none; -webkit-tap-highlight-color: transparent;
21
+ ">🔧</div>
22
+ <div id="debugPanel" style="
23
+ display: none; position: fixed; bottom: 140px; right: 10px; left: 10px;
24
+ max-height: 40vh; background: rgba(26, 26, 46, 0.98); color: #0f0;
25
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace; font-size: 11px;
26
+ padding: 12px; border-radius: 12px; z-index: 99998; overflow-y: auto;
27
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.5); border: 1px solid rgba(255,255,255,0.1);
28
+ word-break: break-all;
29
+ ">
30
+ <div style="color:#888;margin-bottom:8px;">📱 Mobile Debug Console</div>
31
+ <div id="debugOutput"></div>
32
+ </div>
33
+ `;
34
+ document.body.appendChild(container);
35
+
36
+ const toggle = document.getElementById('debugToggle');
37
+ const panel = document.getElementById('debugPanel');
38
+
39
+ toggle.addEventListener('click', function() {
40
+ panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
41
+ });
42
+
43
+ console.log('🔧 Mobile debug panel ready - tap the button to view logs');
44
+ }
45
+
46
+ function escapeHtmlDebug(str) {
47
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
48
+ }
49
+
50
+ function addLog(type, args) {
51
+ const timestamp = new Date().toLocaleTimeString();
52
+ const color = type === 'ERR' ? '#ff6b6b' : type === 'WRN' ? '#ffd93d' : '#0f0';
53
+ const msg = escapeHtmlDebug(Array.from(args).map(function(a) {
54
+ if (a === null) return 'null';
55
+ if (a === undefined) return 'undefined';
56
+ if (typeof a === 'object') {
57
+ try { return JSON.stringify(a, null, 2); }
58
+ catch(e) { return String(a); }
59
+ }
60
+ return String(a);
61
+ }).join(' '));
62
+
63
+ logs.push({ type, msg, time: timestamp, color });
64
+ if (logs.length > 200) logs.shift();
65
+
66
+ const output = document.getElementById('debugOutput');
67
+ if (output) {
68
+ output.innerHTML = logs.map(function(l) {
69
+ return '<div style="color:' + l.color + ';margin:2px 0;"><span style="color:#666;">' + l.time + '</span> ' + l.msg + '</div>';
70
+ }).join('');
71
+ output.scrollTop = output.scrollHeight;
72
+ }
73
+ }
74
+
75
+ // Override console methods
76
+ const origLog = console.log;
77
+ const origErr = console.error;
78
+ const origWarn = console.warn;
79
+ const origDebug = console.debug;
80
+
81
+ console.log = function() { addLog('LOG', arguments); origLog.apply(console, arguments); };
82
+ console.error = function() { addLog('ERR', arguments); origErr.apply(console, arguments); };
83
+ console.warn = function() { addLog('WRN', arguments); origWarn.apply(console, arguments); };
84
+ console.debug = function() { addLog('DBG', arguments); origDebug.apply(console, arguments); };
85
+
86
+ if (document.readyState === 'loading') {
87
+ document.addEventListener('DOMContentLoaded', initDebug);
88
+ } else {
89
+ initDebug();
90
+ }
91
+ }
92
+
93
+ // Export empty object for module system compatibility
94
+ export const UplinkMobileDebug = {};
95
+ window.UplinkMobileDebug = UplinkMobileDebug;