@remits/remits-cli 0.1.106 → 0.1.108
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 +378 -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,204 @@ 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
|
+
const groupedSessions = Array.isArray(options.groupedSessions)
|
|
1333
|
+
? options.groupedSessions.filter((entry) => entry && entry.token)
|
|
1334
|
+
: [];
|
|
1335
|
+
const sessionDataModes = Array.from(new Set(
|
|
1336
|
+
(groupedSessions.length ? groupedSessions : [session])
|
|
1337
|
+
.map((entry) => normalizeDataMode(entry && entry.dataMode))
|
|
1338
|
+
.filter(Boolean)
|
|
1339
|
+
)).sort();
|
|
1340
|
+
const sessionAccountIds = Array.from(new Set(
|
|
1341
|
+
(groupedSessions.length ? groupedSessions : [session])
|
|
1342
|
+
.map((entry) => Number(entry && entry.accountId))
|
|
1343
|
+
.filter((accountId) => Number.isFinite(accountId) && accountId > 0)
|
|
1344
|
+
)).sort((a, b) => a - b);
|
|
1345
|
+
return {
|
|
1346
|
+
client: 'remits-cli',
|
|
1347
|
+
reason: options.reason || 'heartbeat',
|
|
1348
|
+
baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
|
|
1349
|
+
accountId: Number(session.accountId),
|
|
1350
|
+
dataMode: normalizeDataMode(session.dataMode),
|
|
1351
|
+
dataModes: sessionDataModes,
|
|
1352
|
+
sessionAccountIds,
|
|
1353
|
+
registrationSessionCount: groupedSessions.length || 1,
|
|
1354
|
+
websocketTopic: session.websocketTopic || (session.user && session.user.uuid ? String(session.user.uuid).replace(/-/g, '') : null),
|
|
1355
|
+
dashboardUrl: service.dashboardUrl || null,
|
|
1356
|
+
agent: getPreferredAgent(),
|
|
1357
|
+
service,
|
|
1358
|
+
repoAccounts: listenerRegistrationRepoAccounts()
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function listenerRegistrationSignature(payload) {
|
|
1363
|
+
return sha256(stableStringify({
|
|
1364
|
+
baseUrl: payload.baseUrl,
|
|
1365
|
+
accountId: payload.accountId,
|
|
1366
|
+
dataMode: payload.dataMode,
|
|
1367
|
+
dataModes: payload.dataModes || [],
|
|
1368
|
+
sessionAccountIds: payload.sessionAccountIds || [],
|
|
1369
|
+
websocketTopic: payload.websocketTopic,
|
|
1370
|
+
dashboardUrl: payload.dashboardUrl,
|
|
1371
|
+
agent: payload.agent,
|
|
1372
|
+
serviceRunning: payload.service && payload.service.running,
|
|
1373
|
+
repoAccounts: (payload.repoAccounts || []).map((entry) => ({
|
|
1374
|
+
accountId: entry.accountId,
|
|
1375
|
+
type: entry.type,
|
|
1376
|
+
routingScope: entry.routingScope,
|
|
1377
|
+
updatedAt: entry.updatedAt
|
|
1378
|
+
}))
|
|
1379
|
+
}));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function shouldRegisterCliListener(session, payload, options = {}) {
|
|
1383
|
+
if (options.force === true) {
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
const state = readListenerRegistrationState();
|
|
1387
|
+
const key = listenerRegistrationStateKey(session);
|
|
1388
|
+
const existing = state[key];
|
|
1389
|
+
if (!existing) {
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
const lastRegisteredAtMs = Number(existing.lastRegisteredAtMs || 0);
|
|
1393
|
+
if (!lastRegisteredAtMs || Date.now() - lastRegisteredAtMs >= LISTENER_REGISTRATION_MIN_INTERVAL_MS) {
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
return existing.signature !== listenerRegistrationSignature(payload);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function rememberCliListenerRegistration(session, payload) {
|
|
1400
|
+
const state = readListenerRegistrationState();
|
|
1401
|
+
const key = listenerRegistrationStateKey(session);
|
|
1402
|
+
state[key] = {
|
|
1403
|
+
baseUrl: payload.baseUrl,
|
|
1404
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
1405
|
+
signature: listenerRegistrationSignature(payload),
|
|
1406
|
+
accountCount: Array.isArray(payload.repoAccounts) ? payload.repoAccounts.length : 0,
|
|
1407
|
+
sessionAccountIds: payload.sessionAccountIds || [],
|
|
1408
|
+
dataModes: payload.dataModes || [],
|
|
1409
|
+
registrationSessionCount: payload.registrationSessionCount || 1,
|
|
1410
|
+
lastRegisteredAt: new Date().toISOString(),
|
|
1411
|
+
lastRegisteredAtMs: Date.now()
|
|
1412
|
+
};
|
|
1413
|
+
writeListenerRegistrationState(state);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
async function registerCliListenerSession(session, options = {}) {
|
|
1417
|
+
if (!session || !session.token) {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
|
|
1421
|
+
const payload = listenerRegistrationPayload(session, options);
|
|
1422
|
+
if (!shouldRegisterCliListener(session, payload, options)) {
|
|
1423
|
+
return { success: true, skipped: true, reason: 'recently_registered' };
|
|
1424
|
+
}
|
|
1425
|
+
const api = buildAxios(baseUrl, session.token, parsePositiveInt(options.timeoutMs, 5000));
|
|
1426
|
+
const response = await api.post('/cli/register', payload);
|
|
1427
|
+
rememberCliListenerRegistration(session, payload);
|
|
1428
|
+
return response.data;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function bestEffortRegisterCliListeners(options = {}) {
|
|
1432
|
+
const sessions = listSessionsFromStore(readAllSessions()).filter((session) => session && session.token);
|
|
1433
|
+
if (!sessions.length) {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
const groupedSessions = [];
|
|
1437
|
+
for (const session of sessions) {
|
|
1438
|
+
const key = listenerRegistrationStateKey(session);
|
|
1439
|
+
let group = groupedSessions.find((entry) => entry.key === key);
|
|
1440
|
+
if (!group) {
|
|
1441
|
+
group = { key, session, sessions: [] };
|
|
1442
|
+
groupedSessions.push(group);
|
|
1443
|
+
}
|
|
1444
|
+
group.sessions.push(session);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const results = await Promise.all(groupedSessions.map(async (group) => {
|
|
1448
|
+
try {
|
|
1449
|
+
const data = await registerCliListenerSession(group.session, {
|
|
1450
|
+
...options,
|
|
1451
|
+
groupedSessions: group.sessions
|
|
1452
|
+
});
|
|
1453
|
+
return {
|
|
1454
|
+
success: true,
|
|
1455
|
+
accountId: group.session.accountId,
|
|
1456
|
+
baseUrl: group.session.baseUrl,
|
|
1457
|
+
groupedAccountIds: group.sessions.map((session) => session.accountId),
|
|
1458
|
+
data
|
|
1459
|
+
};
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
appendGlobalActivityLog('listener', 'registration.failed', {
|
|
1462
|
+
accountId: group.session.accountId,
|
|
1463
|
+
baseUrl: normalizeBaseUrl(group.session.baseUrl || DEFAULT_BASE_URL),
|
|
1464
|
+
groupedAccountIds: group.sessions.map((session) => session.accountId),
|
|
1465
|
+
error: describeError(err)
|
|
1466
|
+
}, 'error');
|
|
1467
|
+
return {
|
|
1468
|
+
success: false,
|
|
1469
|
+
accountId: group.session.accountId,
|
|
1470
|
+
baseUrl: group.session.baseUrl,
|
|
1471
|
+
groupedAccountIds: group.sessions.map((session) => session.accountId),
|
|
1472
|
+
error: describeError(err)
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
}));
|
|
1476
|
+
return results;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1276
1479
|
function resolvePreferredAccountId(cwd, flags, session) {
|
|
1277
1480
|
// Explicit --account-id wins over the repo's account-info.json so users can
|
|
1278
1481
|
// target a child account (e.g. a customer) from a parent-account repo.
|
|
@@ -1987,6 +2190,9 @@ async function authCommand(flags) {
|
|
|
1987
2190
|
});
|
|
1988
2191
|
console.log('Authenticated as user', session.user ? session.user.id : 'unknown', 'for account', session.accountId);
|
|
1989
2192
|
console.log('Data mode:', session.dataMode);
|
|
2193
|
+
try {
|
|
2194
|
+
await bestEffortRegisterCliListeners({ cwd, reason: 'auth' });
|
|
2195
|
+
} catch (_) { /* best-effort */ }
|
|
1990
2196
|
|
|
1991
2197
|
try {
|
|
1992
2198
|
const branchName = currentBranch(cwd);
|
|
@@ -4027,6 +4233,10 @@ let runtimeState = {
|
|
|
4027
4233
|
websocket: {},
|
|
4028
4234
|
lastAction: null
|
|
4029
4235
|
};
|
|
4236
|
+
let supportTicketSnapshotCache = {
|
|
4237
|
+
fetchedAtMs: 0,
|
|
4238
|
+
value: null
|
|
4239
|
+
};
|
|
4030
4240
|
|
|
4031
4241
|
function listenerSessionEntriesFromStore(store) {
|
|
4032
4242
|
return listSessionsFromStore(store).filter((session) => session && session.token && session.websocketTopic);
|
|
@@ -4414,6 +4624,7 @@ function exportIssueBundle(snapshot) {
|
|
|
4414
4624
|
copyFileIfExists(SESSIONS_FILE, path.join(globalDir, 'sessions.json'));
|
|
4415
4625
|
copyFileIfExists(CONFIG_FILE, path.join(globalDir, 'config.json'));
|
|
4416
4626
|
copyFileIfExists(SERVICE_STATE_FILE, path.join(globalDir, 'service-state.json'));
|
|
4627
|
+
copyFileIfExists(LISTENER_REGISTRATION_STATE_FILE, path.join(globalDir, 'listener-registration-state.json'));
|
|
4417
4628
|
copyFileIfExists(ACCOUNT_REPO_INDEX_FILE, path.join(globalDir, 'account-repos.json'));
|
|
4418
4629
|
copyFileIfExists(DISPATCH_STATE_FILE, path.join(globalDir, 'dispatch-panes.json'));
|
|
4419
4630
|
copyFileIfExists(TMUX_ACTIVITY_LOG_FILE, path.join(globalDir, 'tmux-activity.log'));
|
|
@@ -4455,6 +4666,7 @@ function buildGlobalFileSnapshot() {
|
|
|
4455
4666
|
{ label: 'config.json', path: CONFIG_FILE, mode: 'json' },
|
|
4456
4667
|
{ label: 'service-state.json', path: SERVICE_STATE_FILE, mode: 'json' },
|
|
4457
4668
|
{ label: 'websocket-state.json', path: WEBSOCKET_STATE_FILE, mode: 'json' },
|
|
4669
|
+
{ label: 'listener-registration-state.json', path: LISTENER_REGISTRATION_STATE_FILE, mode: 'json' },
|
|
4458
4670
|
{ label: 'dispatch-panes.json', path: DISPATCH_STATE_FILE, mode: 'json' },
|
|
4459
4671
|
{ label: 'account-repos.json', path: ACCOUNT_REPO_INDEX_FILE, mode: 'json' },
|
|
4460
4672
|
{ label: 'tmux-activity.log (tail)', path: TMUX_ACTIVITY_LOG_FILE, mode: 'tail' }
|
|
@@ -4511,7 +4723,140 @@ function buildRepoSnapshot(entry) {
|
|
|
4511
4723
|
};
|
|
4512
4724
|
}
|
|
4513
4725
|
|
|
4514
|
-
function
|
|
4726
|
+
function compactSupportTicketRow(row) {
|
|
4727
|
+
if (!row || typeof row !== 'object') {
|
|
4728
|
+
return null;
|
|
4729
|
+
}
|
|
4730
|
+
return {
|
|
4731
|
+
ticketId: row.ticketId || row.id || null,
|
|
4732
|
+
accountId: row.accountId || row.account_id || null,
|
|
4733
|
+
accountName: row.accountName || null,
|
|
4734
|
+
subject: row.subject || null,
|
|
4735
|
+
priority: row.priority || null,
|
|
4736
|
+
status: row.status || null,
|
|
4737
|
+
type: row.type || null,
|
|
4738
|
+
source: row.source || null,
|
|
4739
|
+
assignedTo: row.assignedTo || null,
|
|
4740
|
+
implementationAccountId: row.implementationAccountId || null,
|
|
4741
|
+
implementationAccountName: row.implementationAccountName || null,
|
|
4742
|
+
affectedComponent: row.affectedComponent || null,
|
|
4743
|
+
updatedAt: row.updatedAt || row.updated || row.lastActivityAt || null,
|
|
4744
|
+
activityAt: row.activityAt || row.lastActivityAt || null,
|
|
4745
|
+
activityLabel: row.activityLabel || null
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
function supportTicketQueueInput(session) {
|
|
4750
|
+
return {
|
|
4751
|
+
action: 'list',
|
|
4752
|
+
accountId: Number(session.accountId),
|
|
4753
|
+
includeChildren: true,
|
|
4754
|
+
includeRoot: true,
|
|
4755
|
+
sortBy: 'triage',
|
|
4756
|
+
limit: Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
async function fetchSupportTicketQueueForSession(session) {
|
|
4761
|
+
const baseUrl = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL);
|
|
4762
|
+
const api = buildAxios(baseUrl, session.token, 12000);
|
|
4763
|
+
const response = await api.post('/cli/tool', {
|
|
4764
|
+
token: session.token,
|
|
4765
|
+
accountId: Number(session.accountId),
|
|
4766
|
+
branchName: '',
|
|
4767
|
+
dataMode: 'prod',
|
|
4768
|
+
name: 'mcp_support_ticket_queue',
|
|
4769
|
+
callId: 'dashboard-support-ticket-queue-' + crypto.randomUUID(),
|
|
4770
|
+
input: supportTicketQueueInput(session)
|
|
4771
|
+
}).then((r) => r.data);
|
|
4772
|
+
|
|
4773
|
+
const result = response && response.result ? response.result : {};
|
|
4774
|
+
const tickets = Array.isArray(result.tickets)
|
|
4775
|
+
? result.tickets.map(compactSupportTicketRow).filter(Boolean)
|
|
4776
|
+
: [];
|
|
4777
|
+
return {
|
|
4778
|
+
success: response && response.success !== false && response.toolSuccess !== false && result.success !== false,
|
|
4779
|
+
baseUrl,
|
|
4780
|
+
accountId: Number(session.accountId),
|
|
4781
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
4782
|
+
dataMode: result.dataMode || response.dataMode || 'prod',
|
|
4783
|
+
totalCount: result.totalCount || tickets.length,
|
|
4784
|
+
count: result.count || tickets.length,
|
|
4785
|
+
counts: result.counts || {},
|
|
4786
|
+
recommendedTicket: compactSupportTicketRow(result.recommendedTicket),
|
|
4787
|
+
tickets,
|
|
4788
|
+
searchedAccounts: result.searchedAccounts || [],
|
|
4789
|
+
message: response.toolMessage || result.message || null
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
async function collectSupportTicketSnapshot() {
|
|
4794
|
+
const nowMs = Date.now();
|
|
4795
|
+
if (supportTicketSnapshotCache.value &&
|
|
4796
|
+
nowMs - supportTicketSnapshotCache.fetchedAtMs < SUPPORT_TICKET_DASHBOARD_REFRESH_MS) {
|
|
4797
|
+
return supportTicketSnapshotCache.value;
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
const sessions = listSessionsFromStore(readAllSessions())
|
|
4801
|
+
.filter((session) => session && session.token && session.accountId);
|
|
4802
|
+
const seen = new Set();
|
|
4803
|
+
const queueSessions = sessions.filter((session) => {
|
|
4804
|
+
const key = normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL) + '|' + String(session.accountId) + '|' +
|
|
4805
|
+
String(session.user && session.user.id ? session.user.id : '');
|
|
4806
|
+
if (seen.has(key)) {
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
seen.add(key);
|
|
4810
|
+
return true;
|
|
4811
|
+
}).slice(0, 12);
|
|
4812
|
+
|
|
4813
|
+
const queues = [];
|
|
4814
|
+
const errors = [];
|
|
4815
|
+
await Promise.all(queueSessions.map(async (session) => {
|
|
4816
|
+
try {
|
|
4817
|
+
queues.push(await fetchSupportTicketQueueForSession(session));
|
|
4818
|
+
} catch (err) {
|
|
4819
|
+
errors.push({
|
|
4820
|
+
baseUrl: normalizeBaseUrl(session.baseUrl || DEFAULT_BASE_URL),
|
|
4821
|
+
accountId: Number(session.accountId),
|
|
4822
|
+
userId: session.user && session.user.id ? session.user.id : null,
|
|
4823
|
+
message: describeError(err)
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
}));
|
|
4827
|
+
|
|
4828
|
+
const ticketsById = {};
|
|
4829
|
+
for (const queue of queues) {
|
|
4830
|
+
for (const ticket of queue.tickets || []) {
|
|
4831
|
+
const key = String(ticket.ticketId || '');
|
|
4832
|
+
if (!key || ticketsById[key]) {
|
|
4833
|
+
continue;
|
|
4834
|
+
}
|
|
4835
|
+
ticketsById[key] = {
|
|
4836
|
+
...ticket,
|
|
4837
|
+
queueAccountId: queue.accountId,
|
|
4838
|
+
baseUrl: queue.baseUrl
|
|
4839
|
+
};
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
const value = {
|
|
4844
|
+
fetchedAt: new Date(nowMs).toISOString(),
|
|
4845
|
+
refreshMs: SUPPORT_TICKET_DASHBOARD_REFRESH_MS,
|
|
4846
|
+
queueCount: queues.length,
|
|
4847
|
+
totalCount: queues.reduce((sum, queue) => sum + Number(queue.totalCount || 0), 0),
|
|
4848
|
+
tickets: Object.values(ticketsById).slice(0, Math.min(Math.max(parsePositiveInt(SUPPORT_TICKET_DASHBOARD_LIMIT, 20), 1), 100)),
|
|
4849
|
+
queues,
|
|
4850
|
+
errors
|
|
4851
|
+
};
|
|
4852
|
+
supportTicketSnapshotCache = {
|
|
4853
|
+
fetchedAtMs: nowMs,
|
|
4854
|
+
value
|
|
4855
|
+
};
|
|
4856
|
+
return value;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
async function collectDashboardSnapshot() {
|
|
4515
4860
|
const accountRepoIndex = readAccountRepoIndex();
|
|
4516
4861
|
const repoEntries = listAccountRepoEntries(accountRepoIndex)
|
|
4517
4862
|
.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')))
|
|
@@ -4566,6 +4911,7 @@ function collectDashboardSnapshot() {
|
|
|
4566
4911
|
globalFiles,
|
|
4567
4912
|
repos: repoEntries,
|
|
4568
4913
|
platformRepo,
|
|
4914
|
+
supportTickets: await collectSupportTicketSnapshot(),
|
|
4569
4915
|
issues,
|
|
4570
4916
|
lastAction: runtimeState.lastAction
|
|
4571
4917
|
};
|
|
@@ -4621,6 +4967,9 @@ function renderDashboardHtml(snapshot) {
|
|
|
4621
4967
|
'<div id="last-action" class="empty">No dashboard actions have been triggered yet.</div>' +
|
|
4622
4968
|
'</section>' +
|
|
4623
4969
|
'</div>' +
|
|
4970
|
+
'<div class="section-grid">' +
|
|
4971
|
+
'<section class="panel"><h2>Support Tickets</h2><div id="support-ticket-section" class="accordion"></div></section>' +
|
|
4972
|
+
'</div>' +
|
|
4624
4973
|
'<div class="section-grid">' +
|
|
4625
4974
|
'<section class="panel"><h2>Issues & Escalation</h2><div id="issues-section" class="accordion"></div></section>' +
|
|
4626
4975
|
'</div>' +
|
|
@@ -4651,9 +5000,10 @@ function renderDashboardHtml(snapshot) {
|
|
|
4651
5000
|
'function summaryItem(key,value){return \'<div class="summary-item"><span class="k">\'+escapeHtml(key)+\'</span><span class="v">\'+escapeHtml(value)+\'</span></div>\';}' +
|
|
4652
5001
|
'function codeBlock(key,content,extraClass){return \'<pre class="code-block \'+(extraClass||"")+\'" data-scroll-key="\'+escapeHtml(key)+\'">\'+escapeHtml(content||"[missing]")+\'</pre>\';}' +
|
|
4653
5002
|
'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=[' +
|
|
5003
|
+
'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
5004
|
'{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
5005
|
'{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")},' +
|
|
5006
|
+
'{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
5007
|
'{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
5008
|
'{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
5009
|
'{title:"Repos",value:String(repoCount),label:"Indexed account repos",tone:repoCount?"ok":"warn",sub:"Last scan: "+formatTime(state.discovery&&state.discovery.scannedAt)}' +
|
|
@@ -4664,15 +5014,18 @@ function renderDashboardHtml(snapshot) {
|
|
|
4664
5014
|
'summaryItem("Repo Matches",String(state.discovery&&state.discovery.matches||0)),' +
|
|
4665
5015
|
'summaryItem("Connections",String(ws.length)),' +
|
|
4666
5016
|
'summaryItem("Topics",String(topics)),' +
|
|
5017
|
+
'summaryItem("Open Tickets",String(state.supportTickets&&state.supportTickets.totalCount||0)),' +
|
|
4667
5018
|
'summaryItem("Tracked Panes",String(tracked))' +
|
|
4668
5019
|
'].join("");}' +
|
|
4669
5020
|
'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
5021
|
'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("");}' +
|
|
5022
|
+
'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;}' +
|
|
5023
|
+
'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
5024
|
'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
5025
|
'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
5026
|
'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
5027
|
'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);}' +
|
|
5028
|
+
'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
5029
|
'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
5030
|
'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
5031
|
'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 +5084,10 @@ function startDashboardServer(preferredPort) {
|
|
|
4731
5084
|
const requestUrl = new URL(req.url, 'http://' + DASHBOARD_HOST + ':' + (runtimeState.service.dashboardPort || preferredPort));
|
|
4732
5085
|
if (req.method === 'GET' && requestUrl.pathname === '/api/state') {
|
|
4733
5086
|
try {
|
|
5087
|
+
const snapshot = await collectDashboardSnapshot();
|
|
4734
5088
|
res.statusCode = 200;
|
|
4735
5089
|
res.setHeader('Content-Type', 'application/json');
|
|
4736
|
-
res.end(JSON.stringify(
|
|
5090
|
+
res.end(JSON.stringify(snapshot, null, 2));
|
|
4737
5091
|
} catch (err) {
|
|
4738
5092
|
sendDashboardError(res, err, true);
|
|
4739
5093
|
}
|
|
@@ -4756,11 +5110,13 @@ function startDashboardServer(preferredPort) {
|
|
|
4756
5110
|
at: new Date().toISOString()
|
|
4757
5111
|
};
|
|
4758
5112
|
if (action === 'export-issues') {
|
|
4759
|
-
const bundlePath = exportIssueBundle(collectDashboardSnapshot());
|
|
5113
|
+
const bundlePath = exportIssueBundle(await collectDashboardSnapshot());
|
|
4760
5114
|
actionResult = { bundlePath };
|
|
4761
5115
|
runtimeState.lastAction.bundlePath = bundlePath;
|
|
4762
5116
|
} else if (action === 'rescan-repos') {
|
|
4763
5117
|
recordDiscoverySummary(discoverAccountRepos());
|
|
5118
|
+
supportTicketSnapshotCache = { fetchedAtMs: 0, value: null };
|
|
5119
|
+
await bestEffortRegisterCliListeners({ reason: 'dashboard.rescan' });
|
|
4764
5120
|
} else if (action === 'open-tmux') {
|
|
4765
5121
|
openTmuxSessionForUser();
|
|
4766
5122
|
} else if (action === 'start-tmux') {
|
|
@@ -4777,7 +5133,7 @@ function startDashboardServer(preferredPort) {
|
|
|
4777
5133
|
success: true,
|
|
4778
5134
|
action,
|
|
4779
5135
|
result: actionResult,
|
|
4780
|
-
state: collectDashboardSnapshot()
|
|
5136
|
+
state: await collectDashboardSnapshot()
|
|
4781
5137
|
}));
|
|
4782
5138
|
return;
|
|
4783
5139
|
}
|
|
@@ -4793,9 +5149,10 @@ function startDashboardServer(preferredPort) {
|
|
|
4793
5149
|
|
|
4794
5150
|
if (req.method === 'GET' && requestUrl.pathname === '/') {
|
|
4795
5151
|
try {
|
|
5152
|
+
const snapshot = await collectDashboardSnapshot();
|
|
4796
5153
|
res.statusCode = 200;
|
|
4797
5154
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
4798
|
-
res.end(renderDashboardHtml(
|
|
5155
|
+
res.end(renderDashboardHtml(snapshot));
|
|
4799
5156
|
} catch (err) {
|
|
4800
5157
|
sendDashboardError(res, err, false);
|
|
4801
5158
|
}
|
|
@@ -5324,14 +5681,15 @@ function processInboundWebsocketMessage(client, baseUrl, topic, accountIds, user
|
|
|
5324
5681
|
const acctId = payload.accountId || accountIds[0];
|
|
5325
5682
|
console.log('[ws][TestSuite][account:' + acctId + ']', JSON.stringify(payload));
|
|
5326
5683
|
}
|
|
5327
|
-
} else if (data.type === 'remits-cli') {
|
|
5684
|
+
} else if (data.type === 'remits-cli' || data.type === 'cli_listener' || data.type === 'listener') {
|
|
5328
5685
|
const payload = data.payload || data;
|
|
5329
5686
|
const dispatchAccountId = resolveDispatchAccountId(payload, accountIds[0]);
|
|
5330
|
-
console.log('[ws][
|
|
5331
|
-
appendGlobalActivityLog('tmux', '
|
|
5687
|
+
console.log('[ws][listener][account:' + dispatchAccountId + ']', JSON.stringify(payload));
|
|
5688
|
+
appendGlobalActivityLog('tmux', 'listener_delivery.received', {
|
|
5332
5689
|
baseUrl,
|
|
5333
5690
|
topic,
|
|
5334
5691
|
accountId: dispatchAccountId,
|
|
5692
|
+
messageType: data.type,
|
|
5335
5693
|
source,
|
|
5336
5694
|
messageId,
|
|
5337
5695
|
message: summarizeDispatchMessage(payload)
|
|
@@ -5733,6 +6091,8 @@ async function listenCommand(flags) {
|
|
|
5733
6091
|
recordDiscoverySummary(discoverAccountRepos());
|
|
5734
6092
|
const dashboardPort = Number(flags.port || flags.dashboardPort || DEFAULT_DASHBOARD_PORT);
|
|
5735
6093
|
await startDashboardServer(dashboardPort);
|
|
6094
|
+
await bestEffortRegisterCliListeners({ reason: 'listener.start' });
|
|
6095
|
+
let lastListenerRegistrationAt = Date.now();
|
|
5736
6096
|
|
|
5737
6097
|
// Create the tmux session immediately so the user can attach right away
|
|
5738
6098
|
try {
|
|
@@ -5820,6 +6180,11 @@ async function listenCommand(flags) {
|
|
|
5820
6180
|
activeSessionSignature = currentSignature;
|
|
5821
6181
|
}
|
|
5822
6182
|
|
|
6183
|
+
if (Date.now() - lastListenerRegistrationAt >= LISTENER_REGISTRATION_INTERVAL_MS) {
|
|
6184
|
+
await bestEffortRegisterCliListeners({ reason: 'listener.heartbeat' });
|
|
6185
|
+
lastListenerRegistrationAt = Date.now();
|
|
6186
|
+
}
|
|
6187
|
+
|
|
5823
6188
|
await runManagedWebsocketWatchdog();
|
|
5824
6189
|
updateRuntimeServiceState();
|
|
5825
6190
|
} catch (err) {
|
|
@@ -6213,6 +6578,9 @@ async function main() {
|
|
|
6213
6578
|
|
|
6214
6579
|
// Best-effort: update account-repo index whenever running from inside a remits repo
|
|
6215
6580
|
try { updateAccountRepoIndex(process.cwd()); } catch (_) { /* not in account repo */ }
|
|
6581
|
+
if (!wantsJson) {
|
|
6582
|
+
try { await bestEffortRegisterCliListeners({ cwd: process.cwd(), reason: 'command' }); } catch (_) { /* best-effort */ }
|
|
6583
|
+
}
|
|
6216
6584
|
|
|
6217
6585
|
// Auto-start the background service if not already running.
|
|
6218
6586
|
// Skip for lifecycle subcommands and help.
|