a2acalling 0.6.56 → 0.6.58
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/.a2a-manifest.json +47 -0
- package/.claude/a2a-skill-reference.md +462 -0
- package/bin/cli.js +48 -61
- package/docs/plans/2026-02-18-dashboard-backlog-blitz.md +585 -0
- package/native/macos/src-tauri/src/lib.rs +4 -4
- package/package.json +1 -1
- package/scripts/cleanup.js +251 -0
- package/scripts/preuninstall.js +68 -0
- package/src/dashboard/public/app.js +296 -180
- package/src/dashboard/public/index.html +201 -209
- package/src/dashboard/public/style.css +76 -104
|
@@ -214,13 +214,13 @@ function formatUpdaterState(stateValue) {
|
|
|
214
214
|
return state.replaceAll('_', ' ');
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function
|
|
217
|
+
function badgeVariant(stateValue) {
|
|
218
218
|
const state = String(stateValue || '').trim();
|
|
219
|
-
if (state === 'failed') return '
|
|
219
|
+
if (state === 'failed') return 'danger';
|
|
220
220
|
if (state === 'waiting_for_safe_restart' || state === 'checking' || state === 'downloading' || state === 'applying' || state === 'restarting') {
|
|
221
|
-
return '
|
|
221
|
+
return 'warning';
|
|
222
222
|
}
|
|
223
|
-
return '
|
|
223
|
+
return 'success';
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
async function copyText(value) {
|
|
@@ -250,36 +250,33 @@ async function copyText(value) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
function bindTabs() {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
if (!target) return false;
|
|
256
|
-
const btn = Array.from(document.querySelectorAll('.tab')).find(b => b.dataset.tab === target);
|
|
257
|
-
const panel = document.getElementById(`tab-${target}`);
|
|
258
|
-
if (!btn || !panel) return false;
|
|
259
|
-
|
|
260
|
-
document.querySelectorAll('.tab').forEach(b => b.classList.remove('is-active'));
|
|
261
|
-
document.querySelectorAll('.panel').forEach(p => p.classList.remove('is-active'));
|
|
262
|
-
btn.classList.add('is-active');
|
|
263
|
-
panel.classList.add('is-active');
|
|
264
|
-
|
|
265
|
-
if (options.updateHash) {
|
|
266
|
-
try { window.location.hash = target; } catch (err) {}
|
|
267
|
-
}
|
|
268
|
-
return true;
|
|
269
|
-
};
|
|
253
|
+
const tabGroup = document.getElementById('main-tabs');
|
|
254
|
+
if (!tabGroup) return;
|
|
270
255
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
256
|
+
tabGroup.addEventListener('sl-tab-show', (e) => {
|
|
257
|
+
const tabName = e.detail.name;
|
|
258
|
+
try { window.location.hash = tabName; } catch (err) {}
|
|
259
|
+
if (typeof onTabSwitch === 'function') onTabSwitch(tabName);
|
|
275
260
|
});
|
|
276
261
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
262
|
+
// Deep-link support: activate the tab matching the URL hash
|
|
263
|
+
const activateFromHash = () => {
|
|
264
|
+
const hash = window.location.hash.slice(1);
|
|
265
|
+
if (hash) {
|
|
266
|
+
// Use try/catch in case the tab group isn't fully ready
|
|
267
|
+
try { tabGroup.show(hash); } catch (err) {}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
window.addEventListener('hashchange', activateFromHash);
|
|
280
272
|
|
|
281
|
-
//
|
|
282
|
-
|
|
273
|
+
// On initial load, activate from hash (wait for Shoelace to be ready)
|
|
274
|
+
if (tabGroup.updateComplete) {
|
|
275
|
+
tabGroup.updateComplete.then(activateFromHash);
|
|
276
|
+
} else {
|
|
277
|
+
// Fallback: try after a short delay
|
|
278
|
+
setTimeout(activateFromHash, 100);
|
|
279
|
+
}
|
|
283
280
|
}
|
|
284
281
|
|
|
285
282
|
function norm(value) {
|
|
@@ -307,6 +304,35 @@ function contactLabel(contact) {
|
|
|
307
304
|
return String(contact?.name || '').trim() || String(contact?.host || '').trim() || '-';
|
|
308
305
|
}
|
|
309
306
|
|
|
307
|
+
function getPinnedContacts() {
|
|
308
|
+
try {
|
|
309
|
+
const raw = localStorage.getItem('a2a-pinned-contacts');
|
|
310
|
+
if (!raw) return [];
|
|
311
|
+
const parsed = JSON.parse(raw);
|
|
312
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function togglePin(contactId) {
|
|
319
|
+
const id = String(contactId || '');
|
|
320
|
+
if (!id) return;
|
|
321
|
+
const pinned = getPinnedContacts();
|
|
322
|
+
const index = pinned.indexOf(id);
|
|
323
|
+
if (index >= 0) {
|
|
324
|
+
pinned.splice(index, 1);
|
|
325
|
+
} else {
|
|
326
|
+
pinned.push(id);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
localStorage.setItem('a2a-pinned-contacts', JSON.stringify(pinned));
|
|
330
|
+
} catch (err) {
|
|
331
|
+
// localStorage may be unavailable
|
|
332
|
+
}
|
|
333
|
+
renderContacts();
|
|
334
|
+
}
|
|
335
|
+
|
|
310
336
|
function renderContacts() {
|
|
311
337
|
const el = document.getElementById('contacts-sections');
|
|
312
338
|
if (!el) return;
|
|
@@ -318,30 +344,18 @@ function renderContacts() {
|
|
|
318
344
|
.filter(c => isMine(c))
|
|
319
345
|
.sort((a, b) => contactLabel(a).localeCompare(contactLabel(b)));
|
|
320
346
|
|
|
347
|
+
const pinnedIds = getPinnedContacts();
|
|
321
348
|
const lastCalled = contacts
|
|
322
|
-
.filter(c => c && c.last_call_at)
|
|
323
|
-
.sort((a, b) =>
|
|
349
|
+
.filter(c => c && c.last_call_at && !isMine(c))
|
|
350
|
+
.sort((a, b) => {
|
|
351
|
+
const aPinned = pinnedIds.includes(String(a.id));
|
|
352
|
+
const bPinned = pinnedIds.includes(String(b.id));
|
|
353
|
+
if (aPinned && !bPinned) return -1;
|
|
354
|
+
if (!aPinned && bPinned) return 1;
|
|
355
|
+
return String(b.last_call_at || '').localeCompare(String(a.last_call_at || ''));
|
|
356
|
+
})
|
|
324
357
|
.slice(0, 12);
|
|
325
358
|
|
|
326
|
-
const groups = new Map();
|
|
327
|
-
for (const c of contacts) {
|
|
328
|
-
const owner = String(c?.owner || '').trim() || '(unknown owner)';
|
|
329
|
-
if (!groups.has(owner)) groups.set(owner, []);
|
|
330
|
-
groups.get(owner).push(c);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const owners = Array.from(groups.keys()).sort((a, b) => {
|
|
334
|
-
// Keep local owner group near the top (after the dedicated "My agents" section).
|
|
335
|
-
const local = norm(getLocalOwnerName());
|
|
336
|
-
const aIsLocal = local && norm(a) === local;
|
|
337
|
-
const bIsLocal = local && norm(b) === local;
|
|
338
|
-
if (aIsLocal && !bIsLocal) return -1;
|
|
339
|
-
if (!aIsLocal && bIsLocal) return 1;
|
|
340
|
-
if (a === '(unknown owner)' && b !== '(unknown owner)') return 1;
|
|
341
|
-
if (a !== '(unknown owner)' && b === '(unknown owner)') return -1;
|
|
342
|
-
return a.localeCompare(b);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
359
|
const rowHtml = (c, opts = {}) => {
|
|
346
360
|
const canCall = Boolean(c?.can_call);
|
|
347
361
|
const mine = Boolean(c?.is_mine);
|
|
@@ -350,13 +364,17 @@ function renderContacts() {
|
|
|
350
364
|
const lastCallAt = c?.last_call_at ? fmtDate(c.last_call_at) : '-';
|
|
351
365
|
const calls = Number.isFinite(c?.call_count) ? c.call_count : (c?.call_count || 0);
|
|
352
366
|
const isSelected = selected && String(c?.id) === selected;
|
|
367
|
+
const isPinned = pinnedIds.includes(String(c?.id));
|
|
353
368
|
|
|
354
369
|
const actionBits = [];
|
|
370
|
+
if (opts.showPin) {
|
|
371
|
+
actionBits.push(`<sl-icon-button name="${isPinned ? 'pin-fill' : 'pin'}" class="pin-btn${isPinned ? ' pinned' : ''}" data-pin-contact="${esc(c.id)}" title="${isPinned ? 'Unpin' : 'Pin to top'}"></sl-icon-button>`);
|
|
372
|
+
}
|
|
355
373
|
if (c?.last_call_id) {
|
|
356
|
-
actionBits.push(`<button data-open-call="${esc(c.last_call_id)}"
|
|
374
|
+
actionBits.push(`<sl-button size="small" data-open-call="${esc(c.last_call_id)}">Transcript</sl-button>`);
|
|
357
375
|
}
|
|
358
|
-
actionBits.push(`<button data-toggle-mine="${esc(c.id)}"
|
|
359
|
-
actionBits.push(`<button data-remove-contact="${esc(c.id)}"
|
|
376
|
+
actionBits.push(`<sl-button size="small" data-toggle-mine="${esc(c.id)}">${mine ? 'Unmark mine' : 'Mark mine'}</sl-button>`);
|
|
377
|
+
actionBits.push(`<sl-button size="small" variant="danger" data-remove-contact="${esc(c.id)}">Remove</sl-button>`);
|
|
360
378
|
|
|
361
379
|
const locationCell = opts.showLocation ? `<td>${esc(formatLocation(c))}</td>` : '';
|
|
362
380
|
const ownerCell = opts.showOwner ? `<td>${esc(c?.owner || '-')}</td>` : '';
|
|
@@ -366,8 +384,8 @@ function renderContacts() {
|
|
|
366
384
|
<tr ${isSelected ? 'data-selected="1"' : ''}>
|
|
367
385
|
<td>
|
|
368
386
|
<div class="row" style="margin:0;">
|
|
369
|
-
<button
|
|
370
|
-
<button data-contact-call="${esc(c.id)}"
|
|
387
|
+
<sl-button variant="text" size="small" data-contact-select="${esc(c.id)}">${esc(contactLabel(c))}</sl-button>
|
|
388
|
+
<sl-button size="small" variant="primary" data-contact-call="${esc(c.id)}" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
371
389
|
</div>
|
|
372
390
|
</td>
|
|
373
391
|
${locationCell}
|
|
@@ -400,30 +418,48 @@ function renderContacts() {
|
|
|
400
418
|
};
|
|
401
419
|
|
|
402
420
|
const myAgentsSection = `
|
|
403
|
-
<
|
|
421
|
+
<sl-card>
|
|
404
422
|
<h3>My agents</h3>
|
|
405
423
|
${tableHtml(myAgents, { showLocation: true, showOwner: false, showSummary: false })}
|
|
406
|
-
</
|
|
424
|
+
</sl-card>
|
|
407
425
|
`;
|
|
408
426
|
|
|
409
427
|
const lastCalledSection = `
|
|
410
|
-
<
|
|
428
|
+
<sl-card>
|
|
411
429
|
<h3>Last called agents</h3>
|
|
412
|
-
${tableHtml(lastCalled, { showLocation: false, showOwner: true, showSummary: false })}
|
|
413
|
-
</
|
|
430
|
+
${tableHtml(lastCalled, { showLocation: false, showOwner: true, showSummary: false, showPin: true })}
|
|
431
|
+
</sl-card>
|
|
414
432
|
`;
|
|
415
433
|
|
|
416
|
-
const
|
|
417
|
-
|
|
434
|
+
const otherContacts = contacts.filter(c => !isMine(c));
|
|
435
|
+
const otherGroups = new Map();
|
|
436
|
+
for (const c of otherContacts) {
|
|
437
|
+
const owner = String(c?.owner || '').trim() || '(unknown owner)';
|
|
438
|
+
if (!otherGroups.has(owner)) otherGroups.set(owner, []);
|
|
439
|
+
otherGroups.get(owner).push(c);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const otherOwners = Array.from(otherGroups.keys()).sort((a, b) => {
|
|
443
|
+
if (a === '(unknown owner)' && b !== '(unknown owner)') return 1;
|
|
444
|
+
if (a !== '(unknown owner)' && b === '(unknown owner)') return -1;
|
|
445
|
+
return a.localeCompare(b);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const groupedSections = otherOwners.map(owner => {
|
|
449
|
+
const rows = (otherGroups.get(owner) || []).slice().sort((a, b) => contactLabel(a).localeCompare(contactLabel(b)));
|
|
418
450
|
return `
|
|
419
|
-
<
|
|
451
|
+
<sl-card>
|
|
420
452
|
<h3>${esc(owner)}</h3>
|
|
421
453
|
${tableHtml(rows, { showLocation: false, showOwner: false, showSummary: true })}
|
|
422
|
-
</
|
|
454
|
+
</sl-card>
|
|
423
455
|
`;
|
|
424
456
|
}).join('');
|
|
425
457
|
|
|
426
|
-
|
|
458
|
+
const otherAgentsHeading = otherOwners.length
|
|
459
|
+
? `<h3 style="margin-top:1rem;">Other Agents</h3>`
|
|
460
|
+
: '';
|
|
461
|
+
|
|
462
|
+
el.innerHTML = `${myAgentsSection}${lastCalledSection}${otherAgentsHeading}${groupedSections}`;
|
|
427
463
|
}
|
|
428
464
|
|
|
429
465
|
async function loadContacts() {
|
|
@@ -437,6 +473,15 @@ function bindContactsActions() {
|
|
|
437
473
|
const form = document.getElementById('add-contact-form');
|
|
438
474
|
if (!form) return;
|
|
439
475
|
|
|
476
|
+
// Cancel button collapses the sl-details
|
|
477
|
+
const cancelBtn = document.getElementById('add-contact-cancel');
|
|
478
|
+
const addDetails = document.getElementById('add-contact-details');
|
|
479
|
+
if (cancelBtn && addDetails) {
|
|
480
|
+
cancelBtn.addEventListener('click', () => {
|
|
481
|
+
addDetails.open = false;
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
440
485
|
const urlEl = document.getElementById('add-contact-url');
|
|
441
486
|
const mineEl = document.getElementById('add-contact-mine');
|
|
442
487
|
const serverNameEl = document.getElementById('add-contact-server-name');
|
|
@@ -449,9 +494,9 @@ function bindContactsActions() {
|
|
|
449
494
|
serverNameEl.value = match[1];
|
|
450
495
|
}
|
|
451
496
|
};
|
|
452
|
-
urlEl?.addEventListener('blur', defaultServerNameFromUrl);
|
|
453
|
-
urlEl?.addEventListener('change', defaultServerNameFromUrl);
|
|
454
|
-
mineEl?.addEventListener('change', () => {
|
|
497
|
+
urlEl?.addEventListener('sl-blur', defaultServerNameFromUrl);
|
|
498
|
+
urlEl?.addEventListener('sl-change', defaultServerNameFromUrl);
|
|
499
|
+
mineEl?.addEventListener('sl-change', () => {
|
|
455
500
|
if (!serverNameEl) return;
|
|
456
501
|
serverNameEl.disabled = !mineEl.checked;
|
|
457
502
|
if (mineEl.checked) {
|
|
@@ -467,7 +512,7 @@ function bindContactsActions() {
|
|
|
467
512
|
const url = document.getElementById('add-contact-url').value.trim();
|
|
468
513
|
const name = document.getElementById('add-contact-name').value.trim();
|
|
469
514
|
const owner = document.getElementById('add-contact-owner').value.trim();
|
|
470
|
-
const
|
|
515
|
+
const isMineVal = Boolean(document.getElementById('add-contact-mine')?.checked);
|
|
471
516
|
const serverName = document.getElementById('add-contact-server-name').value.trim();
|
|
472
517
|
const tagsRaw = document.getElementById('add-contact-tags').value.trim();
|
|
473
518
|
const notes = document.getElementById('add-contact-notes').value.trim();
|
|
@@ -497,7 +542,7 @@ function bindContactsActions() {
|
|
|
497
542
|
invite_url: url,
|
|
498
543
|
name: name || undefined,
|
|
499
544
|
owner: owner || undefined,
|
|
500
|
-
is_mine:
|
|
545
|
+
is_mine: isMineVal,
|
|
501
546
|
server_name: serverName || undefined,
|
|
502
547
|
tags,
|
|
503
548
|
notes: notes || undefined,
|
|
@@ -506,15 +551,26 @@ function bindContactsActions() {
|
|
|
506
551
|
});
|
|
507
552
|
showNotice('Contact added');
|
|
508
553
|
form.reset();
|
|
554
|
+
// Collapse the sl-details after successful add
|
|
555
|
+
if (addDetails) addDetails.open = false;
|
|
509
556
|
await loadContacts();
|
|
510
557
|
} catch (err) {
|
|
511
558
|
showNotice(err.message);
|
|
512
559
|
}
|
|
513
560
|
});
|
|
514
561
|
|
|
515
|
-
|
|
562
|
+
// Event delegation on the contacts tab panel
|
|
563
|
+
const panel = document.querySelector('sl-tab-panel[name="contacts"]');
|
|
516
564
|
panel?.addEventListener('click', async (e) => {
|
|
517
|
-
const
|
|
565
|
+
const pinBtn = e.target.closest('[data-pin-contact]');
|
|
566
|
+
if (pinBtn) {
|
|
567
|
+
e.preventDefault();
|
|
568
|
+
const id = pinBtn.dataset.pinContact;
|
|
569
|
+
if (id) togglePin(id);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const selectBtn = e.target.closest('[data-contact-select]');
|
|
518
574
|
if (selectBtn) {
|
|
519
575
|
e.preventDefault();
|
|
520
576
|
const id = selectBtn.dataset.contactSelect;
|
|
@@ -524,14 +580,14 @@ function bindContactsActions() {
|
|
|
524
580
|
return;
|
|
525
581
|
}
|
|
526
582
|
|
|
527
|
-
const openBtn = e.target.closest('
|
|
583
|
+
const openBtn = e.target.closest('[data-open-call]');
|
|
528
584
|
if (openBtn) {
|
|
529
585
|
e.preventDefault();
|
|
530
586
|
openCallTranscript(openBtn.dataset.openCall);
|
|
531
587
|
return;
|
|
532
588
|
}
|
|
533
589
|
|
|
534
|
-
const mineBtn = e.target.closest('
|
|
590
|
+
const mineBtn = e.target.closest('[data-toggle-mine]');
|
|
535
591
|
if (mineBtn) {
|
|
536
592
|
e.preventDefault();
|
|
537
593
|
const id = mineBtn.dataset.toggleMine;
|
|
@@ -558,7 +614,7 @@ function bindContactsActions() {
|
|
|
558
614
|
return;
|
|
559
615
|
}
|
|
560
616
|
|
|
561
|
-
const removeBtn = e.target.closest('
|
|
617
|
+
const removeBtn = e.target.closest('[data-remove-contact]');
|
|
562
618
|
if (removeBtn) {
|
|
563
619
|
e.preventDefault();
|
|
564
620
|
const id = removeBtn.dataset.removeContact;
|
|
@@ -580,7 +636,7 @@ function bindContactsActions() {
|
|
|
580
636
|
return;
|
|
581
637
|
}
|
|
582
638
|
|
|
583
|
-
const callBtn = e.target.closest('
|
|
639
|
+
const callBtn = e.target.closest('[data-contact-call]');
|
|
584
640
|
if (callBtn) {
|
|
585
641
|
e.preventDefault();
|
|
586
642
|
const id = callBtn.dataset.contactCall;
|
|
@@ -645,15 +701,35 @@ async function loadCallDetail(conversationId) {
|
|
|
645
701
|
const payload = await request(`/calls/${encodeURIComponent(conversationId)}?messages=40`);
|
|
646
702
|
const call = payload.call;
|
|
647
703
|
const el = document.getElementById('call-detail');
|
|
648
|
-
|
|
649
|
-
|
|
704
|
+
|
|
705
|
+
// Summary: prefer agent-generated summary, fall back to owner summary
|
|
706
|
+
const summaryText = call.summary || call.ownerContext?.summary || '';
|
|
707
|
+
const summaryHtml = summaryText
|
|
708
|
+
? `<pre class="summary">${esc(summaryText)}</pre>`
|
|
709
|
+
: `<p class="summary-pending"><em>${call.status === 'active' ? 'Call in progress\u2026' : 'Summary pending\u2026'}</em></p>`;
|
|
710
|
+
|
|
711
|
+
// Full transcript in a collapsible section
|
|
712
|
+
const messages = (call.recentMessages || []);
|
|
713
|
+
const transcriptLines = messages
|
|
714
|
+
.map(msg => `[${esc(fmtDate(msg.timestamp))}] ${esc(msg.direction)}: ${esc(msg.content)}`)
|
|
650
715
|
.join('\n\n');
|
|
716
|
+
const totalMessages = call.messageCount || messages.length;
|
|
717
|
+
const countLabel = messages.length < totalMessages
|
|
718
|
+
? `${messages.length} of ${totalMessages} messages`
|
|
719
|
+
: `${messages.length} message${messages.length === 1 ? '' : 's'}`;
|
|
720
|
+
const transcriptHtml = messages.length
|
|
721
|
+
? `<sl-details class="transcript-details" summary="Full Transcript (${countLabel})">
|
|
722
|
+
<pre class="transcript">${transcriptLines}</pre>
|
|
723
|
+
</sl-details>`
|
|
724
|
+
: '';
|
|
725
|
+
|
|
651
726
|
el.innerHTML = `
|
|
652
|
-
<h3>Call Detail: ${call.id}</h3>
|
|
653
|
-
<p><strong>Contact:</strong> ${call.contact?.name || call.contact || '-'}</p>
|
|
654
|
-
<p><strong>Status:</strong> ${call.status || '-'}</p>
|
|
655
|
-
<p><strong>Summary:</strong
|
|
656
|
-
|
|
727
|
+
<h3>Call Detail: ${esc(call.id)}</h3>
|
|
728
|
+
<p><strong>Contact:</strong> ${esc(call.contact?.name || call.contact || '-')}</p>
|
|
729
|
+
<p><strong>Status:</strong> ${esc(call.status || '-')}</p>
|
|
730
|
+
<p><strong>Summary:</strong></p>
|
|
731
|
+
${summaryHtml}
|
|
732
|
+
${transcriptHtml}
|
|
657
733
|
`;
|
|
658
734
|
}
|
|
659
735
|
|
|
@@ -690,7 +766,7 @@ function renderContactDetail() {
|
|
|
690
766
|
const resultHtml = result
|
|
691
767
|
? `<div style="margin-top:0.6rem;">
|
|
692
768
|
<strong>Last call result:</strong> ${result.success ? 'success' : 'failed'}<br>
|
|
693
|
-
${result.conversation_id ? `Conversation: <span class="mono">${esc(result.conversation_id)}</span> <button data-open-call="${esc(result.conversation_id)}"
|
|
769
|
+
${result.conversation_id ? `Conversation: <span class="mono">${esc(result.conversation_id)}</span> <sl-button size="small" data-open-call="${esc(result.conversation_id)}">Transcript</sl-button><br>` : ''}
|
|
694
770
|
${result.error ? `<span class="mono">${esc(result.error)}</span><br>` : ''}
|
|
695
771
|
${result.response ? `<pre class="summary">${esc(String(result.response))}</pre>` : ''}
|
|
696
772
|
</div>`
|
|
@@ -705,7 +781,7 @@ function renderContactDetail() {
|
|
|
705
781
|
<td>${esc(call.status || '-')}</td>
|
|
706
782
|
<td>${esc(fmtDate(call.last_message_at))}</td>
|
|
707
783
|
<td title="${esc(summary)}">${esc(preview)}</td>
|
|
708
|
-
<td><button data-open-call="${esc(call.id)}"
|
|
784
|
+
<td><sl-button size="small" data-open-call="${esc(call.id)}">Transcript</sl-button></td>
|
|
709
785
|
</tr>
|
|
710
786
|
`;
|
|
711
787
|
}).join('');
|
|
@@ -713,16 +789,16 @@ function renderContactDetail() {
|
|
|
713
789
|
el.innerHTML = `
|
|
714
790
|
<div class="row">
|
|
715
791
|
<h3 style="margin:0;">Contact: ${esc(contactLabel(contact))}</h3>
|
|
716
|
-
<button data-contact-call="${esc(contact.id)}"
|
|
717
|
-
<button data-remove-contact="${esc(contact.id)}"
|
|
792
|
+
<sl-button size="small" variant="primary" data-contact-call="${esc(contact.id)}" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
793
|
+
<sl-button size="small" variant="danger" data-remove-contact="${esc(contact.id)}">Remove</sl-button>
|
|
718
794
|
</div>
|
|
719
795
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
796
|
+
<div class="row" style="margin-bottom:0.4rem;">
|
|
797
|
+
<div><strong>Mine:</strong> ${contact.is_mine ? 'yes' : 'no'}</div>
|
|
798
|
+
<div><strong>Owner:</strong> ${esc(contact.owner || '-')}</div>
|
|
799
|
+
<div><strong>Web address:</strong> <span class="mono">${esc(contact.web_address || contact.host || '-')}</span></div>
|
|
800
|
+
<div><strong>Server name:</strong> ${esc(contact.server_name || '-')}</div>
|
|
801
|
+
</div>
|
|
726
802
|
<div class="row">
|
|
727
803
|
<div><strong>Status:</strong> ${esc(contact.status || '-')}</div>
|
|
728
804
|
<div><strong>Total calls:</strong> ${esc(String(contact.call_count || 0))}</div>
|
|
@@ -731,48 +807,45 @@ function renderContactDetail() {
|
|
|
731
807
|
|
|
732
808
|
${resultHtml}
|
|
733
809
|
|
|
734
|
-
<details style="margin-top:0.8rem;"
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<details style="margin-top:0.8rem;" open>
|
|
751
|
-
<summary><strong>Call</strong></summary>
|
|
810
|
+
<sl-details summary="Edit contact" open style="margin-top:0.8rem;">
|
|
811
|
+
<form id="contact-edit-form" data-contact-id="${esc(contact.id)}" style="margin-top:0.6rem;">
|
|
812
|
+
<sl-input id="contact-edit-name" label="Agent name" value="${esc(contact.name || '')}"></sl-input>
|
|
813
|
+
<sl-input id="contact-edit-owner" label="Owner name" value="${esc(contact.owner || '')}"></sl-input>
|
|
814
|
+
<sl-checkbox id="contact-edit-mine" ${contact.is_mine ? 'checked' : ''}>Mark as mine (personal agent)</sl-checkbox>
|
|
815
|
+
<sl-input id="contact-edit-server-name" label="Server name (my agents only)" value="${esc(contact.server_name || '')}" ${contact.is_mine ? '' : 'disabled'}></sl-input>
|
|
816
|
+
<sl-input id="contact-edit-tags" label="Tags" value="${esc(tagsText)}" placeholder="comma,separated"></sl-input>
|
|
817
|
+
<sl-textarea id="contact-edit-notes" label="Notes" rows="3" value="${esc(contact.notes || '')}"></sl-textarea>
|
|
818
|
+
<sl-textarea id="contact-edit-fields" label="Fields (JSON)" rows="5" value="${esc(fieldsText)}"></sl-textarea>
|
|
819
|
+
<div class="row">
|
|
820
|
+
<sl-button type="submit" variant="primary" size="small">Save</sl-button>
|
|
821
|
+
</div>
|
|
822
|
+
</form>
|
|
823
|
+
</sl-details>
|
|
824
|
+
|
|
825
|
+
<sl-details summary="Call" open style="margin-top:0.8rem;">
|
|
752
826
|
<form id="contact-call-form" data-contact-id="${esc(contact.id)}" style="margin-top:0.6rem;">
|
|
753
|
-
<
|
|
827
|
+
<sl-textarea id="contact-call-message" label="Message" rows="4" placeholder="Message to send"></sl-textarea>
|
|
754
828
|
<div class="row">
|
|
755
|
-
<button type="submit" ${canCall ? '' : 'disabled'}>Call</button>
|
|
829
|
+
<sl-button type="submit" variant="primary" size="small" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
756
830
|
</div>
|
|
757
831
|
</form>
|
|
758
|
-
</details>
|
|
832
|
+
</sl-details>
|
|
759
833
|
|
|
760
|
-
<details style="margin-top:0.8rem;">
|
|
761
|
-
<summary><strong>Call history</strong></summary>
|
|
834
|
+
<sl-details summary="Call history" style="margin-top:0.8rem;">
|
|
762
835
|
<div style="margin-top:0.6rem;">
|
|
763
836
|
<table>
|
|
764
837
|
<thead><tr><th>ID</th><th>Status</th><th>Updated</th><th>Summary</th><th>Action</th></tr></thead>
|
|
765
838
|
<tbody>${callRows || '<tr><td colspan="5">No calls found.</td></tr>'}</tbody>
|
|
766
839
|
</table>
|
|
767
840
|
</div>
|
|
768
|
-
</details>
|
|
841
|
+
</sl-details>
|
|
769
842
|
`;
|
|
770
843
|
|
|
771
844
|
const editForm = document.getElementById('contact-edit-form');
|
|
772
845
|
if (editForm) {
|
|
773
846
|
const mineEl = document.getElementById('contact-edit-mine');
|
|
774
847
|
const serverNameEl = document.getElementById('contact-edit-server-name');
|
|
775
|
-
mineEl?.addEventListener('change', () => {
|
|
848
|
+
mineEl?.addEventListener('sl-change', () => {
|
|
776
849
|
if (!serverNameEl) return;
|
|
777
850
|
serverNameEl.disabled = !mineEl.checked;
|
|
778
851
|
});
|
|
@@ -802,19 +875,19 @@ function renderContactDetail() {
|
|
|
802
875
|
}
|
|
803
876
|
}
|
|
804
877
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
878
|
+
try {
|
|
879
|
+
await request(`/contacts/${encodeURIComponent(id)}`, {
|
|
880
|
+
method: 'PUT',
|
|
881
|
+
body: JSON.stringify({
|
|
882
|
+
name: document.getElementById('contact-edit-name').value,
|
|
883
|
+
owner: document.getElementById('contact-edit-owner').value,
|
|
884
|
+
is_mine: Boolean(document.getElementById('contact-edit-mine')?.checked),
|
|
885
|
+
server_name: document.getElementById('contact-edit-server-name').value,
|
|
886
|
+
notes: document.getElementById('contact-edit-notes').value,
|
|
887
|
+
tags,
|
|
888
|
+
fields
|
|
889
|
+
})
|
|
890
|
+
});
|
|
818
891
|
showNotice('Contact saved');
|
|
819
892
|
await loadContacts();
|
|
820
893
|
await loadCallsForContact(id);
|
|
@@ -933,10 +1006,10 @@ function renderLogStats() {
|
|
|
933
1006
|
<strong>Total:</strong> ${stats.total || 0}
|
|
934
1007
|
</div>
|
|
935
1008
|
<div class="row">
|
|
936
|
-
<strong>By level:</strong> ${levels.map(([k, v]) => `${esc(k)}=${v}`).join('
|
|
1009
|
+
<strong>By level:</strong> ${levels.map(([k, v]) => `${esc(k)}=${v}`).join(' \u00b7 ') || '(none)'}
|
|
937
1010
|
</div>
|
|
938
1011
|
<div class="row">
|
|
939
|
-
<strong>Top components:</strong> ${components.map(([k, v]) => `${esc(k)}=${v}`).join('
|
|
1012
|
+
<strong>Top components:</strong> ${components.map(([k, v]) => `${esc(k)}=${v}`).join(' \u00b7 ') || '(none)'}
|
|
940
1013
|
</div>
|
|
941
1014
|
`;
|
|
942
1015
|
}
|
|
@@ -964,7 +1037,7 @@ function renderTraceDetail() {
|
|
|
964
1037
|
el.innerHTML = `
|
|
965
1038
|
<div class="row">
|
|
966
1039
|
<h3 style="margin:0;">Trace: <span class="mono">${esc(state.trace.trace_id || '')}</span></h3>
|
|
967
|
-
<button id="clear-trace">Clear</button>
|
|
1040
|
+
<sl-button id="clear-trace" size="small">Clear</sl-button>
|
|
968
1041
|
</div>
|
|
969
1042
|
<pre class="summary mono">${esc(lines || 'No trace logs.')}</pre>
|
|
970
1043
|
`;
|
|
@@ -990,8 +1063,8 @@ function renderLogs() {
|
|
|
990
1063
|
<td>${esc(row.component || '-')}</td>
|
|
991
1064
|
<td>${esc(row.event || '-')}</td>
|
|
992
1065
|
<td title="${esc(row.message || '')}">${esc(String(row.message || '').slice(0, 120) || '-')}</td>
|
|
993
|
-
<td class="mono">${esc(trace ? trace.slice(0, 14) + '
|
|
994
|
-
<td class="mono">${esc(row.conversation_id ? row.conversation_id.slice(0, 14) + '
|
|
1066
|
+
<td class="mono">${esc(trace ? trace.slice(0, 14) + '\u2026' : '-')}</td>
|
|
1067
|
+
<td class="mono">${esc(row.conversation_id ? row.conversation_id.slice(0, 14) + '\u2026' : '-')}</td>
|
|
995
1068
|
<td class="mono">${esc(row.token_id || '-')}</td>
|
|
996
1069
|
<td>${esc(row.error_code || '-')}</td>
|
|
997
1070
|
<td>${esc(row.status_code ?? '-')}</td>
|
|
@@ -1029,16 +1102,15 @@ function fillTierSelects() {
|
|
|
1029
1102
|
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1030
1103
|
const inviteTier = document.getElementById('invite-tier');
|
|
1031
1104
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1105
|
+
// Build options HTML for sl-select elements
|
|
1106
|
+
const optionsHtml = tiers.map(tier =>
|
|
1107
|
+
`<sl-option value="${esc(tier.id)}">${esc(tier.id)} (${esc(tier.name || tier.id)})</sl-option>`
|
|
1108
|
+
).join('');
|
|
1034
1109
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
inviteTier.add(option.cloneNode(true));
|
|
1040
|
-
newTierCopy.add(option.cloneNode(true));
|
|
1041
|
-
});
|
|
1110
|
+
tierSelect.innerHTML = optionsHtml;
|
|
1111
|
+
copyFrom.innerHTML = optionsHtml;
|
|
1112
|
+
inviteTier.innerHTML = optionsHtml;
|
|
1113
|
+
newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1042
1114
|
|
|
1043
1115
|
if (tiers.length > 0) {
|
|
1044
1116
|
tierSelect.value = tiers[0].id;
|
|
@@ -1062,7 +1134,7 @@ function renderTierEditor(tierId) {
|
|
|
1062
1134
|
}
|
|
1063
1135
|
|
|
1064
1136
|
function bindSettingsActions() {
|
|
1065
|
-
document.getElementById('tier-select').addEventListener('change', (e) => {
|
|
1137
|
+
document.getElementById('tier-select').addEventListener('sl-change', (e) => {
|
|
1066
1138
|
renderTierEditor(e.target.value);
|
|
1067
1139
|
});
|
|
1068
1140
|
|
|
@@ -1150,7 +1222,7 @@ function renderCallbookStatus() {
|
|
|
1150
1222
|
|
|
1151
1223
|
const s = state.dashboardStatus;
|
|
1152
1224
|
if (!s) {
|
|
1153
|
-
el.textContent = 'Loading
|
|
1225
|
+
el.textContent = 'Loading\u2026';
|
|
1154
1226
|
return;
|
|
1155
1227
|
}
|
|
1156
1228
|
|
|
@@ -1171,8 +1243,7 @@ function renderCallbookStatus() {
|
|
|
1171
1243
|
const extMetaText = extMeta.length ? ` <span class="mono">(${esc(extMeta.join(', '))})</span>` : '';
|
|
1172
1244
|
const extErrorText = ext && ext.error ? esc(ext.error) : '';
|
|
1173
1245
|
const extAttemptsHtml = extAttempts.length
|
|
1174
|
-
? `<details style="margin-top:0.5rem;">
|
|
1175
|
-
<summary>External IP probe</summary>
|
|
1246
|
+
? `<sl-details summary="External IP probe" style="margin-top:0.5rem;">
|
|
1176
1247
|
<div class="mono" style="margin-top:0.35rem;">
|
|
1177
1248
|
${extAttempts.map(a => {
|
|
1178
1249
|
const service = a && a.service ? String(a.service) : '-';
|
|
@@ -1182,7 +1253,7 @@ function renderCallbookStatus() {
|
|
|
1182
1253
|
return esc(`${service}: ${ok ? 'ok' + status : 'failed' + err}`);
|
|
1183
1254
|
}).join('<br>')}
|
|
1184
1255
|
</div>
|
|
1185
|
-
</details>`
|
|
1256
|
+
</sl-details>`
|
|
1186
1257
|
: '';
|
|
1187
1258
|
|
|
1188
1259
|
el.innerHTML = `
|
|
@@ -1204,18 +1275,18 @@ function renderAutoUpdateStatus() {
|
|
|
1204
1275
|
|
|
1205
1276
|
const au = state.autoUpdate;
|
|
1206
1277
|
if (!au) {
|
|
1207
|
-
el.textContent = 'Loading
|
|
1278
|
+
el.textContent = 'Loading\u2026';
|
|
1208
1279
|
if (toggleBtn) toggleBtn.disabled = true;
|
|
1209
1280
|
return;
|
|
1210
1281
|
}
|
|
1211
1282
|
|
|
1212
1283
|
const stateText = formatUpdaterState(au.state);
|
|
1213
|
-
const
|
|
1284
|
+
const variant = badgeVariant(au.state);
|
|
1214
1285
|
const enabled = Boolean(au.enabled);
|
|
1215
1286
|
const intervalSec = Number.isFinite(au.interval_ms) ? Math.floor(au.interval_ms / 1000) : null;
|
|
1216
1287
|
|
|
1217
1288
|
el.innerHTML = `
|
|
1218
|
-
<div><strong>Status:</strong> <
|
|
1289
|
+
<div><strong>Status:</strong> <sl-badge variant="${variant}">${esc(stateText)}</sl-badge></div>
|
|
1219
1290
|
<div><strong>Enabled:</strong> ${enabled ? 'yes' : 'no'}</div>
|
|
1220
1291
|
<div><strong>Current version:</strong> <span class="mono">${esc(au.current_version || '-')}</span></div>
|
|
1221
1292
|
<div><strong>Latest version:</strong> <span class="mono">${esc(au.latest_version || '-')}</span></div>
|
|
@@ -1274,13 +1345,13 @@ function renderCallbookDevices() {
|
|
|
1274
1345
|
<td>${esc(String(sessions))}</td>
|
|
1275
1346
|
<td>${revoked ? esc(fmtDate(dev.revoked_at)) : '-'}</td>
|
|
1276
1347
|
<td>
|
|
1277
|
-
<button data-revoke="${esc(dev.id)}" ${revoked ? 'disabled' : ''}>Revoke</button>
|
|
1348
|
+
<sl-button size="small" variant="danger" data-revoke="${esc(dev.id)}" ${revoked ? 'disabled' : ''}>Revoke</sl-button>
|
|
1278
1349
|
</td>
|
|
1279
1350
|
`;
|
|
1280
1351
|
tbody.appendChild(tr);
|
|
1281
1352
|
});
|
|
1282
1353
|
|
|
1283
|
-
tbody.querySelectorAll('
|
|
1354
|
+
tbody.querySelectorAll('[data-revoke]').forEach(btn => {
|
|
1284
1355
|
btn.addEventListener('click', async () => {
|
|
1285
1356
|
const deviceId = btn.dataset.revoke;
|
|
1286
1357
|
if (!deviceId) return;
|
|
@@ -1311,14 +1382,6 @@ function bindCallbookActions() {
|
|
|
1311
1382
|
const labelEl = document.getElementById('callbook-label');
|
|
1312
1383
|
const warningsEl = document.getElementById('callbook-warnings');
|
|
1313
1384
|
|
|
1314
|
-
document.getElementById('callbook-refresh')?.addEventListener('click', () => {
|
|
1315
|
-
Promise.all([loadDashboardStatus(true), loadCallbookDevices()]).catch(err => showNotice(err.message));
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
document.getElementById('callbook-refresh-devices')?.addEventListener('click', () => {
|
|
1319
|
-
loadCallbookDevices().catch(err => showNotice(err.message));
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
1385
|
document.getElementById('callbook-logout')?.addEventListener('click', async () => {
|
|
1323
1386
|
try {
|
|
1324
1387
|
await request('/callbook/logout', { method: 'POST' });
|
|
@@ -1362,10 +1425,6 @@ function bindCallbookActions() {
|
|
|
1362
1425
|
}
|
|
1363
1426
|
|
|
1364
1427
|
function bindAutoUpdateActions() {
|
|
1365
|
-
document.getElementById('auto-update-refresh')?.addEventListener('click', () => {
|
|
1366
|
-
loadAutoUpdateStatus().catch(err => showNotice(err.message));
|
|
1367
|
-
});
|
|
1368
|
-
|
|
1369
1428
|
document.getElementById('auto-update-check')?.addEventListener('click', async () => {
|
|
1370
1429
|
try {
|
|
1371
1430
|
await request('/update/check', { method: 'POST', body: JSON.stringify({}) });
|
|
@@ -1413,13 +1472,13 @@ function renderInvites() {
|
|
|
1413
1472
|
<td>${invite.tier || '-'}</td>
|
|
1414
1473
|
<td>${invite.calls_made || 0}${invite.max_calls ? `/${invite.max_calls}` : ''}</td>
|
|
1415
1474
|
<td>${fmtDate(invite.expires_at)}</td>
|
|
1416
|
-
<td>${invite.revoked ? 'revoked' : 'active'}</td>
|
|
1417
|
-
<td><button data-revoke="${invite.id}" ${invite.revoked ? 'disabled' : ''}>Revoke</button></td>
|
|
1475
|
+
<td><sl-badge variant="${invite.revoked ? 'danger' : 'success'}">${invite.revoked ? 'revoked' : 'active'}</sl-badge></td>
|
|
1476
|
+
<td><sl-button size="small" variant="danger" data-revoke="${invite.id}" ${invite.revoked ? 'disabled' : ''}>Revoke</sl-button></td>
|
|
1418
1477
|
`;
|
|
1419
1478
|
tbody.appendChild(tr);
|
|
1420
1479
|
});
|
|
1421
1480
|
|
|
1422
|
-
tbody.querySelectorAll('
|
|
1481
|
+
tbody.querySelectorAll('[data-revoke]').forEach(btn => {
|
|
1423
1482
|
btn.addEventListener('click', async () => {
|
|
1424
1483
|
const tokenId = btn.dataset.revoke;
|
|
1425
1484
|
await request(`/invites/${encodeURIComponent(tokenId)}/revoke`, { method: 'POST' });
|
|
@@ -1436,6 +1495,18 @@ async function loadInvites() {
|
|
|
1436
1495
|
}
|
|
1437
1496
|
|
|
1438
1497
|
function bindInviteActions() {
|
|
1498
|
+
const cancelBtn = document.getElementById('generate-invite-cancel');
|
|
1499
|
+
const inviteDetails = document.getElementById('generate-invite-details');
|
|
1500
|
+
const inviteMessageWrap = document.getElementById('invite-message-wrap');
|
|
1501
|
+
const inviteMessage = document.getElementById('invite-message');
|
|
1502
|
+
|
|
1503
|
+
// Cancel button collapses the sl-details
|
|
1504
|
+
if (cancelBtn && inviteDetails) {
|
|
1505
|
+
cancelBtn.addEventListener('click', () => {
|
|
1506
|
+
inviteDetails.open = false;
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1439
1510
|
document.getElementById('invite-form').addEventListener('submit', async (e) => {
|
|
1440
1511
|
e.preventDefault();
|
|
1441
1512
|
const body = {
|
|
@@ -1450,7 +1521,13 @@ function bindInviteActions() {
|
|
|
1450
1521
|
method: 'POST',
|
|
1451
1522
|
body: JSON.stringify(body)
|
|
1452
1523
|
});
|
|
1453
|
-
|
|
1524
|
+
// Show the invite message textarea with the result
|
|
1525
|
+
if (inviteMessage) {
|
|
1526
|
+
inviteMessage.value = result.invite_message || result.invite_url;
|
|
1527
|
+
if (inviteMessageWrap) inviteMessageWrap.style.display = 'block';
|
|
1528
|
+
}
|
|
1529
|
+
// Collapse the details after successful creation
|
|
1530
|
+
if (inviteDetails) inviteDetails.open = false;
|
|
1454
1531
|
if (result.warnings && result.warnings.length) {
|
|
1455
1532
|
showNotice(result.warnings[0]);
|
|
1456
1533
|
} else {
|
|
@@ -1460,13 +1537,7 @@ function bindInviteActions() {
|
|
|
1460
1537
|
});
|
|
1461
1538
|
}
|
|
1462
1539
|
|
|
1463
|
-
function
|
|
1464
|
-
document.getElementById('refresh-contacts').addEventListener('click', () => loadContacts().catch(err => showNotice(err.message)));
|
|
1465
|
-
document.getElementById('refresh-calls').addEventListener('click', () => loadCalls().catch(err => showNotice(err.message)));
|
|
1466
|
-
document.getElementById('refresh-invites').addEventListener('click', () => loadInvites().catch(err => showNotice(err.message)));
|
|
1467
|
-
document.getElementById('refresh-logs').addEventListener('click', () => loadLogs().catch(err => showNotice(err.message)));
|
|
1468
|
-
document.getElementById('refresh-log-stats').addEventListener('click', () => loadLogStats().catch(err => showNotice(err.message)));
|
|
1469
|
-
|
|
1540
|
+
function bindLogFilterRefresh() {
|
|
1470
1541
|
// Auto-refresh logs as filters change (debounced).
|
|
1471
1542
|
let debounce = null;
|
|
1472
1543
|
const schedule = () => {
|
|
@@ -1485,11 +1556,55 @@ function bindRefreshButtons() {
|
|
|
1485
1556
|
].forEach(id => {
|
|
1486
1557
|
const el = document.getElementById(id);
|
|
1487
1558
|
if (!el) return;
|
|
1559
|
+
// Shoelace components fire sl-input and sl-change events
|
|
1560
|
+
el.addEventListener('sl-input', schedule);
|
|
1561
|
+
el.addEventListener('sl-change', schedule);
|
|
1562
|
+
// Also listen for native events as fallback
|
|
1488
1563
|
el.addEventListener('input', schedule);
|
|
1489
1564
|
el.addEventListener('change', schedule);
|
|
1490
1565
|
});
|
|
1491
1566
|
}
|
|
1492
1567
|
|
|
1568
|
+
// --- Smart tab polling ---
|
|
1569
|
+
|
|
1570
|
+
let pollTimer = null;
|
|
1571
|
+
|
|
1572
|
+
function getActiveTab() {
|
|
1573
|
+
const tabGroup = document.getElementById('main-tabs');
|
|
1574
|
+
if (!tabGroup) return 'contacts';
|
|
1575
|
+
// Shoelace tab group: find the active tab by checking which tab has the active attribute
|
|
1576
|
+
const activeTab = tabGroup.querySelector('sl-tab[active]');
|
|
1577
|
+
return activeTab ? activeTab.getAttribute('panel') : 'contacts';
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const tabLoaders = {
|
|
1581
|
+
contacts: loadContacts,
|
|
1582
|
+
calls: loadCalls,
|
|
1583
|
+
logs: () => { loadLogs(); loadLogStats(); },
|
|
1584
|
+
settings: () => {},
|
|
1585
|
+
invites: loadInvites,
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1588
|
+
function startPolling() {
|
|
1589
|
+
stopPolling();
|
|
1590
|
+
pollTimer = setInterval(() => {
|
|
1591
|
+
const loader = tabLoaders[getActiveTab()];
|
|
1592
|
+
if (loader) loader().catch(() => {});
|
|
1593
|
+
}, 5000);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function stopPolling() {
|
|
1597
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function onTabSwitch(tabName) {
|
|
1601
|
+
const loader = tabLoaders[tabName];
|
|
1602
|
+
if (loader) {
|
|
1603
|
+
try { loader().catch(() => {}); } catch (_) {}
|
|
1604
|
+
}
|
|
1605
|
+
startPolling(); // reset the 5s timer
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1493
1608
|
async function bootstrap() {
|
|
1494
1609
|
bindTabs();
|
|
1495
1610
|
bindContactsActions();
|
|
@@ -1497,7 +1612,7 @@ async function bootstrap() {
|
|
|
1497
1612
|
bindCallbookActions();
|
|
1498
1613
|
bindAutoUpdateActions();
|
|
1499
1614
|
bindInviteActions();
|
|
1500
|
-
|
|
1615
|
+
bindLogFilterRefresh();
|
|
1501
1616
|
|
|
1502
1617
|
try {
|
|
1503
1618
|
await Promise.all([
|
|
@@ -1513,6 +1628,7 @@ async function bootstrap() {
|
|
|
1513
1628
|
]);
|
|
1514
1629
|
showNotice('Dashboard loaded');
|
|
1515
1630
|
connectRealtimeEvents();
|
|
1631
|
+
startPolling();
|
|
1516
1632
|
|
|
1517
1633
|
setInterval(() => {
|
|
1518
1634
|
loadAutoUpdateStatus().catch(() => {});
|