@delt/claude-alarm 0.7.1 → 0.8.1
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.
- package/dist/channel/server.js +14 -1
- package/dist/channel/server.js.map +1 -1
- package/dist/cli.js +122 -32
- package/dist/cli.js.map +1 -1
- package/dist/dashboard/index.html +226 -11
- package/dist/hub/server.js +74 -10
- package/dist/hub/server.js.map +1 -1
- package/dist/index.js +74 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/dashboard/index.html +226 -11
|
@@ -150,11 +150,43 @@
|
|
|
150
150
|
padding: 12px;
|
|
151
151
|
margin-bottom: 8px;
|
|
152
152
|
cursor: pointer;
|
|
153
|
-
transition: border-color 0.15s;
|
|
153
|
+
transition: border-color 0.15s, opacity 0.15s;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: flex-start;
|
|
156
|
+
gap: 8px;
|
|
154
157
|
}
|
|
155
158
|
.session-card:hover, .session-card.active {
|
|
156
159
|
border-color: var(--accent);
|
|
157
160
|
}
|
|
161
|
+
.session-card.dragging { opacity: 0.4; }
|
|
162
|
+
.session-card.drag-over { border-color: var(--accent); border-style: dashed; }
|
|
163
|
+
.session-body { flex: 1; min-width: 0; }
|
|
164
|
+
.drag-handle {
|
|
165
|
+
flex-shrink: 0;
|
|
166
|
+
color: var(--text-dim);
|
|
167
|
+
font-size: 14px;
|
|
168
|
+
line-height: 1.4;
|
|
169
|
+
cursor: grab;
|
|
170
|
+
opacity: 0.45;
|
|
171
|
+
user-select: none;
|
|
172
|
+
padding-top: 1px;
|
|
173
|
+
transition: opacity 0.15s, color 0.15s;
|
|
174
|
+
}
|
|
175
|
+
.session-card:hover .drag-handle { opacity: 0.8; }
|
|
176
|
+
.drag-handle:hover { opacity: 1 !important; color: var(--accent); }
|
|
177
|
+
.drag-handle:active { cursor: grabbing; }
|
|
178
|
+
.name-edit-input {
|
|
179
|
+
width: 100%;
|
|
180
|
+
background: var(--bg);
|
|
181
|
+
border: 1px solid var(--accent);
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
color: var(--text);
|
|
184
|
+
font-size: 14px;
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
padding: 1px 5px;
|
|
187
|
+
outline: none;
|
|
188
|
+
font-family: inherit;
|
|
189
|
+
}
|
|
158
190
|
.session-name {
|
|
159
191
|
font-size: 14px;
|
|
160
192
|
font-weight: 500;
|
|
@@ -940,6 +972,7 @@
|
|
|
940
972
|
unread: {},
|
|
941
973
|
waitingReply: {},
|
|
942
974
|
permissionRequests: {}, // sessionId -> [{ requestId, toolName, description, inputPreview, timestamp, resolved, behavior }]
|
|
975
|
+
sessionMeta: { names: {}, order: [] }, // custom names + order, keyed by volatile sessionId
|
|
943
976
|
};
|
|
944
977
|
|
|
945
978
|
const $ = (sel) => document.querySelector(sel);
|
|
@@ -978,6 +1011,67 @@
|
|
|
978
1011
|
|
|
979
1012
|
loadMessages();
|
|
980
1013
|
|
|
1014
|
+
// --- Session meta persistence (custom names + order) ---
|
|
1015
|
+
// Keyed by sessionId, which is volatile: same id survives a browser refresh
|
|
1016
|
+
// (terminal stays alive) but changes on reconnect/server restart, so renamed
|
|
1017
|
+
// names and custom order naturally reset when a session truly ends.
|
|
1018
|
+
const META_STORAGE_KEY = 'claude-alarm-session-meta';
|
|
1019
|
+
|
|
1020
|
+
function saveSessionMeta() {
|
|
1021
|
+
try { localStorage.setItem(META_STORAGE_KEY, JSON.stringify(state.sessionMeta)); } catch {}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function loadSessionMeta() {
|
|
1025
|
+
try {
|
|
1026
|
+
const raw = localStorage.getItem(META_STORAGE_KEY);
|
|
1027
|
+
if (raw) {
|
|
1028
|
+
const data = JSON.parse(raw);
|
|
1029
|
+
state.sessionMeta = {
|
|
1030
|
+
names: (data && typeof data.names === 'object' && data.names) || {},
|
|
1031
|
+
order: Array.isArray(data && data.order) ? data.order : [],
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
} catch {}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Drop meta + messages for sessions that no longer exist. Must run only after
|
|
1038
|
+
// a sessions_list arrives (state.sessions populated), never on initial load.
|
|
1039
|
+
function pruneSessionData() {
|
|
1040
|
+
const live = new Set(Object.keys(state.sessions));
|
|
1041
|
+
let metaChanged = false;
|
|
1042
|
+
for (const id of Object.keys(state.sessionMeta.names)) {
|
|
1043
|
+
if (!live.has(id)) { delete state.sessionMeta.names[id]; metaChanged = true; }
|
|
1044
|
+
}
|
|
1045
|
+
const trimmedOrder = state.sessionMeta.order.filter(id => live.has(id));
|
|
1046
|
+
if (trimmedOrder.length !== state.sessionMeta.order.length) {
|
|
1047
|
+
state.sessionMeta.order = trimmedOrder;
|
|
1048
|
+
metaChanged = true;
|
|
1049
|
+
}
|
|
1050
|
+
if (metaChanged) saveSessionMeta();
|
|
1051
|
+
|
|
1052
|
+
let msgChanged = false;
|
|
1053
|
+
for (const id of Object.keys(state.messages)) {
|
|
1054
|
+
if (!live.has(id)) { delete state.messages[id]; msgChanged = true; }
|
|
1055
|
+
}
|
|
1056
|
+
if (msgChanged) saveMessages();
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
loadSessionMeta();
|
|
1060
|
+
|
|
1061
|
+
// Ordered session ids: explicitly-ordered ones first (in saved order),
|
|
1062
|
+
// then any newly-arrived sessions in registration order.
|
|
1063
|
+
function getOrderedSessionIds() {
|
|
1064
|
+
const liveSet = new Set(Object.keys(state.sessions));
|
|
1065
|
+
const ordered = state.sessionMeta.order.filter(id => liveSet.has(id));
|
|
1066
|
+
const orderedSet = new Set(ordered);
|
|
1067
|
+
const rest = Object.keys(state.sessions).filter(id => !orderedSet.has(id));
|
|
1068
|
+
return [...ordered, ...rest];
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function sessionDisplayName(s) {
|
|
1072
|
+
return state.sessionMeta.names[s.id] || s.displayName || s.name;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
981
1075
|
// --- Theme ---
|
|
982
1076
|
function initTheme() {
|
|
983
1077
|
const saved = localStorage.getItem('claude-alarm-theme') || 'dark';
|
|
@@ -1054,6 +1148,7 @@
|
|
|
1054
1148
|
case 'sessions_list':
|
|
1055
1149
|
state.sessions = {};
|
|
1056
1150
|
msg.sessions.forEach(s => { state.sessions[s.id] = s; });
|
|
1151
|
+
pruneSessionData();
|
|
1057
1152
|
renderSessions();
|
|
1058
1153
|
break;
|
|
1059
1154
|
case 'session_connected':
|
|
@@ -1063,6 +1158,9 @@
|
|
|
1063
1158
|
break;
|
|
1064
1159
|
case 'session_disconnected':
|
|
1065
1160
|
delete state.sessions[msg.sessionId];
|
|
1161
|
+
delete state.sessionMeta.names[msg.sessionId];
|
|
1162
|
+
state.sessionMeta.order = state.sessionMeta.order.filter(id => id !== msg.sessionId);
|
|
1163
|
+
saveSessionMeta();
|
|
1066
1164
|
if (state.selectedSession === msg.sessionId) { state.selectedSession = null; renderMessages(); }
|
|
1067
1165
|
renderSessions();
|
|
1068
1166
|
break;
|
|
@@ -1121,7 +1219,7 @@
|
|
|
1121
1219
|
// --- Render ---
|
|
1122
1220
|
function renderSessions() {
|
|
1123
1221
|
const el = $('#sessionsList');
|
|
1124
|
-
const ids =
|
|
1222
|
+
const ids = getOrderedSessionIds();
|
|
1125
1223
|
if (!ids.length) {
|
|
1126
1224
|
el.innerHTML = '<div class="no-sessions">No active sessions.<br>Click + to see connection commands.</div>';
|
|
1127
1225
|
return;
|
|
@@ -1131,18 +1229,128 @@
|
|
|
1131
1229
|
const active = state.selectedSession === id ? ' active' : '';
|
|
1132
1230
|
const cwdDisplay = s.cwd ? s.cwd.replace(/^.*[/\\]/, '') : '';
|
|
1133
1231
|
const unread = state.unread[id] || 0;
|
|
1134
|
-
const dispName = s
|
|
1232
|
+
const dispName = sessionDisplayName(s);
|
|
1135
1233
|
const connTime = new Date(s.connectedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1136
1234
|
return `<div class="session-card${active}" data-id="${id}">
|
|
1137
|
-
<
|
|
1138
|
-
|
|
1139
|
-
|
|
1235
|
+
<span class="drag-handle" title="Drag to reorder">⠿</span>
|
|
1236
|
+
<div class="session-body">
|
|
1237
|
+
<div class="session-name" title="Double-click to rename">${esc(dispName)}${unread ? `<span class="unread-badge">${unread}</span>` : ''}</div>
|
|
1238
|
+
${cwdDisplay ? `<div class="session-cwd" title="${esc(s.cwd)}">${esc(cwdDisplay)} · ${connTime}</div>` : `<div class="session-cwd">${connTime}</div>`}
|
|
1239
|
+
<span class="session-status ${s.status}">${s.status.replace('_', ' ')}</span>
|
|
1240
|
+
</div>
|
|
1140
1241
|
</div>`;
|
|
1141
1242
|
}).join('');
|
|
1243
|
+
bindSessionCards(el);
|
|
1244
|
+
updateImageUI();
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function bindSessionCards(el) {
|
|
1142
1248
|
el.querySelectorAll('.session-card').forEach(card => {
|
|
1143
|
-
|
|
1249
|
+
const id = card.dataset.id;
|
|
1250
|
+
card.addEventListener('click', (e) => {
|
|
1251
|
+
if (e.target.closest('.drag-handle')) return;
|
|
1252
|
+
if (e.target.classList.contains('name-edit-input')) return;
|
|
1253
|
+
selectSession(id);
|
|
1254
|
+
});
|
|
1255
|
+
const nameEl = card.querySelector('.session-name');
|
|
1256
|
+
nameEl.addEventListener('dblclick', (e) => {
|
|
1257
|
+
e.stopPropagation();
|
|
1258
|
+
startRename(id, nameEl);
|
|
1259
|
+
});
|
|
1260
|
+
// Drag only via handle: enable draggable on handle press, reset on dragend
|
|
1261
|
+
const handle = card.querySelector('.drag-handle');
|
|
1262
|
+
handle.addEventListener('mousedown', () => card.setAttribute('draggable', 'true'));
|
|
1263
|
+
handle.addEventListener('touchstart', () => card.setAttribute('draggable', 'true'), { passive: true });
|
|
1264
|
+
// Reset draggable if the handle was pressed but no drag occurred
|
|
1265
|
+
card.addEventListener('mouseup', () => card.setAttribute('draggable', 'false'));
|
|
1266
|
+
card.addEventListener('dragstart', (e) => onDragStart(e, id, card));
|
|
1267
|
+
card.addEventListener('dragend', () => onDragEnd(card));
|
|
1268
|
+
card.addEventListener('dragover', (e) => onDragOver(e, card));
|
|
1269
|
+
card.addEventListener('drop', (e) => onDrop(e, id, card));
|
|
1144
1270
|
});
|
|
1145
|
-
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// --- Rename (inline edit) ---
|
|
1274
|
+
function startRename(id, nameEl) {
|
|
1275
|
+
const s = state.sessions[id];
|
|
1276
|
+
if (!s) return;
|
|
1277
|
+
const input = document.createElement('input');
|
|
1278
|
+
input.type = 'text';
|
|
1279
|
+
input.className = 'name-edit-input';
|
|
1280
|
+
input.value = sessionDisplayName(s);
|
|
1281
|
+
nameEl.innerHTML = '';
|
|
1282
|
+
nameEl.appendChild(input);
|
|
1283
|
+
input.focus();
|
|
1284
|
+
input.select();
|
|
1285
|
+
|
|
1286
|
+
let done = false;
|
|
1287
|
+
const commit = (save) => {
|
|
1288
|
+
if (done) return;
|
|
1289
|
+
done = true;
|
|
1290
|
+
if (save) {
|
|
1291
|
+
const val = input.value.trim();
|
|
1292
|
+
const baseName = s.displayName || s.name;
|
|
1293
|
+
if (!val || val === baseName) delete state.sessionMeta.names[id];
|
|
1294
|
+
else state.sessionMeta.names[id] = val;
|
|
1295
|
+
saveSessionMeta();
|
|
1296
|
+
}
|
|
1297
|
+
renderSessions();
|
|
1298
|
+
};
|
|
1299
|
+
input.addEventListener('keydown', (e) => {
|
|
1300
|
+
e.stopPropagation();
|
|
1301
|
+
if (e.key === 'Enter') { e.preventDefault(); commit(true); }
|
|
1302
|
+
else if (e.key === 'Escape') { e.preventDefault(); commit(false); }
|
|
1303
|
+
});
|
|
1304
|
+
input.addEventListener('blur', () => commit(true));
|
|
1305
|
+
input.addEventListener('click', (e) => e.stopPropagation());
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// --- Reorder (drag & drop) ---
|
|
1309
|
+
let dragSrcId = null;
|
|
1310
|
+
|
|
1311
|
+
function onDragStart(e, id, card) {
|
|
1312
|
+
dragSrcId = id;
|
|
1313
|
+
card.classList.add('dragging');
|
|
1314
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1315
|
+
try { e.dataTransfer.setData('text/plain', id); } catch {}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function onDragEnd(card) {
|
|
1319
|
+
card.classList.remove('dragging');
|
|
1320
|
+
card.setAttribute('draggable', 'false');
|
|
1321
|
+
document.querySelectorAll('.session-card.drag-over').forEach(c => c.classList.remove('drag-over'));
|
|
1322
|
+
dragSrcId = null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function onDragOver(e, card) {
|
|
1326
|
+
if (!dragSrcId || card.dataset.id === dragSrcId) return;
|
|
1327
|
+
e.preventDefault();
|
|
1328
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1329
|
+
document.querySelectorAll('.session-card.drag-over').forEach(c => { if (c !== card) c.classList.remove('drag-over'); });
|
|
1330
|
+
card.classList.add('drag-over');
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function onDrop(e, targetId, card) {
|
|
1334
|
+
e.preventDefault();
|
|
1335
|
+
card.classList.remove('drag-over');
|
|
1336
|
+
if (!dragSrcId || dragSrcId === targetId) return;
|
|
1337
|
+
const rect = card.getBoundingClientRect();
|
|
1338
|
+
const after = (e.clientY - rect.top) > rect.height / 2;
|
|
1339
|
+
reorderSessions(dragSrcId, targetId, after);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function reorderSessions(srcId, targetId, after) {
|
|
1343
|
+
const order = getOrderedSessionIds();
|
|
1344
|
+
const from = order.indexOf(srcId);
|
|
1345
|
+
if (from === -1) return;
|
|
1346
|
+
order.splice(from, 1);
|
|
1347
|
+
let to = order.indexOf(targetId);
|
|
1348
|
+
if (to === -1) return;
|
|
1349
|
+
if (after) to += 1;
|
|
1350
|
+
order.splice(to, 0, srcId);
|
|
1351
|
+
state.sessionMeta.order = order;
|
|
1352
|
+
saveSessionMeta();
|
|
1353
|
+
renderSessions();
|
|
1146
1354
|
}
|
|
1147
1355
|
|
|
1148
1356
|
function selectSession(id) {
|
|
@@ -1152,7 +1360,13 @@
|
|
|
1152
1360
|
$('#msgInput').disabled = false;
|
|
1153
1361
|
$('#sendBtn').disabled = false;
|
|
1154
1362
|
$('#msgInput').placeholder = 'Send a message to session... (Shift+Enter for new line)';
|
|
1155
|
-
renderSessions()
|
|
1363
|
+
// Update active highlight + clear unread badge in place. A full renderSessions()
|
|
1364
|
+
// here would replace the card DOM mid-click and break the rename double-click.
|
|
1365
|
+
document.querySelectorAll('#sessionsList .session-card').forEach(c => {
|
|
1366
|
+
const isSel = c.dataset.id === id;
|
|
1367
|
+
c.classList.toggle('active', isSel);
|
|
1368
|
+
if (isSel) { const badge = c.querySelector('.unread-badge'); if (badge) badge.remove(); }
|
|
1369
|
+
});
|
|
1156
1370
|
renderMessages();
|
|
1157
1371
|
updateImageUI();
|
|
1158
1372
|
$('#msgInput').focus();
|
|
@@ -1177,7 +1391,7 @@
|
|
|
1177
1391
|
}
|
|
1178
1392
|
|
|
1179
1393
|
const s = state.sessions[state.selectedSession];
|
|
1180
|
-
header.textContent = s ? (s
|
|
1394
|
+
header.textContent = s ? sessionDisplayName(s) : state.selectedSession.slice(0, 12);
|
|
1181
1395
|
|
|
1182
1396
|
const msgs = state.messages[state.selectedSession] || [];
|
|
1183
1397
|
if (!msgs.length) {
|
|
@@ -1224,7 +1438,7 @@
|
|
|
1224
1438
|
el.innerHTML = state.notifications.slice(0, 50).map((n, i) => {
|
|
1225
1439
|
const timeStr = relativeTime(n.time);
|
|
1226
1440
|
const session = state.sessions[n.sessionId];
|
|
1227
|
-
const sName = session ? session
|
|
1441
|
+
const sName = session ? sessionDisplayName(session) : n.sessionId.slice(0, 8);
|
|
1228
1442
|
// Check if this is an unresolved permission request
|
|
1229
1443
|
let permButtons = '';
|
|
1230
1444
|
if (n.permRequestId) {
|
|
@@ -1559,6 +1773,7 @@
|
|
|
1559
1773
|
// Keyboard shortcuts: Enter=allow, Esc=deny — works even in textarea when permission is pending
|
|
1560
1774
|
document.addEventListener('keydown', (e) => {
|
|
1561
1775
|
if (e.key !== 'Enter' && e.key !== 'Escape') return;
|
|
1776
|
+
if (e.target.classList && e.target.classList.contains('name-edit-input')) return;
|
|
1562
1777
|
const sid = state.selectedSession;
|
|
1563
1778
|
if (!sid) return;
|
|
1564
1779
|
const reqs = state.permissionRequests[sid] || [];
|
package/dist/hub/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/hub/server.ts
|
|
4
4
|
import http from "http";
|
|
5
|
-
import
|
|
5
|
+
import fs4 from "fs";
|
|
6
6
|
import path4 from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { WebSocketServer, WebSocket } from "ws";
|
|
@@ -653,6 +653,54 @@ function saveConfig(config) {
|
|
|
653
653
|
fs2.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
654
654
|
}
|
|
655
655
|
|
|
656
|
+
// src/shared/crash-guard.ts
|
|
657
|
+
import fs3 from "fs";
|
|
658
|
+
var BURST_WINDOW_MS = 6e4;
|
|
659
|
+
var BURST_LIMIT = 10;
|
|
660
|
+
function appendToLog(text) {
|
|
661
|
+
try {
|
|
662
|
+
fs3.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
663
|
+
fs3.appendFileSync(LOG_FILE, text, "utf-8");
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function describe(err) {
|
|
668
|
+
if (err instanceof Error) return err.stack ?? `${err.name}: ${err.message}`;
|
|
669
|
+
try {
|
|
670
|
+
return JSON.stringify(err);
|
|
671
|
+
} catch {
|
|
672
|
+
return String(err);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function installCrashGuard(context) {
|
|
676
|
+
let recent = [];
|
|
677
|
+
const handle = (kind, err) => {
|
|
678
|
+
const stack = describe(err);
|
|
679
|
+
appendToLog(`
|
|
680
|
+
[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind} (${context})
|
|
681
|
+
${stack}
|
|
682
|
+
`);
|
|
683
|
+
logger.error(`${kind} caught by crash guard: ${stack}`);
|
|
684
|
+
const now = Date.now();
|
|
685
|
+
recent = recent.filter((t) => now - t < BURST_WINDOW_MS);
|
|
686
|
+
recent.push(now);
|
|
687
|
+
if (recent.length > BURST_LIMIT) {
|
|
688
|
+
const msg = `${recent.length} unhandled errors within ${BURST_WINDOW_MS / 1e3}s \u2014 exiting`;
|
|
689
|
+
appendToLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
690
|
+
`);
|
|
691
|
+
logger.error(msg);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
process.on("uncaughtException", (err) => handle("uncaughtException", err));
|
|
696
|
+
process.on("unhandledRejection", (reason) => handle("unhandledRejection", reason));
|
|
697
|
+
}
|
|
698
|
+
function logStartup(context) {
|
|
699
|
+
appendToLog(`
|
|
700
|
+
[${(/* @__PURE__ */ new Date()).toISOString()}] ${context} started (pid ${process.pid})
|
|
701
|
+
`);
|
|
702
|
+
}
|
|
703
|
+
|
|
656
704
|
// src/hub/server.ts
|
|
657
705
|
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
658
706
|
var HubServer = class {
|
|
@@ -696,9 +744,12 @@ var HubServer = class {
|
|
|
696
744
|
this.httpServer = http.createServer((req, res) => this.handleHttp(req, res));
|
|
697
745
|
this.wssChannel = new WebSocketServer({ noServer: true });
|
|
698
746
|
this.wssChannel.on("connection", (ws, req) => this.handleChannelConnection(ws, req));
|
|
747
|
+
this.wssChannel.on("error", (err) => logger.warn(`Channel WebSocket server error: ${err.message}`));
|
|
699
748
|
this.wssDashboard = new WebSocketServer({ noServer: true });
|
|
700
749
|
this.wssDashboard.on("connection", (ws) => this.handleDashboardConnection(ws));
|
|
750
|
+
this.wssDashboard.on("error", (err) => logger.warn(`Dashboard WebSocket server error: ${err.message}`));
|
|
701
751
|
this.httpServer.on("upgrade", (req, socket, head) => {
|
|
752
|
+
socket.on("error", (err) => logger.debug(`Upgrade socket error: ${err.message}`));
|
|
702
753
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
703
754
|
const pathname = url.pathname;
|
|
704
755
|
if (this.token && !this.isLocalRequest(req)) {
|
|
@@ -725,8 +776,11 @@ var HubServer = class {
|
|
|
725
776
|
this.cleanupUploads();
|
|
726
777
|
this.startHeartbeat();
|
|
727
778
|
return new Promise((resolve, reject) => {
|
|
728
|
-
|
|
779
|
+
const onStartupError = (err) => reject(err);
|
|
780
|
+
this.httpServer.once("error", onStartupError);
|
|
729
781
|
this.httpServer.listen(this.port, this.host, () => {
|
|
782
|
+
this.httpServer.removeListener("error", onStartupError);
|
|
783
|
+
this.httpServer.on("error", (err) => logger.error(`HTTP server error: ${err.message}`));
|
|
730
784
|
const displayHost = this.host === "0.0.0.0" ? "127.0.0.1" : this.host;
|
|
731
785
|
logger.info(`Hub server listening on http://${displayHost}:${this.port}`);
|
|
732
786
|
resolve();
|
|
@@ -847,8 +901,8 @@ var HubServer = class {
|
|
|
847
901
|
];
|
|
848
902
|
logger.debug(`Dashboard candidates: ${JSON.stringify(candidates)}`);
|
|
849
903
|
for (const candidate of candidates) {
|
|
850
|
-
if (
|
|
851
|
-
const html =
|
|
904
|
+
if (fs4.existsSync(candidate)) {
|
|
905
|
+
const html = fs4.readFileSync(candidate, "utf-8");
|
|
852
906
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
853
907
|
res.end(html);
|
|
854
908
|
return;
|
|
@@ -895,6 +949,10 @@ var HubServer = class {
|
|
|
895
949
|
handleChannelConnection(ws, req) {
|
|
896
950
|
const isLocal = this.isLocalRequest(req);
|
|
897
951
|
logger.info(`Channel server connected (local: ${isLocal})`);
|
|
952
|
+
ws.on("error", (err) => {
|
|
953
|
+
logger.warn(`Channel WebSocket error: ${err.message}`);
|
|
954
|
+
ws.terminate();
|
|
955
|
+
});
|
|
898
956
|
ws.on("pong", () => {
|
|
899
957
|
for (const [sessionId, sock] of this.channelSockets) {
|
|
900
958
|
if (sock === ws) {
|
|
@@ -1004,6 +1062,10 @@ var HubServer = class {
|
|
|
1004
1062
|
handleDashboardConnection(ws) {
|
|
1005
1063
|
this.dashboardSockets.add(ws);
|
|
1006
1064
|
logger.info(`Dashboard connected (total: ${this.dashboardSockets.size})`);
|
|
1065
|
+
ws.on("error", (err) => {
|
|
1066
|
+
logger.warn(`Dashboard WebSocket error: ${err.message}`);
|
|
1067
|
+
ws.terminate();
|
|
1068
|
+
});
|
|
1007
1069
|
const sessionsMsg = {
|
|
1008
1070
|
type: "sessions_list",
|
|
1009
1071
|
sessions: this.sessions.getAll()
|
|
@@ -1060,11 +1122,11 @@ var HubServer = class {
|
|
|
1060
1122
|
logger.warn("Image upload rejected: exceeds 10MB");
|
|
1061
1123
|
return;
|
|
1062
1124
|
}
|
|
1063
|
-
|
|
1125
|
+
fs4.mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
1064
1126
|
const ext = mimeType.split("/")[1] === "jpeg" ? "jpg" : mimeType.split("/")[1];
|
|
1065
1127
|
const filename = `${randomUUID3()}.${ext}`;
|
|
1066
1128
|
const filePath = path4.join(UPLOADS_DIR, filename);
|
|
1067
|
-
|
|
1129
|
+
fs4.writeFileSync(filePath, buffer);
|
|
1068
1130
|
const forwardMsg = {
|
|
1069
1131
|
type: "image_to_session",
|
|
1070
1132
|
sessionId,
|
|
@@ -1077,7 +1139,7 @@ var HubServer = class {
|
|
|
1077
1139
|
logger.info(`Image saved and forwarded: ${filename} (${buffer.length} bytes)`);
|
|
1078
1140
|
setTimeout(() => {
|
|
1079
1141
|
try {
|
|
1080
|
-
|
|
1142
|
+
fs4.unlinkSync(filePath);
|
|
1081
1143
|
} catch {
|
|
1082
1144
|
}
|
|
1083
1145
|
}, 5 * 60 * 1e3);
|
|
@@ -1247,11 +1309,11 @@ var HubServer = class {
|
|
|
1247
1309
|
}
|
|
1248
1310
|
cleanupUploads() {
|
|
1249
1311
|
try {
|
|
1250
|
-
if (!
|
|
1251
|
-
const files =
|
|
1312
|
+
if (!fs4.existsSync(UPLOADS_DIR)) return;
|
|
1313
|
+
const files = fs4.readdirSync(UPLOADS_DIR);
|
|
1252
1314
|
for (const file of files) {
|
|
1253
1315
|
try {
|
|
1254
|
-
|
|
1316
|
+
fs4.unlinkSync(path4.join(UPLOADS_DIR, file));
|
|
1255
1317
|
} catch {
|
|
1256
1318
|
}
|
|
1257
1319
|
}
|
|
@@ -1295,6 +1357,8 @@ var HubServer = class {
|
|
|
1295
1357
|
}
|
|
1296
1358
|
};
|
|
1297
1359
|
if (process.argv[1] && (process.argv[1].endsWith("hub/server.js") || process.argv[1].endsWith("hub/server.ts"))) {
|
|
1360
|
+
installCrashGuard("hub daemon");
|
|
1361
|
+
logStartup("Hub daemon");
|
|
1298
1362
|
const config = loadConfig();
|
|
1299
1363
|
const hub = new HubServer(config);
|
|
1300
1364
|
hub.start().catch((err) => {
|