@remits/remits-cli 0.1.106 → 0.1.107
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/index.js +330 -10
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const ACCOUNT_REPO_INDEX_FILE = path.join(SESSION_DIR, 'account-repos.json');
|
|
|
29
29
|
const SERVICE_STATE_FILE = path.join(SESSION_DIR, 'service-state.json');
|
|
30
30
|
const ISSUES_DIR = path.join(SESSION_DIR, 'issues');
|
|
31
31
|
const WEBSOCKET_STATE_FILE = path.join(SESSION_DIR, 'websocket-state.json');
|
|
32
|
+
const LISTENER_REGISTRATION_STATE_FILE = path.join(SESSION_DIR, 'listener-registration-state.json');
|
|
32
33
|
const AUTO_UPDATE_LOCK_DIR = path.join(SESSION_DIR, 'auto-update.lock');
|
|
33
34
|
const AUTO_UPDATE_DISABLED_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
34
35
|
const REMITS_CLI_PACKAGE_NAME = '@remits/remits-cli';
|
|
@@ -45,6 +46,10 @@ const WEBSOCKET_RECONNECT_DELAY_MS = Number(process.env.REMITS_CLI_WEBSOCKET_REC
|
|
|
45
46
|
const WEBSOCKET_HEARTBEAT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_HEARTBEAT_MS || 10000);
|
|
46
47
|
const WEBSOCKET_CONNECTION_TIMEOUT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_CONNECTION_TIMEOUT_MS || 15000);
|
|
47
48
|
const WEBSOCKET_WATCHDOG_GRACE_MS = Number(process.env.REMITS_CLI_WEBSOCKET_WATCHDOG_GRACE_MS || 30000);
|
|
49
|
+
const SUPPORT_TICKET_DASHBOARD_REFRESH_MS = Number(process.env.REMITS_CLI_SUPPORT_TICKET_REFRESH_MS || 30000);
|
|
50
|
+
const SUPPORT_TICKET_DASHBOARD_LIMIT = Number(process.env.REMITS_CLI_SUPPORT_TICKET_LIMIT || 20);
|
|
51
|
+
const LISTENER_REGISTRATION_INTERVAL_MS = Number(process.env.REMITS_CLI_REGISTRATION_INTERVAL_MS || 5 * 60 * 1000);
|
|
52
|
+
const LISTENER_REGISTRATION_MIN_INTERVAL_MS = Number(process.env.REMITS_CLI_REGISTRATION_MIN_INTERVAL_MS || 5 * 60 * 1000);
|
|
48
53
|
const ACCOUNT_SCAN_EXCLUDE_DIRS = new Set([
|
|
49
54
|
'.git',
|
|
50
55
|
'.gradle',
|
|
@@ -1273,6 +1278,156 @@ function discoverAccountRepos() {
|
|
|
1273
1278
|
};
|
|
1274
1279
|
}
|
|
1275
1280
|
|
|
1281
|
+
function listenerRegistrationRepoAccounts() {
|
|
1282
|
+
return listAccountRepoEntries(readAccountRepoIndex())
|
|
1283
|
+
.sort((a, b) => Number(a.accountId) - Number(b.accountId))
|
|
1284
|
+
.map((entry) => ({
|
|
1285
|
+
accountId: Number(entry.accountId),
|
|
1286
|
+
name: entry.name || null,
|
|
1287
|
+
type: entry.type || null,
|
|
1288
|
+
platformAccountId: entry.platformAccountId || null,
|
|
1289
|
+
routingScope: ['PLATFORM', 'PRODUCT'].includes(String(entry.type || '').toUpperCase())
|
|
1290
|
+
? 'self_and_descendants'
|
|
1291
|
+
: 'self',
|
|
1292
|
+
source: entry.source || null,
|
|
1293
|
+
updatedAt: entry.updatedAt || null
|
|
1294
|
+
}));
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function readListenerRegistrationState() {
|
|
1298
|
+
if (!fs.existsSync(LISTENER_REGISTRATION_STATE_FILE)) {
|
|
1299
|
+
return {};
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
const data = JSON.parse(fs.readFileSync(LISTENER_REGISTRATION_STATE_FILE, 'utf8'));
|
|
1303
|
+
return data && typeof data === 'object' ? data : {};
|
|
1304
|
+
} catch (_) {
|
|
1305
|
+
return {};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function writeListenerRegistrationState(data) {
|
|
1310
|
+
ensureSessionDir();
|
|
1311
|
+
atomicWriteFile(LISTENER_REGISTRATION_STATE_FILE, JSON.stringify(data || {}, null, 2));
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function listenerRegistrationStateKey(session) {
|
|
1315
|
+
const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
|
|
1316
|
+
const userId = session.user && session.user.id ? session.user.id : 'unknown';
|
|
1317
|
+
return baseUrl + '|' + String(userId);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function serviceRegistrationState() {
|
|
1321
|
+
const state = readServiceState();
|
|
1322
|
+
return {
|
|
1323
|
+
running: isListenerRunning(),
|
|
1324
|
+
pid: state && state.pid ? state.pid : (fs.existsSync(LISTENER_PID_FILE) ? fs.readFileSync(LISTENER_PID_FILE, 'utf8').trim() : null),
|
|
1325
|
+
startedAt: state && state.startedAt ? state.startedAt : runtimeState.service.startedAt,
|
|
1326
|
+
dashboardUrl: state && state.dashboardUrl ? state.dashboardUrl : runtimeState.service.dashboardUrl
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function listenerRegistrationPayload(session, options = {}) {
|
|
1331
|
+
const service = serviceRegistrationState();
|
|
1332
|
+
return {
|
|
1333
|
+
client: 'remits-cli',
|
|
1334
|
+
reason: options.reason || 'heartbeat',
|
|
1335
|
+
baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
|
|
1336
|
+
accountId: Number(session.accountId),
|
|
1337
|
+
dataMode: normalizeDataMode(session.dataMode),
|
|
1338
|
+
websocketTopic: session.websocketTopic || (session.user && session.user.uuid ? String(session.user.uuid).replace(/-/g, '') : null),
|
|
1339
|
+
dashboardUrl: service.dashboardUrl || null,
|
|
1340
|
+
agent: getPreferredAgent(),
|
|
1341
|
+
service,
|
|
1342
|
+
repoAccounts: listenerRegistrationRepoAccounts()
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function listenerRegistrationSignature(payload) {
|
|
1347
|
+
return sha256(stableStringify({
|
|
1348
|
+
baseUrl: payload.baseUrl,
|
|
1349
|
+
accountId: payload.accountId,
|
|
1350
|
+
dataMode: payload.dataMode,
|
|
1351
|
+
websocketTopic: payload.websocketTopic,
|
|
1352
|
+
dashboardUrl: payload.dashboardUrl,
|
|
1353
|
+
agent: payload.agent,
|
|
1354
|
+
serviceRunning: payload.service && payload.service.running,
|
|
1355
|
+
repoAccounts: (payload.repoAccounts || []).map((entry) => ({
|
|
1356
|
+
accountId: entry.accountId,
|
|
1357
|
+
type: entry.type,
|
|
1358
|
+
routingScope: entry.routingScope,
|
|
1359
|
+
updatedAt: entry.updatedAt
|
|
1360
|
+
}))
|
|
1361
|
+
}));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
function shouldRegisterCliListener(session, payload, options = {}) {
|
|
1365
|
+
if (options.force === true) {
|
|
1366
|
+
return true;
|
|
1367
|
+
}
|
|
1368
|
+
const state = readListenerRegistrationState();
|
|
1369
|
+
const key = listenerRegistrationStateKey(session);
|
|
1370
|
+
const existing = state[key];
|
|
1371
|
+
if (!existing) {
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
const lastRegisteredAtMs = Number(existing.lastRegisteredAtMs || 0);
|
|
1375
|
+
if (!lastRegisteredAtMs || Date.now() - lastRegisteredAtMs >= LISTENER_REGISTRATION_MIN_INTERVAL_MS) {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
return existing.signature !== listenerRegistrationSignature(payload);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
function rememberCliListenerRegistration(session, payload) {
|
|
1382
|
+
const state = readListenerRegistrationState();
|
|
1383
|
+
const key = listenerRegistrationStateKey(session);
|
|
1384
|
+
state[key] = {
|
|
1385
|
+
baseUrl: payload.baseUrl,
|
|
1386
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
1387
|
+
signature: listenerRegistrationSignature(payload),
|
|
1388
|
+
accountCount: Array.isArray(payload.repoAccounts) ? payload.repoAccounts.length : 0,
|
|
1389
|
+
lastRegisteredAt: new Date().toISOString(),
|
|
1390
|
+
lastRegisteredAtMs: Date.now()
|
|
1391
|
+
};
|
|
1392
|
+
writeListenerRegistrationState(state);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async function registerCliListenerSession(session, options = {}) {
|
|
1396
|
+
if (!session || !session.token) {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
|
|
1400
|
+
const payload = listenerRegistrationPayload(session, options);
|
|
1401
|
+
if (!shouldRegisterCliListener(session, payload, options)) {
|
|
1402
|
+
return { success: true, skipped: true, reason: 'recently_registered' };
|
|
1403
|
+
}
|
|
1404
|
+
const api = buildAxios(baseUrl, session.token, parsePositiveInt(options.timeoutMs, 5000));
|
|
1405
|
+
const response = await api.post('/cli/register', payload);
|
|
1406
|
+
rememberCliListenerRegistration(session, payload);
|
|
1407
|
+
return response.data;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async function bestEffortRegisterCliListeners(options = {}) {
|
|
1411
|
+
const sessions = listSessionsFromStore(readAllSessions()).filter((session) => session && session.token);
|
|
1412
|
+
if (!sessions.length) {
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
const results = await Promise.all(sessions.map(async (session) => {
|
|
1416
|
+
try {
|
|
1417
|
+
const data = await registerCliListenerSession(session, options);
|
|
1418
|
+
return { success: true, accountId: session.accountId, baseUrl: session.baseUrl, data };
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
appendGlobalActivityLog('listener', 'registration.failed', {
|
|
1421
|
+
accountId: session.accountId,
|
|
1422
|
+
baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
|
|
1423
|
+
error: describeError(err)
|
|
1424
|
+
}, 'error');
|
|
1425
|
+
return { success: false, accountId: session.accountId, baseUrl: session.baseUrl, error: describeError(err) };
|
|
1426
|
+
}
|
|
1427
|
+
}));
|
|
1428
|
+
return results;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1276
1431
|
function resolvePreferredAccountId(cwd, flags, session) {
|
|
1277
1432
|
// Explicit --account-id wins over the repo's account-info.json so users can
|
|
1278
1433
|
// target a child account (e.g. a customer) from a parent-account repo.
|
|
@@ -1987,6 +2142,9 @@ async function authCommand(flags) {
|
|
|
1987
2142
|
});
|
|
1988
2143
|
console.log('Authenticated as user', session.user ? session.user.id : 'unknown', 'for account', session.accountId);
|
|
1989
2144
|
console.log('Data mode:', session.dataMode);
|
|
2145
|
+
try {
|
|
2146
|
+
await bestEffortRegisterCliListeners({ cwd, reason: 'auth' });
|
|
2147
|
+
} catch (_) { /* best-effort */ }
|
|
1990
2148
|
|
|
1991
2149
|
try {
|
|
1992
2150
|
const branchName = currentBranch(cwd);
|
|
@@ -4027,6 +4185,10 @@ let runtimeState = {
|
|
|
4027
4185
|
websocket: {},
|
|
4028
4186
|
lastAction: null
|
|
4029
4187
|
};
|
|
4188
|
+
let supportTicketSnapshotCache = {
|
|
4189
|
+
fetchedAtMs: 0,
|
|
4190
|
+
value: null
|
|
4191
|
+
};
|
|
4030
4192
|
|
|
4031
4193
|
function listenerSessionEntriesFromStore(store) {
|
|
4032
4194
|
return listSessionsFromStore(store).filter((session) => session && session.token && session.websocketTopic);
|
|
@@ -4414,6 +4576,7 @@ function exportIssueBundle(snapshot) {
|
|
|
4414
4576
|
copyFileIfExists(SESSIONS_FILE, path.join(globalDir, 'sessions.json'));
|
|
4415
4577
|
copyFileIfExists(CONFIG_FILE, path.join(globalDir, 'config.json'));
|
|
4416
4578
|
copyFileIfExists(SERVICE_STATE_FILE, path.join(globalDir, 'service-state.json'));
|
|
4579
|
+
copyFileIfExists(LISTENER_REGISTRATION_STATE_FILE, path.join(globalDir, 'listener-registration-state.json'));
|
|
4417
4580
|
copyFileIfExists(ACCOUNT_REPO_INDEX_FILE, path.join(globalDir, 'account-repos.json'));
|
|
4418
4581
|
copyFileIfExists(DISPATCH_STATE_FILE, path.join(globalDir, 'dispatch-panes.json'));
|
|
4419
4582
|
copyFileIfExists(TMUX_ACTIVITY_LOG_FILE, path.join(globalDir, 'tmux-activity.log'));
|
|
@@ -4455,6 +4618,7 @@ function buildGlobalFileSnapshot() {
|
|
|
4455
4618
|
{ label: 'config.json', path: CONFIG_FILE, mode: 'json' },
|
|
4456
4619
|
{ label: 'service-state.json', path: SERVICE_STATE_FILE, mode: 'json' },
|
|
4457
4620
|
{ label: 'websocket-state.json', path: WEBSOCKET_STATE_FILE, mode: 'json' },
|
|
4621
|
+
{ label: 'listener-registration-state.json', path: LISTENER_REGISTRATION_STATE_FILE, mode: 'json' },
|
|
4458
4622
|
{ label: 'dispatch-panes.json', path: DISPATCH_STATE_FILE, mode: 'json' },
|
|
4459
4623
|
{ label: 'account-repos.json', path: ACCOUNT_REPO_INDEX_FILE, mode: 'json' },
|
|
4460
4624
|
{ label: 'tmux-activity.log (tail)', path: TMUX_ACTIVITY_LOG_FILE, mode: 'tail' }
|
|
@@ -4511,7 +4675,140 @@ function buildRepoSnapshot(entry) {
|
|
|
4511
4675
|
};
|
|
4512
4676
|
}
|
|
4513
4677
|
|
|
4514
|
-
function
|
|
4678
|
+
function compactSupportTicketRow(row) {
|
|
4679
|
+
if (!row || typeof row !== 'object') {
|
|
4680
|
+
return null;
|
|
4681
|
+
}
|
|
4682
|
+
return {
|
|
4683
|
+
ticketId: row.ticketId || row.id || null,
|
|
4684
|
+
accountId: row.accountId || row.account_id || null,
|
|
4685
|
+
accountName: row.accountName || null,
|
|
4686
|
+
subject: row.subject || null,
|
|
4687
|
+
priority: row.priority || null,
|
|
4688
|
+
status: row.status || null,
|
|
4689
|
+
type: row.type || null,
|
|
4690
|
+
source: row.source || null,
|
|
4691
|
+
assignedTo: row.assignedTo || null,
|
|
4692
|
+
implementationAccountId: row.implementationAccountId || null,
|
|
4693
|
+
implementationAccountName: row.implementationAccountName || null,
|
|
4694
|
+
affectedComponent: row.affectedComponent || null,
|
|
4695
|
+
updatedAt: row.updatedAt || row.updated || row.lastActivityAt || null,
|
|
4696
|
+
activityAt: row.activityAt || row.lastActivityAt || null,
|
|
4697
|
+
activityLabel: row.activityLabel || null
|
|
4698
|
+
};
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
function supportTicketQueueInput(session) {
|
|
4702
|
+
return {
|
|
4703
|
+
action: 'list',
|
|
4704
|
+
accountId: Number(session.accountId),
|
|
4705
|
+
includeChildren: true,
|
|
4706
|
+
includeRoot: true,
|
|
4707
|
+
sortBy: 'triage',
|
|
4708
|
+
limit: Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
async function fetchSupportTicketQueueForSession(session) {
|
|
4713
|
+
const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
|
|
4714
|
+
const api = buildAxios(baseUrl, session.token, 12000);
|
|
4715
|
+
const response = await api.post('/cli/tool', {
|
|
4716
|
+
token: session.token,
|
|
4717
|
+
accountId: Number(session.accountId),
|
|
4718
|
+
branchName: '',
|
|
4719
|
+
dataMode: 'prod',
|
|
4720
|
+
name: 'mcp_support_ticket_queue',
|
|
4721
|
+
callId: 'dashboard-support-ticket-queue-' + crypto.randomUUID(),
|
|
4722
|
+
input: supportTicketQueueInput(session)
|
|
4723
|
+
}).then((r) => r.data);
|
|
4724
|
+
|
|
4725
|
+
const result = response && response.result ? response.result : {};
|
|
4726
|
+
const tickets = Array.isArray(result.tickets)
|
|
4727
|
+
? result.tickets.map(compactSupportTicketRow).filter(Boolean)
|
|
4728
|
+
: [];
|
|
4729
|
+
return {
|
|
4730
|
+
success: response && response.success !== false && response.toolSuccess !== false && result.success !== false,
|
|
4731
|
+
baseUrl,
|
|
4732
|
+
accountId: Number(session.accountId),
|
|
4733
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
4734
|
+
dataMode: result.dataMode || response.dataMode || 'prod',
|
|
4735
|
+
totalCount: result.totalCount || tickets.length,
|
|
4736
|
+
count: result.count || tickets.length,
|
|
4737
|
+
counts: result.counts || {},
|
|
4738
|
+
recommendedTicket: compactSupportTicketRow(result.recommendedTicket),
|
|
4739
|
+
tickets,
|
|
4740
|
+
searchedAccounts: result.searchedAccounts || [],
|
|
4741
|
+
message: response.toolMessage || result.message || null
|
|
4742
|
+
};
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
async function collectSupportTicketSnapshot() {
|
|
4746
|
+
const nowMs = Date.now();
|
|
4747
|
+
if (supportTicketSnapshotCache.value &&
|
|
4748
|
+
nowMs - supportTicketSnapshotCache.fetchedAtMs < SUPPORT_TICKET_DASHBOARD_REFRESH_MS) {
|
|
4749
|
+
return supportTicketSnapshotCache.value;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
const sessions = listSessionsFromStore(readAllSessions())
|
|
4753
|
+
.filter((session) => session && session.token && session.accountId);
|
|
4754
|
+
const seen = new Set();
|
|
4755
|
+
const queueSessions = sessions.filter((session) => {
|
|
4756
|
+
const key = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL) + '|' + String(session.accountId) + '|' +
|
|
4757
|
+
String(session.user && session.user.id ? session.user.id : '');
|
|
4758
|
+
if (seen.has(key)) {
|
|
4759
|
+
return false;
|
|
4760
|
+
}
|
|
4761
|
+
seen.add(key);
|
|
4762
|
+
return true;
|
|
4763
|
+
}).slice(0, 12);
|
|
4764
|
+
|
|
4765
|
+
const queues = [];
|
|
4766
|
+
const errors = [];
|
|
4767
|
+
await Promise.all(queueSessions.map(async (session) => {
|
|
4768
|
+
try {
|
|
4769
|
+
queues.push(await fetchSupportTicketQueueForSession(session));
|
|
4770
|
+
} catch (err) {
|
|
4771
|
+
errors.push({
|
|
4772
|
+
baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
|
|
4773
|
+
accountId: Number(session.accountId),
|
|
4774
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
4775
|
+
message: describeError(err)
|
|
4776
|
+
});
|
|
4777
|
+
}
|
|
4778
|
+
}));
|
|
4779
|
+
|
|
4780
|
+
const ticketsById = {};
|
|
4781
|
+
for (const queue of queues) {
|
|
4782
|
+
for (const ticket of queue.tickets || []) {
|
|
4783
|
+
const key = String(ticket.ticketId || '');
|
|
4784
|
+
if (!key || ticketsById[key]) {
|
|
4785
|
+
continue;
|
|
4786
|
+
}
|
|
4787
|
+
ticketsById[key] = {
|
|
4788
|
+
...ticket,
|
|
4789
|
+
queueAccountId: queue.accountId,
|
|
4790
|
+
baseUrl: queue.baseUrl
|
|
4791
|
+
};
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
|
|
4795
|
+
const value = {
|
|
4796
|
+
fetchedAt: new Date(nowMs).toISOString(),
|
|
4797
|
+
refreshMs: SUPPORT_TICKET_DASHBOARD_REFRESH_MS,
|
|
4798
|
+
queueCount: queues.length,
|
|
4799
|
+
totalCount: queues.reduce((sum, queue) => sum + Number(queue.totalCount || 0), 0),
|
|
4800
|
+
tickets: Object.values(ticketsById).slice(0, Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)),
|
|
4801
|
+
queues,
|
|
4802
|
+
errors
|
|
4803
|
+
};
|
|
4804
|
+
supportTicketSnapshotCache = {
|
|
4805
|
+
fetchedAtMs: nowMs,
|
|
4806
|
+
value
|
|
4807
|
+
};
|
|
4808
|
+
return value;
|
|
4809
|
+
}
|
|
4810
|
+
|
|
4811
|
+
async function collectDashboardSnapshot() {
|
|
4515
4812
|
const accountRepoIndex = readAccountRepoIndex();
|
|
4516
4813
|
const repoEntries = listAccountRepoEntries(accountRepoIndex)
|
|
4517
4814
|
.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
|
|
@@ -4566,6 +4863,7 @@ function collectDashboardSnapshot() {
|
|
|
4566
4863
|
globalFiles,
|
|
4567
4864
|
repos: repoEntries,
|
|
4568
4865
|
platformRepo,
|
|
4866
|
+
supportTickets: await collectSupportTicketSnapshot(),
|
|
4569
4867
|
issues,
|
|
4570
4868
|
lastAction: runtimeState.lastAction
|
|
4571
4869
|
};
|
|
@@ -4621,6 +4919,9 @@ function renderDashboardHtml(snapshot) {
|
|
|
4621
4919
|
'<div id="last-action" class="empty">No dashboard actions have been triggered yet.</div>' +
|
|
4622
4920
|
'</section>' +
|
|
4623
4921
|
'</div>' +
|
|
4922
|
+
'<div class="section-grid">' +
|
|
4923
|
+
'<section class="panel"><h2>Support Tickets</h2><div id="support-ticket-section" class="accordion"></div></section>' +
|
|
4924
|
+
'</div>' +
|
|
4624
4925
|
'<div class="section-grid">' +
|
|
4625
4926
|
'<section class="panel"><h2>Issues & Escalation</h2><div id="issues-section" class="accordion"></div></section>' +
|
|
4626
4927
|
'</div>' +
|
|
@@ -4651,9 +4952,10 @@ function renderDashboardHtml(snapshot) {
|
|
|
4651
4952
|
'function summaryItem(key,value){return \'<div class="summary-item"><span class="k">\'+escapeHtml(key)+\'</span><span class="v">\'+escapeHtml(value)+\'</span></div>\';}' +
|
|
4652
4953
|
'function codeBlock(key,content,extraClass){return \'<pre class="code-block \'+(extraClass||"")+\'" data-scroll-key="\'+escapeHtml(key)+\'">\'+escapeHtml(content||"[missing]")+\'</pre>\';}' +
|
|
4653
4954
|
'function detailsBlock(key,title,meta,bodyHtml,openByDefault){return \'<details class="item" data-key="\'+escapeHtml(key)+\'"\'+(openByDefault?" open":"")+\'><summary><div class="summary-main"><div class="summary-text"><span class="summary-title">\'+escapeHtml(title)+\'</span><span class="summary-meta">\'+meta+\'</span></div></div><span class="summary-caret">›</span></summary><div class="item-body">\'+bodyHtml+\'</div></details>\';}' +
|
|
4654
|
-
'function renderStatusCards(state){const wsValues=Object.values((state.websocket&&state.websocket.connections)||{});const connectedCount=wsValues.filter(function(entry){return entry.status==="connected";}).length;const warnCount=wsValues.filter(function(entry){return entry.status==="connecting";}).length;const errorCount=wsValues.filter(function(entry){return entry.status==="error"||entry.status==="disconnected";}).length;const repoCount=(state.repos||[]).length;const issueCount=(state.issues||[]).length;const criticalIssueCount=(state.issues||[]).filter(function(issue){return issue.severity==="bad";}).length;const tmuxHealthy=state.tmux&&state.tmux.available&&state.tmux.sessionExists;const cards=[' +
|
|
4955
|
+
'function renderStatusCards(state){const wsValues=Object.values((state.websocket&&state.websocket.connections)||{});const connectedCount=wsValues.filter(function(entry){return entry.status==="connected";}).length;const warnCount=wsValues.filter(function(entry){return entry.status==="connecting";}).length;const errorCount=wsValues.filter(function(entry){return entry.status==="error"||entry.status==="disconnected";}).length;const repoCount=(state.repos||[]).length;const issueCount=(state.issues||[]).length;const criticalIssueCount=(state.issues||[]).filter(function(issue){return issue.severity==="bad";}).length;const ticketState=state.supportTickets||{};const ticketErrors=(ticketState.errors||[]).length;const ticketTotal=ticketState.totalCount||0;const tmuxHealthy=state.tmux&&state.tmux.available&&state.tmux.sessionExists;const cards=[' +
|
|
4655
4956
|
'{title:"Service",value:state.service&&state.service.running?"Running":"Stopped",label:"Daemon status",tone:statusTone(state.service&&state.service.running),sub:(state.service&&state.service.dashboardUrl?state.service.dashboardUrl:"No dashboard URL yet")},' +
|
|
4656
4957
|
'{title:"Websockets",value:String(connectedCount)+"/"+String(wsValues.length||0),label:"Connected topics groups",tone:errorCount? "bad" : (warnCount? "warn" : "ok"),sub: errorCount? String(errorCount)+" unhealthy connection(s)" : (warnCount? String(warnCount)+" connecting" : "All connections healthy")},' +
|
|
4958
|
+
'{title:"Tickets",value:String(ticketTotal),label:"Open support queue",tone:ticketErrors?"warn":"ok",sub:ticketErrors?String(ticketErrors)+" queue fetch error(s)":"Fetched "+String(ticketState.queueCount||0)+" queue(s)"},' +
|
|
4657
4959
|
'{title:"Tmux",value:tmuxHealthy?"Ready":"Attention",label:"Dispatch workspace",tone:tmuxHealthy?"ok":(state.tmux&&state.tmux.available?"warn":"bad"),sub:state.tmux&&state.tmux.available?(state.tmux.sessionExists?String((state.tmux.livePanes||[]).length)+" live pane(s)":"tmux installed, session missing"):"tmux not installed"},' +
|
|
4658
4960
|
'{title:"Issues",value:String(issueCount),label:"Detected escalation signals",tone:criticalIssueCount?"bad":(issueCount?"warn":"ok"),sub:issueCount?(criticalIssueCount?String(criticalIssueCount)+" critical signal(s)":"Review before continuing"):"No issue signals detected"},' +
|
|
4659
4961
|
'{title:"Repos",value:String(repoCount),label:"Indexed account repos",tone:repoCount?"ok":"warn",sub:"Last scan: "+formatTime(state.discovery&&state.discovery.scannedAt)}' +
|
|
@@ -4664,15 +4966,18 @@ function renderDashboardHtml(snapshot) {
|
|
|
4664
4966
|
'summaryItem("Repo Matches",String(state.discovery&&state.discovery.matches||0)),' +
|
|
4665
4967
|
'summaryItem("Connections",String(ws.length)),' +
|
|
4666
4968
|
'summaryItem("Topics",String(topics)),' +
|
|
4969
|
+
'summaryItem("Open Tickets",String(state.supportTickets&&state.supportTickets.totalCount||0)),' +
|
|
4667
4970
|
'summaryItem("Tracked Panes",String(tracked))' +
|
|
4668
4971
|
'].join("");}' +
|
|
4669
4972
|
'function renderLastAction(state){const target=document.getElementById("last-action");if(!state.lastAction){target.className="empty";target.textContent="No dashboard actions have been triggered yet.";return;}target.className="summary-item";target.innerHTML=\'<span class="k">Last Action</span><span class="v">\'+escapeHtml(state.lastAction.action||"unknown")+\'</span><div class="muted" style="margin-top:8px;">\'+escapeHtml(formatTime(state.lastAction.at))+\'</div>\'+(state.lastAction.bundlePath?\'<div class="muted" style="margin-top:8px;word-break:break-word;"><strong>Bundle:</strong> \'+escapeHtml(state.lastAction.bundlePath)+\'</div>\':"");}' +
|
|
4670
4973
|
'function renderIssues(state){const target=document.getElementById("issues-section");const issues=state.issues||[];if(!issues.length){target.innerHTML=\'<div class="empty">No automatic issue signals are currently detected. If something still feels wrong, you can still click "Export Issue Bundle" to prepare a package for a Remits system admin.</div>\';return;}target.innerHTML=issues.map(function(issue,index){const tone=issue.severity==="bad"?"bad":"warn";const meta=badge(issue.severity==="bad"?"critical":"warning",tone)+\' <span class="muted">\'+escapeHtml(issue.scope+(issue.repoAccountId?(" account "+issue.repoAccountId):""))+\'</span>\';const evidence=(issue.evidence||[]).map(function(line){return "<li>"+escapeHtml(line)+"</li>";}).join("");const body=\'<div class="stack"><div class="muted">\'+escapeHtml(issue.summary||"")+\'</div><div class="mini-card"><strong>Recommendation</strong>\'+escapeHtml(issue.recommendation||"") + \'</div><div><strong style="display:block;margin-bottom:8px;">Evidence</strong><ul style="margin:0;padding-left:18px;display:grid;gap:8px;">\'+(evidence||"<li>No evidence lines captured.</li>")+\'</ul></div></div>\';return detailsBlock("issue:"+issue.id,(index+1)+". "+issue.title,meta,body,index===0);}).join("");}' +
|
|
4974
|
+
'function renderSupportTickets(state){const target=document.getElementById("support-ticket-section");const support=state.supportTickets||{};const tickets=support.tickets||[];const queues=support.queues||[];const errors=support.errors||[];let html=\'<div class="mini-grid"><div class="mini-card"><strong>Total Open</strong>\'+escapeHtml(String(support.totalCount||0))+\'</div><div class="mini-card"><strong>Queues</strong>\'+escapeHtml(String(support.queueCount||0))+\'</div><div class="mini-card"><strong>Fetched</strong>\'+escapeHtml(formatTime(support.fetchedAt))+\'</div></div>\';if(errors.length){html+=detailsBlock("support:errors","Queue fetch errors",badge(String(errors.length)+" error(s)","warn"),codeBlock("support-errors",JSON.stringify(errors,null,2),"log"),false);}if(!queues.length&&!errors.length){target.innerHTML=html+\'<div class="empty">No authenticated support-ticket queue is available yet.</div>\';return;}const recs=queues.map(function(queue){return queue.recommendedTicket?Object.assign({queueAccountId:queue.accountId,baseUrl:queue.baseUrl},queue.recommendedTicket):null;}).filter(Boolean);if(recs.length){const recBody=recs.map(function(ticket,index){return supportTicketDetails(ticket,"support-rec:"+index);}).join("");html+=detailsBlock("support:recommended","Recommended next tickets",badge(String(recs.length)+" queue(s)","ok"),recBody,true);}if(tickets.length){html+=tickets.map(function(ticket,index){const meta=badge(ticket.priority||"priority","warn")+\' \'+badge(ticket.status||"status",ticket.status==="open"?"ok":"warn")+\' <span class="muted">\'+escapeHtml(ticket.accountName||ticket.accountId||"unknown account")+\'</span>\';return detailsBlock("support-ticket:"+ticket.ticketId,String(ticket.ticketId||"ticket")+" "+(ticket.subject||""),meta,supportTicketDetails(ticket,"support-ticket:"+index),index===0&&!recs.length);}).join("");}else{html+=\'<div class="empty">No open support tickets were returned by the current queues.</div>\';}target.innerHTML=html;}' +
|
|
4975
|
+
'function supportTicketDetails(ticket,key){const command="remits-cli tool --name mcp_support_ticket --input "+JSON.stringify(JSON.stringify({action:"read",ticketId:String(ticket.ticketId||"")}))+" --data-mode prod";return \'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Ticket</strong>\'+escapeHtml(ticket.ticketId||"n/a")+\'</div><div class="mini-card"><strong>Account</strong>\'+escapeHtml((ticket.accountName||"n/a")+" ("+(ticket.accountId||"n/a")+")")+\'</div><div class="mini-card"><strong>Type</strong>\'+escapeHtml(ticket.type||"n/a")+\'</div><div class="mini-card"><strong>Assigned</strong>\'+escapeHtml(ticket.assignedTo||"unassigned")+\'</div><div class="mini-card"><strong>Implementation</strong>\'+escapeHtml((ticket.implementationAccountName||"n/a")+" ("+(ticket.implementationAccountId||"n/a")+")")+\'</div><div class="mini-card"><strong>Activity</strong>\'+escapeHtml(ticket.activityLabel||formatTime(ticket.activityAt||ticket.updatedAt))+\'</div></div><div class="muted">\'+escapeHtml(ticket.affectedComponent||"No affected component recorded.")+\'</div>\'+codeBlock(key+":cmd",command)+\'</div>\';}' +
|
|
4671
4976
|
'function renderWebsockets(state){const entries=Object.entries((state.websocket&&state.websocket.connections)||{});const target=document.getElementById("websocket-section");if(!entries.length){target.innerHTML=\'<div class="empty">No websocket connections are registered yet.</div>\';return;}target.innerHTML=entries.map(function(pair){const baseUrl=pair[0];const info=pair[1]||{};const topicEntries=Object.entries(info.topics||{});const recentMessages=Array.isArray(info.recentMessages)?info.recentMessages:[];const tone=info.status==="connected"?"ok":(info.status==="connecting"?"warn":"bad");const summaryMeta=badge(info.status||"unknown",tone)+\' <span class="muted">\' + escapeHtml(String(topicEntries.length)) + \' topic(s)</span>\' + \' <span class="muted">\' + escapeHtml(String(recentMessages.length)) + \' recent message(s)</span>\';const topicBody=topicEntries.length?topicEntries.map(function(topicPair){const topic=topicPair[0];const topicInfo=topicPair[1]||{};const mini=\'<div class="mini-grid"><div class="mini-card"><strong>Accounts</strong>\'+escapeHtml((topicInfo.accountIds||[]).join(", ")||"n/a")+\'</div><div class="mini-card"><strong>Subscribed</strong>\'+escapeHtml(formatTime(topicInfo.subscribedAt))+\'</div><div class="mini-card"><strong>Last Message</strong>\'+escapeHtml(formatTime(topicInfo.lastMessageAt))+\'</div></div>\';return detailsBlock("ws-topic:"+baseUrl+":"+topic,topic,"topic subscription",mini,false);}).join(""):\'<div class="empty">No topic subscriptions recorded.</div>\';const messageBody=recentMessages.length?recentMessages.map(function(message,index){const messageMeta=badge(message.duplicate?"duplicate":"handled",message.duplicate?"warn":"ok")+\' <span class="muted">\'+escapeHtml(message.source||"live")+\'</span>\' + \' <span class="muted">\'+escapeHtml(message.type||"unknown-type")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Message ID</strong>\'+escapeHtml(message.messageId||"n/a")+\'</div><div class="mini-card"><strong>Timestamp</strong>\'+escapeHtml(message.timestamp?String(message.timestamp):"n/a")+\'</div><div class="mini-card"><strong>Received</strong>\'+escapeHtml(formatTime(message.receivedAt))+\'</div><div class="mini-card"><strong>Accounts</strong>\'+escapeHtml((message.accountIds||[]).join(", ")||"n/a")+\'</div></div>\'+codeBlock("ws-msg:"+baseUrl+":"+(message.messageId||index),JSON.stringify(message.payload||{},null,2)) + \'</div>\';return detailsBlock("ws-message:"+baseUrl+":"+(message.messageId||index),(message.type||"message")+" "+(message.messageId||""),messageMeta,body,false);}).join(""):\'<div class="empty">No recent websocket messages have been recorded yet.</div>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Status</strong>\'+escapeHtml(info.status||"unknown")+\'</div><div class="mini-card"><strong>Connected</strong>\'+escapeHtml(formatTime(info.connectedAt))+\'</div><div class="mini-card"><strong>Last Message</strong>\'+escapeHtml(formatTime(info.lastMessageAt))+\'</div><div class="mini-card"><strong>Last Error</strong>\'+escapeHtml(info.lastError||"none")+\'</div></div><div class="stack">\'+detailsBlock("ws-recent:"+baseUrl,"Recent messages","latest live and missed-message traffic",messageBody,false)+topicBody+\'</div></div>\';return detailsBlock("ws:"+baseUrl,baseUrl,summaryMeta,body,entries.length===1);}).join("");}' +
|
|
4672
4977
|
'function renderTmux(state){const target=document.getElementById("tmux-section");const panes=state.tmux&&state.tmux.livePanes||[];const trackedText=state.tmux&&state.tmux.trackedPanes?state.tmux.trackedPanes:"{}";const launchSupported=state.tmux&&state.tmux.terminalLaunchSupported;const attachCommand="tmux attach -t "+(state.tmux&&state.tmux.sessionName||"remits-listener");const health=\'<div class="mini-grid"><div class="mini-card"><strong>tmux Installed</strong>\'+escapeHtml(String(state.tmux&&state.tmux.available))+\'</div><div class="mini-card"><strong>Session Exists</strong>\'+escapeHtml(String(state.tmux&&state.tmux.sessionExists))+\'</div><div class="mini-card"><strong>Session Name</strong>\'+escapeHtml(state.tmux&&state.tmux.sessionName||"n/a")+\'</div><div class="mini-card"><strong>Live Panes</strong>\'+escapeHtml(String(panes.length))+\'</div><div class="mini-card"><strong>Open from Dashboard</strong>\'+escapeHtml(launchSupported?"supported":"not supported")+\'</div></div><div class="muted" style="margin-bottom:14px;">\'+escapeHtml(launchSupported?"Use the Open Tmux Terminal button to bring Terminal forward and attach automatically.":"Dashboard-to-terminal launch is not supported on this platform. Run the attach command manually.")+\'</div>\';const quickAttach=detailsBlock("tmux:attach","Quick attach command","manual fallback",codeBlock("tmux-attach-cmd",attachCommand),false);const paneSection=detailsBlock("tmux:panes","Live panes",escapeHtml(String(panes.length))+" pane(s)",codeBlock("tmux-live-panes",JSON.stringify(panes,null,2),"log"),false);const trackedSection=detailsBlock("tmux:tracked","Tracked pane routing","ticket/task to pane mapping",codeBlock("tmux-tracked",trackedText),false);const logFile=(state.globalFiles||[]).find(function(file){return file.label==="tmux-activity.log (tail)";});const logSection=detailsBlock("tmux:activity","Activity tail","stream of tmux and websocket events",codeBlock("tmux-log",logFile&&logFile.content||"[missing]","log"),true);target.innerHTML=health+quickAttach+paneSection+trackedSection+logSection;const openButton=document.querySelector(\'[data-action="open-tmux"]\');if(openButton){openButton.disabled=!launchSupported||!(state.tmux&&state.tmux.available);}}' +
|
|
4673
4978
|
'function renderGlobalFiles(state){const target=document.getElementById("global-files");const files=(state.globalFiles||[]).filter(function(file){return file.label!=="tmux-activity.log (tail)";});if(!files.length){target.innerHTML=\'<div class="empty">No global files found.</div>\';return;}target.innerHTML=files.map(function(file){const tone=file.exists?"ok":"warn";const meta=badge(file.exists?"present":"missing",tone)+\' <span class="muted">\'+escapeHtml(file.updatedAt?formatTime(file.updatedAt):"not found")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Path</strong>\'+escapeHtml(file.path||"")+\'</div><div class="mini-card"><strong>Size</strong>\'+escapeHtml(String(file.size||0))+\' bytes</div><div class="mini-card"><strong>Updated</strong>\'+escapeHtml(formatTime(file.updatedAt))+\'</div></div>\'+codeBlock("global:"+file.label,file.content)+(file.label==="service-state.json"?\'<div class="muted">Use this file to discover the dashboard URL and runtime websocket summary.</div>\':"")+\'</div>\';return detailsBlock("global:"+file.label,file.label,meta,body,false);}).join("");}' +
|
|
4674
4979
|
'function renderRepos(state){const target=document.getElementById("repo-section");const repos=state.repos||[];if(!repos.length){target.innerHTML=\'<div class="empty">No indexed account repos were found. Use "Rescan Account Repos" to rebuild the index.</div>\';return;}target.innerHTML=repos.map(function(repo){const tone=repo.exists?"ok":"warn";const meta=badge(repo.exists?"available":"missing",tone)+\' <span class="muted">type: \'+escapeHtml(repo.type||"n/a")+\'</span>\';const files=(repo.files||[]).map(function(file){const fileMeta=badge(file.exists?"present":"missing",file.exists?"ok":"warn")+\' <span class="muted">\'+escapeHtml(file.updatedAt?formatTime(file.updatedAt):"not found")+\'</span>\';const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Path</strong>\'+escapeHtml(file.path||"")+\'</div><div class="mini-card"><strong>Size</strong>\'+escapeHtml(String(file.size||0))+\' bytes</div><div class="mini-card"><strong>Updated</strong>\'+escapeHtml(formatTime(file.updatedAt))+\'</div></div>\'+codeBlock("repo:"+repo.accountId+":"+file.label,file.content,file.label.indexOf("session log")>=0?"log":"")+\'</div>\';return detailsBlock("repo-file:"+repo.accountId+":"+file.label,file.label,fileMeta,body,false);}).join("");const body=\'<div class="stack"><div class="mini-grid"><div class="mini-card"><strong>Directory</strong>\'+escapeHtml(repo.directory||"")+\'</div><div class="mini-card"><strong>Source</strong>\'+escapeHtml(repo.source||"n/a")+\'</div><div class="mini-card"><strong>Platform Account</strong>\'+escapeHtml(String(repo.platformAccountId||"n/a"))+\'</div></div><div class="repo-files">\'+files+\'</div></div>\';return detailsBlock("repo:"+repo.accountId,(repo.name||"Unknown")+" ("+repo.accountId+")",meta,body,false);}).join("");}' +
|
|
4675
|
-
'function renderAll(state){const detailState=saveDetailsState();const scrollState=saveScrollState();dashboardState=state;document.getElementById("generated-at").textContent=formatTime(state.generatedAt);renderStatusCards(state);renderSummary(state);renderLastAction(state);renderIssues(state);renderWebsockets(state);renderTmux(state);renderGlobalFiles(state);renderRepos(state);restoreDetailsState(detailState);restoreScrollState(scrollState);}' +
|
|
4980
|
+
'function renderAll(state){const detailState=saveDetailsState();const scrollState=saveScrollState();dashboardState=state;document.getElementById("generated-at").textContent=formatTime(state.generatedAt);renderStatusCards(state);renderSummary(state);renderLastAction(state);renderSupportTickets(state);renderIssues(state);renderWebsockets(state);renderTmux(state);renderGlobalFiles(state);renderRepos(state);restoreDetailsState(detailState);restoreScrollState(scrollState);}' +
|
|
4676
4981
|
'async function fetchState(){const res=await fetch("/api/state",{cache:"no-store"});if(!res.ok)throw new Error("State refresh failed with "+res.status);return res.json();}' +
|
|
4677
4982
|
'function showToast(message){const toast=document.getElementById("toast");toast.textContent=message;toast.classList.add("show");clearTimeout(showToast._timer);showToast._timer=setTimeout(function(){toast.classList.remove("show");},2400);}' +
|
|
4678
4983
|
'async function triggerAction(action,button){if(actionInFlight)return;actionInFlight=true;button.disabled=true;try{const res=await fetch("/action",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","Accept":"application/json"},body:"action="+encodeURIComponent(action)});if(!res.ok)throw new Error("Action failed with "+res.status);const payload=await res.json();renderAll(payload.state);if(payload.result&&payload.result.bundlePath){showToast("Issue bundle prepared at "+payload.result.bundlePath);}else{showToast(action.replace(/-/g," ")+" complete");}}catch(err){showToast(err.message||String(err));}finally{actionInFlight=false;button.disabled=false;}}' +
|
|
@@ -4731,9 +5036,10 @@ function startDashboardServer(preferredPort) {
|
|
|
4731
5036
|
const requestUrl = new URL(req.url, 'http://' + DASHBOARD_HOST + ':' + (runtimeState.service.dashboardPort || preferredPort));
|
|
4732
5037
|
if (req.method === 'GET' && requestUrl.pathname === '/api/state') {
|
|
4733
5038
|
try {
|
|
5039
|
+
const snapshot = await collectDashboardSnapshot();
|
|
4734
5040
|
res.statusCode = 200;
|
|
4735
5041
|
res.setHeader('Content-Type', 'application/json');
|
|
4736
|
-
res.end(JSON.stringify(
|
|
5042
|
+
res.end(JSON.stringify(snapshot, null, 2));
|
|
4737
5043
|
} catch (err) {
|
|
4738
5044
|
sendDashboardError(res, err, true);
|
|
4739
5045
|
}
|
|
@@ -4756,11 +5062,13 @@ function startDashboardServer(preferredPort) {
|
|
|
4756
5062
|
at: new Date().toISOString()
|
|
4757
5063
|
};
|
|
4758
5064
|
if (action === 'export-issues') {
|
|
4759
|
-
const bundlePath = exportIssueBundle(collectDashboardSnapshot());
|
|
5065
|
+
const bundlePath = exportIssueBundle(await collectDashboardSnapshot());
|
|
4760
5066
|
actionResult = { bundlePath };
|
|
4761
5067
|
runtimeState.lastAction.bundlePath = bundlePath;
|
|
4762
5068
|
} else if (action === 'rescan-repos') {
|
|
4763
5069
|
recordDiscoverySummary(discoverAccountRepos());
|
|
5070
|
+
supportTicketSnapshotCache = { fetchedAtMs: 0, value: null };
|
|
5071
|
+
await bestEffortRegisterCliListeners({ reason: 'dashboard.rescan' });
|
|
4764
5072
|
} else if (action === 'open-tmux') {
|
|
4765
5073
|
openTmuxSessionForUser();
|
|
4766
5074
|
} else if (action === 'start-tmux') {
|
|
@@ -4777,7 +5085,7 @@ function startDashboardServer(preferredPort) {
|
|
|
4777
5085
|
success: true,
|
|
4778
5086
|
action,
|
|
4779
5087
|
result: actionResult,
|
|
4780
|
-
state: collectDashboardSnapshot()
|
|
5088
|
+
state: await collectDashboardSnapshot()
|
|
4781
5089
|
}));
|
|
4782
5090
|
return;
|
|
4783
5091
|
}
|
|
@@ -4793,9 +5101,10 @@ function startDashboardServer(preferredPort) {
|
|
|
4793
5101
|
|
|
4794
5102
|
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
|
4795
5103
|
try {
|
|
5104
|
+
const snapshot = await collectDashboardSnapshot();
|
|
4796
5105
|
res.statusCode = 200;
|
|
4797
5106
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
4798
|
-
res.end(renderDashboardHtml(
|
|
5107
|
+
res.end(renderDashboardHtml(snapshot));
|
|
4799
5108
|
} catch (err) {
|
|
4800
5109
|
sendDashboardError(res, err, false);
|
|
4801
5110
|
}
|
|
@@ -5324,14 +5633,15 @@ function processInboundWebsocketMessage(client, baseUrl, topic, accountIds, user
|
|
|
5324
5633
|
const acctId = payload.accountId || accountIds[0];
|
|
5325
5634
|
console.log('[ws][TestSuite][account:' + acctId + ']', JSON.stringify(payload));
|
|
5326
5635
|
}
|
|
5327
|
-
} else if (data.type === 'remits-cli') {
|
|
5636
|
+
} else if (data.type === 'remits-cli' || data.type === 'cli_listener' || data.type === 'listener') {
|
|
5328
5637
|
const payload = data.payload || data;
|
|
5329
5638
|
const dispatchAccountId = resolveDispatchAccountId(payload, accountIds[0]);
|
|
5330
|
-
console.log('[ws][
|
|
5331
|
-
appendGlobalActivityLog('tmux', '
|
|
5639
|
+
console.log('[ws][listener][account:' + dispatchAccountId + ']', JSON.stringify(payload));
|
|
5640
|
+
appendGlobalActivityLog('tmux', 'listener_delivery.received', {
|
|
5332
5641
|
baseUrl,
|
|
5333
5642
|
topic,
|
|
5334
5643
|
accountId: dispatchAccountId,
|
|
5644
|
+
messageType: data.type,
|
|
5335
5645
|
source,
|
|
5336
5646
|
messageId,
|
|
5337
5647
|
message: summarizeDispatchMessage(payload)
|
|
@@ -5733,6 +6043,8 @@ async function listenCommand(flags) {
|
|
|
5733
6043
|
recordDiscoverySummary(discoverAccountRepos());
|
|
5734
6044
|
const dashboardPort = Number(flags.port || flags.dashboardPort || DEFAULT_DASHBOARD_PORT);
|
|
5735
6045
|
await startDashboardServer(dashboardPort);
|
|
6046
|
+
await bestEffortRegisterCliListeners({ reason: 'listener.start' });
|
|
6047
|
+
let lastListenerRegistrationAt = Date.now();
|
|
5736
6048
|
|
|
5737
6049
|
// Create the tmux session immediately so the user can attach right away
|
|
5738
6050
|
try {
|
|
@@ -5820,6 +6132,11 @@ async function listenCommand(flags) {
|
|
|
5820
6132
|
activeSessionSignature = currentSignature;
|
|
5821
6133
|
}
|
|
5822
6134
|
|
|
6135
|
+
if (Date.now() - lastListenerRegistrationAt >= LISTENER_REGISTRATION_INTERVAL_MS) {
|
|
6136
|
+
await bestEffortRegisterCliListeners({ reason: 'listener.heartbeat' });
|
|
6137
|
+
lastListenerRegistrationAt = Date.now();
|
|
6138
|
+
}
|
|
6139
|
+
|
|
5823
6140
|
await runManagedWebsocketWatchdog();
|
|
5824
6141
|
updateRuntimeServiceState();
|
|
5825
6142
|
} catch (err) {
|
|
@@ -6213,6 +6530,9 @@ async function main() {
|
|
|
6213
6530
|
|
|
6214
6531
|
// Best-effort: update account-repo index whenever running from inside a remits repo
|
|
6215
6532
|
try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
|
|
6533
|
+
if (!wantsJson) {
|
|
6534
|
+
try { await bestEffortRegisterCliListeners({ cwd: process.cwd(), reason: 'command' }); } catch (_) { /* best-effort */ }
|
|
6535
|
+
}
|
|
6216
6536
|
|
|
6217
6537
|
// Auto-start the background service if not already running.
|
|
6218
6538
|
// Skip for lifecycle subcommands and help.
|