a2acalling 0.6.64 → 0.6.65
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 +2 -2
- package/CONVENTIONS.md +3 -0
- package/package.json +1 -1
- package/src/dashboard/public/app.js +205 -91
- package/src/dashboard/public/index.html +100 -71
- package/src/dashboard/public/style.css +14 -0
package/.a2a-manifest.json
CHANGED
package/CONVENTIONS.md
CHANGED
|
@@ -63,6 +63,9 @@ All modules use CommonJS (`require`/`module.exports`). Each lib file exports a f
|
|
|
63
63
|
- Uses Shoelace web components (`<sl-*>` elements)
|
|
64
64
|
- Communicates via fetch to `/dashboard/api/*` endpoints
|
|
65
65
|
- SSE for real-time updates via `src/lib/dashboard-events.js`
|
|
66
|
+
- Dark theme is the default; uses CSS custom properties for theming
|
|
67
|
+
- Sidebar navigation with tab switching (Contacts, Calls, Invites, Logs, Settings, Permissions, Health)
|
|
68
|
+
- Permissions tab uses tier cards with tool toggles and auto-save
|
|
66
69
|
|
|
67
70
|
## Permission Tiers
|
|
68
71
|
|
package/package.json
CHANGED
|
@@ -255,13 +255,15 @@ async function copyText(value) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// A2A-47: Panel name → section title mapping for the content header
|
|
258
|
+
// A2A-50: Added 'settings' panel for relocated admin settings
|
|
258
259
|
const panelTitles = {
|
|
259
260
|
contacts: 'Contacts',
|
|
260
261
|
calls: 'Calls',
|
|
261
262
|
permissions: 'Permissions',
|
|
262
263
|
invites: 'Invites',
|
|
263
264
|
logs: 'Logs',
|
|
264
|
-
health: 'Health'
|
|
265
|
+
health: 'Health',
|
|
266
|
+
settings: 'Settings'
|
|
265
267
|
};
|
|
266
268
|
|
|
267
269
|
// A2A-47: Show a specific panel and update sidebar + header state.
|
|
@@ -313,9 +315,8 @@ function bindTabs() {
|
|
|
313
315
|
// Deep-link support: activate the panel matching the URL hash
|
|
314
316
|
const activateFromHash = () => {
|
|
315
317
|
let hash = window.location.hash.slice(1);
|
|
316
|
-
// A2A-
|
|
317
|
-
//
|
|
318
|
-
if (hash === 'settings') hash = 'permissions';
|
|
318
|
+
// A2A-50: #settings now points to the real Settings panel (moved from
|
|
319
|
+
// being a backward-compat alias for #permissions in A2A-41).
|
|
319
320
|
if (hash) {
|
|
320
321
|
showPanel(hash);
|
|
321
322
|
}
|
|
@@ -1495,8 +1496,10 @@ function bindSidebarDrag() {
|
|
|
1495
1496
|
});
|
|
1496
1497
|
}
|
|
1497
1498
|
|
|
1498
|
-
// A2A-
|
|
1499
|
-
//
|
|
1499
|
+
// A2A-50: Drop handler for active topic/goal zones. Routes items to the
|
|
1500
|
+
// CORRECT zone based on data.type (not the zone they were dropped on).
|
|
1501
|
+
// This fixes the bug where dragging a topic onto the goals zone would
|
|
1502
|
+
// incorrectly add it as a goal and vice versa.
|
|
1500
1503
|
function handleZoneDrop(zone, e) {
|
|
1501
1504
|
e.preventDefault();
|
|
1502
1505
|
zone.classList.remove('drag-over');
|
|
@@ -1504,49 +1507,65 @@ function handleZoneDrop(zone, e) {
|
|
|
1504
1507
|
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
1505
1508
|
if (!data.name) return;
|
|
1506
1509
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
const
|
|
1510
|
+
// A2A-50: Route to correct zone by data.type, NOT by which zone received
|
|
1511
|
+
// the drop. Falls back to zone.id routing if data.type is missing (defensive).
|
|
1512
|
+
const itemType = data.type || (zone.id === 'active-topics-zone' ? 'topic' : 'goal');
|
|
1513
|
+
const targetZoneId = itemType === 'topic' ? 'active-topics-zone' : 'active-goals-zone';
|
|
1514
|
+
const targetZone = document.getElementById(targetZoneId);
|
|
1515
|
+
if (!targetZone) return;
|
|
1511
1516
|
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1517
|
+
const accentClass = itemType === 'topic' ? 'active-item-card--teal' : 'active-item-card--yellow';
|
|
1518
|
+
const typeLabel = itemType === 'topic' ? 'Topic' : 'Goal';
|
|
1519
|
+
const removeAttr = itemType === 'topic' ? 'data-remove-topic' : 'data-remove-goal';
|
|
1520
|
+
|
|
1521
|
+
// Check if already in target zone
|
|
1522
|
+
const existing = targetZone.querySelectorAll('.active-item-card');
|
|
1514
1523
|
for (const card of existing) {
|
|
1515
1524
|
if (card.dataset.topic === data.name) return; // already active
|
|
1516
1525
|
}
|
|
1517
1526
|
|
|
1518
|
-
//
|
|
1519
|
-
|
|
1527
|
+
// A2A-50: Shared helper builds the card HTML to avoid duplication with
|
|
1528
|
+
// the create-item-submit handler. Both paths now use buildItemCard().
|
|
1529
|
+
const card = buildItemCard(data.name, data.description || '', accentClass, typeLabel, removeAttr);
|
|
1530
|
+
const placeholder = targetZone.querySelector('.drop-placeholder');
|
|
1531
|
+
targetZone.insertBefore(card, placeholder);
|
|
1532
|
+
autoSaveTier();
|
|
1533
|
+
|
|
1534
|
+
// A2A-48: Re-fetch tier from state instead of using captured reference,
|
|
1535
|
+
// since autoSaveTier() may refresh state.settings asynchronously.
|
|
1536
|
+
setTimeout(() => {
|
|
1537
|
+
const freshTier = (state.settings?.tiers || []).find(t => t.id === state.activeTierId);
|
|
1538
|
+
if (freshTier) renderSidebarLists(freshTier);
|
|
1539
|
+
}, 300);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// A2A-50: Shared helper to build an active-item card DOM element.
|
|
1543
|
+
// Used by both handleZoneDrop() and the create-item-submit handler
|
|
1544
|
+
// to avoid duplicating card HTML in two places.
|
|
1545
|
+
function buildItemCard(name, description, accentClass, typeLabel, removeAttr) {
|
|
1520
1546
|
const card = document.createElement('div');
|
|
1521
1547
|
card.className = `active-item-card ${accentClass}`;
|
|
1522
|
-
card.dataset.topic =
|
|
1523
|
-
card.dataset.description =
|
|
1548
|
+
card.dataset.topic = name;
|
|
1549
|
+
card.dataset.description = description;
|
|
1524
1550
|
card.innerHTML = `
|
|
1525
1551
|
<div>
|
|
1526
|
-
<div class="item-name">${esc(
|
|
1552
|
+
<div class="item-name">${esc(name)}</div>
|
|
1527
1553
|
<div class="item-type-label">${typeLabel}</div>
|
|
1528
1554
|
</div>
|
|
1529
|
-
<button class="item-close-btn" ${removeAttr}="${esc(
|
|
1555
|
+
<button class="item-close-btn" ${removeAttr}="${esc(name)}">
|
|
1530
1556
|
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1531
1557
|
</button>
|
|
1532
1558
|
`;
|
|
1533
|
-
|
|
1534
|
-
autoSaveTier();
|
|
1535
|
-
|
|
1536
|
-
// A2A-48: Re-fetch tier from state instead of using captured reference,
|
|
1537
|
-
// since autoSaveTier() may refresh state.settings asynchronously.
|
|
1538
|
-
setTimeout(() => {
|
|
1539
|
-
const freshTier = (state.settings?.tiers || []).find(t => t.id === state.activeTierId);
|
|
1540
|
-
if (freshTier) renderSidebarLists(freshTier);
|
|
1541
|
-
}, 300);
|
|
1559
|
+
return card;
|
|
1542
1560
|
}
|
|
1543
1561
|
|
|
1544
|
-
// A2A-
|
|
1545
|
-
// #
|
|
1546
|
-
//
|
|
1562
|
+
// A2A-50: Populates tier select dropdowns: #invite-tier (Invites tab),
|
|
1563
|
+
// #new-tier-copy-from (Settings tab), and #new-tier-dialog-copy-from
|
|
1564
|
+
// (new tier modal in Permissions tab).
|
|
1547
1565
|
function populateInviteTierSelect() {
|
|
1548
1566
|
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1549
1567
|
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1568
|
+
const newTierDialogCopy = document.getElementById('new-tier-dialog-copy-from');
|
|
1550
1569
|
const inviteTier = document.getElementById('invite-tier');
|
|
1551
1570
|
|
|
1552
1571
|
const optionsHtml = tiers.map(tier => {
|
|
@@ -1556,6 +1575,8 @@ function populateInviteTierSelect() {
|
|
|
1556
1575
|
|
|
1557
1576
|
if (inviteTier) inviteTier.innerHTML = optionsHtml;
|
|
1558
1577
|
if (newTierCopy) newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1578
|
+
// A2A-50: Also populate the Copy From select inside the new-tier-dialog modal
|
|
1579
|
+
if (newTierDialogCopy) newTierDialogCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1559
1580
|
|
|
1560
1581
|
// A2A-48: Default invite tier to 'public'
|
|
1561
1582
|
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
@@ -1683,6 +1704,23 @@ function openCallerPreview() {
|
|
|
1683
1704
|
dialog.show();
|
|
1684
1705
|
}
|
|
1685
1706
|
|
|
1707
|
+
// A2A-50: Shows the delete confirmation dialog for a topic/goal card.
|
|
1708
|
+
// Uses a unique marker attribute on the card so the confirm handler can
|
|
1709
|
+
// find and remove it. This avoids storing DOM references in closure state.
|
|
1710
|
+
let _deleteIdCounter = 0;
|
|
1711
|
+
function showDeleteConfirm(itemName, itemType, cardElement) {
|
|
1712
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1713
|
+
if (!dialog) return;
|
|
1714
|
+
const label = itemType === 'topic' ? 'Topic' : 'Goal';
|
|
1715
|
+
const msgEl = document.getElementById('delete-confirm-message');
|
|
1716
|
+
if (msgEl) msgEl.textContent = `Remove ${label} "${itemName}" from this tier?`;
|
|
1717
|
+
// Tag the card with a unique ID so the confirm handler can find it
|
|
1718
|
+
const deleteId = `del-${++_deleteIdCounter}`;
|
|
1719
|
+
cardElement.setAttribute('data-delete-id', deleteId);
|
|
1720
|
+
dialog.dataset.deleteCardId = deleteId;
|
|
1721
|
+
dialog.show();
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1686
1724
|
// A2A-48: Binds all event handlers for the permissions panel. Replaces old
|
|
1687
1725
|
// bindSettingsActions() — removes handlers for deleted elements (tier-form,
|
|
1688
1726
|
// tier-select, copy-tier-btn, show-drag-columns, preview-caller-btn) and
|
|
@@ -1701,33 +1739,40 @@ function bindPermissionsActions() {
|
|
|
1701
1739
|
return;
|
|
1702
1740
|
}
|
|
1703
1741
|
|
|
1704
|
-
// A2A-
|
|
1742
|
+
// A2A-50: Close button on active topic cards opens delete confirmation dialog
|
|
1705
1743
|
const removeTopic = e.target.closest('[data-remove-topic]');
|
|
1706
1744
|
if (removeTopic) {
|
|
1707
1745
|
const card = removeTopic.closest('.active-item-card');
|
|
1708
|
-
if (card)
|
|
1709
|
-
|
|
1746
|
+
if (card) {
|
|
1747
|
+
const itemName = card.dataset.topic || '';
|
|
1748
|
+
showDeleteConfirm(itemName, 'topic', card);
|
|
1749
|
+
}
|
|
1710
1750
|
return;
|
|
1711
1751
|
}
|
|
1712
1752
|
|
|
1713
|
-
// A2A-
|
|
1753
|
+
// A2A-50: Close button on active goal cards opens delete confirmation dialog
|
|
1714
1754
|
const removeGoal = e.target.closest('[data-remove-goal]');
|
|
1715
1755
|
if (removeGoal) {
|
|
1716
1756
|
const card = removeGoal.closest('.active-item-card');
|
|
1717
|
-
if (card)
|
|
1718
|
-
|
|
1757
|
+
if (card) {
|
|
1758
|
+
const itemName = card.dataset.topic || '';
|
|
1759
|
+
showDeleteConfirm(itemName, 'goal', card);
|
|
1760
|
+
}
|
|
1719
1761
|
return;
|
|
1720
1762
|
}
|
|
1721
1763
|
|
|
1722
|
-
// A2A-
|
|
1764
|
+
// A2A-50: "+ New Tier" button opens glass-styled sl-dialog instead of
|
|
1765
|
+
// scrolling to inline form. Keeps focus trap and accessibility from Shoelace.
|
|
1723
1766
|
const newTierBtn = e.target.closest('#perm-new-tier-btn');
|
|
1724
1767
|
if (newTierBtn) {
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
if (
|
|
1730
|
-
|
|
1768
|
+
const dialog = document.getElementById('new-tier-dialog');
|
|
1769
|
+
if (dialog) {
|
|
1770
|
+
const idInput = document.getElementById('new-tier-dialog-id');
|
|
1771
|
+
const nameInput = document.getElementById('new-tier-dialog-name');
|
|
1772
|
+
if (idInput) idInput.value = '';
|
|
1773
|
+
if (nameInput) nameInput.value = '';
|
|
1774
|
+
dialog.show();
|
|
1775
|
+
}
|
|
1731
1776
|
return;
|
|
1732
1777
|
}
|
|
1733
1778
|
|
|
@@ -1775,7 +1820,8 @@ function bindPermissionsActions() {
|
|
|
1775
1820
|
}
|
|
1776
1821
|
});
|
|
1777
1822
|
|
|
1778
|
-
// A2A-
|
|
1823
|
+
// A2A-50: Create Item dialog — submit handler. Uses shared buildItemCard()
|
|
1824
|
+
// helper to avoid duplicating card HTML with handleZoneDrop().
|
|
1779
1825
|
document.getElementById('create-item-submit')?.addEventListener('click', () => {
|
|
1780
1826
|
const dialog = document.getElementById('create-item-dialog');
|
|
1781
1827
|
const titleInput = document.getElementById('create-item-title');
|
|
@@ -1787,7 +1833,6 @@ function bindPermissionsActions() {
|
|
|
1787
1833
|
const desc = descInput?.value?.trim() || '';
|
|
1788
1834
|
const type = dialog.dataset.createType || 'topic';
|
|
1789
1835
|
|
|
1790
|
-
// A2A-48: Add item to the appropriate active zone
|
|
1791
1836
|
const zoneId = type === 'topic' ? 'active-topics-zone' : 'active-goals-zone';
|
|
1792
1837
|
const zone = document.getElementById(zoneId);
|
|
1793
1838
|
if (!zone) return;
|
|
@@ -1796,20 +1841,8 @@ function bindPermissionsActions() {
|
|
|
1796
1841
|
const typeLabel = type === 'topic' ? 'Topic' : 'Goal';
|
|
1797
1842
|
const removeAttr = type === 'topic' ? 'data-remove-topic' : 'data-remove-goal';
|
|
1798
1843
|
|
|
1844
|
+
const card = buildItemCard(title, desc, accentClass, typeLabel, removeAttr);
|
|
1799
1845
|
const placeholder = zone.querySelector('.drop-placeholder');
|
|
1800
|
-
const card = document.createElement('div');
|
|
1801
|
-
card.className = `active-item-card ${accentClass}`;
|
|
1802
|
-
card.dataset.topic = title;
|
|
1803
|
-
card.dataset.description = desc;
|
|
1804
|
-
card.innerHTML = `
|
|
1805
|
-
<div>
|
|
1806
|
-
<div class="item-name">${esc(title)}</div>
|
|
1807
|
-
<div class="item-type-label">${typeLabel}</div>
|
|
1808
|
-
</div>
|
|
1809
|
-
<button class="item-close-btn" ${removeAttr}="${esc(title)}">
|
|
1810
|
-
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1811
|
-
</button>
|
|
1812
|
-
`;
|
|
1813
1846
|
if (placeholder) zone.insertBefore(card, placeholder);
|
|
1814
1847
|
else zone.appendChild(card);
|
|
1815
1848
|
|
|
@@ -1822,41 +1855,69 @@ function bindPermissionsActions() {
|
|
|
1822
1855
|
document.getElementById('create-item-dialog')?.hide();
|
|
1823
1856
|
});
|
|
1824
1857
|
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1858
|
+
// A2A-50: New Tier dialog — submit handler (replaces inline form handler).
|
|
1859
|
+
// Creates tier via POST and switches to it.
|
|
1860
|
+
document.getElementById('new-tier-dialog-submit')?.addEventListener('click', async () => {
|
|
1861
|
+
const tierId = document.getElementById('new-tier-dialog-id')?.value?.trim();
|
|
1862
|
+
const name = document.getElementById('new-tier-dialog-name')?.value?.trim();
|
|
1863
|
+
const copyFrom = document.getElementById('new-tier-dialog-copy-from')?.value;
|
|
1864
|
+
if (!tierId) {
|
|
1865
|
+
document.getElementById('new-tier-dialog-id')?.focus();
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
await request('/settings/tiers', {
|
|
1870
|
+
method: 'POST',
|
|
1871
|
+
body: JSON.stringify({
|
|
1872
|
+
id: tierId,
|
|
1873
|
+
name: name || tierId,
|
|
1874
|
+
copy_from: copyFrom || undefined
|
|
1875
|
+
})
|
|
1876
|
+
});
|
|
1877
|
+
showNotice(`Created tier "${tierId}"`);
|
|
1878
|
+
document.getElementById('new-tier-dialog')?.hide();
|
|
1879
|
+
await loadSettings();
|
|
1880
|
+
state.activeTierId = tierId;
|
|
1881
|
+
renderPermissions();
|
|
1882
|
+
} catch (err) {
|
|
1883
|
+
showNotice(err.message);
|
|
1884
|
+
}
|
|
1837
1885
|
});
|
|
1838
1886
|
|
|
1839
|
-
// A2A-
|
|
1840
|
-
document.getElementById('new-tier-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1887
|
+
// A2A-50: New Tier dialog — cancel handler
|
|
1888
|
+
document.getElementById('new-tier-dialog-cancel')?.addEventListener('click', () => {
|
|
1889
|
+
document.getElementById('new-tier-dialog')?.hide();
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
// A2A-50: Delete confirm dialog — confirm handler. Removes the card that
|
|
1893
|
+
// was stored in dataset and triggers auto-save.
|
|
1894
|
+
document.getElementById('delete-confirm-yes')?.addEventListener('click', () => {
|
|
1895
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1896
|
+
if (!dialog) return;
|
|
1897
|
+
const cardId = dialog.dataset.deleteCardId;
|
|
1898
|
+
if (cardId) {
|
|
1899
|
+
const card = document.querySelector(`.active-item-card[data-delete-id="${cardId}"]`);
|
|
1900
|
+
if (card) {
|
|
1901
|
+
card.removeAttribute('data-delete-id');
|
|
1902
|
+
card.remove();
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
dialog.hide();
|
|
1906
|
+
autoSaveTier();
|
|
1907
|
+
});
|
|
1908
|
+
|
|
1909
|
+
// A2A-50: Delete confirm dialog — cancel handler
|
|
1910
|
+
document.getElementById('delete-confirm-no')?.addEventListener('click', () => {
|
|
1911
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1912
|
+
if (dialog) {
|
|
1913
|
+
// Clean up the marker attribute from the card
|
|
1914
|
+
const cardId = dialog.dataset.deleteCardId;
|
|
1915
|
+
if (cardId) {
|
|
1916
|
+
const card = document.querySelector(`.active-item-card[data-delete-id="${cardId}"]`);
|
|
1917
|
+
if (card) card.removeAttribute('data-delete-id');
|
|
1918
|
+
}
|
|
1919
|
+
dialog.hide();
|
|
1920
|
+
}
|
|
1860
1921
|
});
|
|
1861
1922
|
|
|
1862
1923
|
// Preview dialog close — unchanged
|
|
@@ -2243,6 +2304,8 @@ const tabLoaders = {
|
|
|
2243
2304
|
permissions: loadSettings,
|
|
2244
2305
|
invites: loadInvites,
|
|
2245
2306
|
health: loadHealth,
|
|
2307
|
+
// A2A-50: Settings tab loads dashboard status, auto-update, and callbook data
|
|
2308
|
+
settings: () => { loadDashboardStatus(); loadAutoUpdateStatus(); loadCallbookDevices(); },
|
|
2246
2309
|
};
|
|
2247
2310
|
|
|
2248
2311
|
function startPolling() {
|
|
@@ -2361,6 +2424,55 @@ function renderHealthHistory(history) {
|
|
|
2361
2424
|
}).join('');
|
|
2362
2425
|
}
|
|
2363
2426
|
|
|
2427
|
+
// A2A-50: Binds handlers for the Settings panel (defaults form, new-tier form).
|
|
2428
|
+
// These were previously inside bindPermissionsActions() but are now in the
|
|
2429
|
+
// separate #panel-settings panel.
|
|
2430
|
+
function bindSettingsActions() {
|
|
2431
|
+
// Defaults form
|
|
2432
|
+
document.getElementById('defaults-form')?.addEventListener('submit', async (e) => {
|
|
2433
|
+
e.preventDefault();
|
|
2434
|
+
try {
|
|
2435
|
+
await request('/settings/defaults', {
|
|
2436
|
+
method: 'PUT',
|
|
2437
|
+
body: JSON.stringify({
|
|
2438
|
+
expiration: document.getElementById('defaults-expiration').value,
|
|
2439
|
+
maxCalls: Number.parseInt(document.getElementById('defaults-max-calls').value, 10) || 100
|
|
2440
|
+
})
|
|
2441
|
+
});
|
|
2442
|
+
showNotice('Saved defaults');
|
|
2443
|
+
await loadSettings();
|
|
2444
|
+
} catch (err) {
|
|
2445
|
+
showNotice(err.message);
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
// New Tier form (inline form in Settings tab, kept for non-modal access)
|
|
2450
|
+
document.getElementById('new-tier-form')?.addEventListener('submit', async (e) => {
|
|
2451
|
+
e.preventDefault();
|
|
2452
|
+
const tierId = document.getElementById('new-tier-id')?.value?.trim();
|
|
2453
|
+
const name = document.getElementById('new-tier-name')?.value?.trim();
|
|
2454
|
+
const copyFrom = document.getElementById('new-tier-copy-from')?.value;
|
|
2455
|
+
if (!tierId) return;
|
|
2456
|
+
try {
|
|
2457
|
+
await request('/settings/tiers', {
|
|
2458
|
+
method: 'POST',
|
|
2459
|
+
body: JSON.stringify({
|
|
2460
|
+
id: tierId,
|
|
2461
|
+
name: name || tierId,
|
|
2462
|
+
copy_from: copyFrom || undefined
|
|
2463
|
+
})
|
|
2464
|
+
});
|
|
2465
|
+
showNotice(`Created tier "${tierId}"`);
|
|
2466
|
+
document.getElementById('new-tier-form')?.reset();
|
|
2467
|
+
await loadSettings();
|
|
2468
|
+
state.activeTierId = tierId;
|
|
2469
|
+
renderPermissions();
|
|
2470
|
+
} catch (err) {
|
|
2471
|
+
showNotice(err.message);
|
|
2472
|
+
}
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2364
2476
|
async function bootstrap() {
|
|
2365
2477
|
bindTabs();
|
|
2366
2478
|
bindContactsActions();
|
|
@@ -2368,6 +2480,8 @@ async function bootstrap() {
|
|
|
2368
2480
|
// bindItemListDelegation(). All tier/tool/topic/goal handlers are now
|
|
2369
2481
|
// inside bindPermissionsActions() using event delegation on #panel-permissions.
|
|
2370
2482
|
bindPermissionsActions();
|
|
2483
|
+
// A2A-50: Settings panel handlers (defaults, new-tier inline form)
|
|
2484
|
+
bindSettingsActions();
|
|
2371
2485
|
bindCallbookActions();
|
|
2372
2486
|
bindAutoUpdateActions();
|
|
2373
2487
|
bindInviteActions();
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
<span class="material-symbols-outlined nav-icon" style="color:#EF4444;">monitor_heart</span>
|
|
46
46
|
<span class="nav-label">Health</span>
|
|
47
47
|
</a>
|
|
48
|
+
<!-- A2A-50: Settings nav item for relocated admin settings -->
|
|
49
|
+
<a data-panel="settings" class="nav-item">
|
|
50
|
+
<span class="material-symbols-outlined nav-icon" style="color:#6B7280;">settings</span>
|
|
51
|
+
<span class="nav-label">Settings</span>
|
|
52
|
+
</a>
|
|
48
53
|
</nav>
|
|
49
54
|
</aside>
|
|
50
55
|
|
|
@@ -147,77 +152,6 @@
|
|
|
147
152
|
|
|
148
153
|
<!-- Tier Warnings -->
|
|
149
154
|
<div id="tier-warnings" class="tier-warnings"></div>
|
|
150
|
-
|
|
151
|
-
<!-- Settings & Administration (collapsed) -->
|
|
152
|
-
<sl-details summary="Settings & Administration">
|
|
153
|
-
<h3>Defaults</h3>
|
|
154
|
-
<form id="defaults-form">
|
|
155
|
-
<sl-input id="defaults-expiration" label="Expiration" placeholder="7d"></sl-input>
|
|
156
|
-
<sl-input id="defaults-max-calls" label="Max Calls" type="number" min="1"></sl-input>
|
|
157
|
-
<div class="row">
|
|
158
|
-
<sl-button type="submit" variant="primary">Save Defaults</sl-button>
|
|
159
|
-
</div>
|
|
160
|
-
</form>
|
|
161
|
-
|
|
162
|
-
<h3>New Tier</h3>
|
|
163
|
-
<form id="new-tier-form">
|
|
164
|
-
<sl-input id="new-tier-id" label="Tier ID" placeholder="partners"></sl-input>
|
|
165
|
-
<sl-input id="new-tier-name" label="Name" placeholder="Partners"></sl-input>
|
|
166
|
-
<label>Copy from
|
|
167
|
-
<sl-select id="new-tier-copy-from" size="small">
|
|
168
|
-
<sl-option value="">None</sl-option>
|
|
169
|
-
</sl-select>
|
|
170
|
-
</label>
|
|
171
|
-
<div class="row">
|
|
172
|
-
<sl-button type="submit" variant="primary">Create Tier</sl-button>
|
|
173
|
-
</div>
|
|
174
|
-
</form>
|
|
175
|
-
|
|
176
|
-
<h3>Remote Callbook</h3>
|
|
177
|
-
<sl-card id="callbook-status"></sl-card>
|
|
178
|
-
|
|
179
|
-
<h3>Auto Update</h3>
|
|
180
|
-
<sl-card id="auto-update-status">Loading...</sl-card>
|
|
181
|
-
<div class="row">
|
|
182
|
-
<sl-button id="auto-update-check" size="small">Check now</sl-button>
|
|
183
|
-
<sl-button id="auto-update-now" size="small">Update now</sl-button>
|
|
184
|
-
<sl-button id="auto-update-toggle" size="small">Disable auto-update</sl-button>
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<sl-card>
|
|
188
|
-
<form id="callbook-provision-form">
|
|
189
|
-
<div class="row">
|
|
190
|
-
<sl-button type="submit" variant="primary" size="small">Create Install Link (24h)</sl-button>
|
|
191
|
-
<sl-button id="callbook-logout" size="small" variant="default">Logout This Browser</sl-button>
|
|
192
|
-
</div>
|
|
193
|
-
<sl-input id="callbook-label" label="Device label" value="Callbook Remote"></sl-input>
|
|
194
|
-
<sl-textarea id="callbook-install-url" label="Install URL" rows="3" readonly></sl-textarea>
|
|
195
|
-
<div class="row">
|
|
196
|
-
<sl-button id="callbook-copy-url" size="small">Copy Link</sl-button>
|
|
197
|
-
</div>
|
|
198
|
-
<div id="callbook-warnings" class="mono"></div>
|
|
199
|
-
</form>
|
|
200
|
-
</sl-card>
|
|
201
|
-
|
|
202
|
-
<sl-card>
|
|
203
|
-
<div class="row">
|
|
204
|
-
<strong>Paired Devices</strong>
|
|
205
|
-
</div>
|
|
206
|
-
<table id="callbook-devices-table">
|
|
207
|
-
<thead>
|
|
208
|
-
<tr>
|
|
209
|
-
<th>Label</th>
|
|
210
|
-
<th>Created</th>
|
|
211
|
-
<th>Last Used</th>
|
|
212
|
-
<th>Sessions</th>
|
|
213
|
-
<th>Revoked</th>
|
|
214
|
-
<th>Action</th>
|
|
215
|
-
</tr>
|
|
216
|
-
</thead>
|
|
217
|
-
<tbody></tbody>
|
|
218
|
-
</table>
|
|
219
|
-
</sl-card>
|
|
220
|
-
</sl-details>
|
|
221
155
|
</div>
|
|
222
156
|
|
|
223
157
|
<!-- Right sidebar: preview + topic/goal lists -->
|
|
@@ -243,6 +177,30 @@
|
|
|
243
177
|
<div id="preview-content"></div>
|
|
244
178
|
<sl-button slot="footer" variant="primary" id="preview-close-btn">Close</sl-button>
|
|
245
179
|
</sl-dialog>
|
|
180
|
+
|
|
181
|
+
<!-- A2A-50: Delete confirmation dialog for topics/goals -->
|
|
182
|
+
<sl-dialog id="delete-confirm-dialog" label="Confirm Removal" style="--width: 400px;">
|
|
183
|
+
<p id="delete-confirm-message">Remove this item from the tier?</p>
|
|
184
|
+
<div slot="footer" class="row">
|
|
185
|
+
<sl-button id="delete-confirm-no" variant="default">Cancel</sl-button>
|
|
186
|
+
<sl-button id="delete-confirm-yes" variant="danger">Remove</sl-button>
|
|
187
|
+
</div>
|
|
188
|
+
</sl-dialog>
|
|
189
|
+
|
|
190
|
+
<!-- A2A-50: New Tier dialog (glass-styled modal replacing inline form scroll) -->
|
|
191
|
+
<sl-dialog id="new-tier-dialog" label="Create New Tier" style="--width: 440px;">
|
|
192
|
+
<sl-input id="new-tier-dialog-id" label="Tier ID" placeholder="partners" required></sl-input>
|
|
193
|
+
<sl-input id="new-tier-dialog-name" label="Name" placeholder="Partners"></sl-input>
|
|
194
|
+
<label>Copy from
|
|
195
|
+
<sl-select id="new-tier-dialog-copy-from" size="small">
|
|
196
|
+
<sl-option value="">None</sl-option>
|
|
197
|
+
</sl-select>
|
|
198
|
+
</label>
|
|
199
|
+
<div slot="footer" class="row">
|
|
200
|
+
<sl-button id="new-tier-dialog-cancel" variant="default">Cancel</sl-button>
|
|
201
|
+
<sl-button id="new-tier-dialog-submit" variant="primary">Create Tier</sl-button>
|
|
202
|
+
</div>
|
|
203
|
+
</sl-dialog>
|
|
246
204
|
</div>
|
|
247
205
|
|
|
248
206
|
<div id="panel-invites" class="panel">
|
|
@@ -345,6 +303,77 @@
|
|
|
345
303
|
<tbody></tbody>
|
|
346
304
|
</table>
|
|
347
305
|
</div>
|
|
306
|
+
|
|
307
|
+
<!-- A2A-50: Settings panel — relocated from sl-details in panel-permissions -->
|
|
308
|
+
<div id="panel-settings" class="panel">
|
|
309
|
+
<h3>Defaults</h3>
|
|
310
|
+
<form id="defaults-form">
|
|
311
|
+
<sl-input id="defaults-expiration" label="Expiration" placeholder="7d"></sl-input>
|
|
312
|
+
<sl-input id="defaults-max-calls" label="Max Calls" type="number" min="1"></sl-input>
|
|
313
|
+
<div class="row">
|
|
314
|
+
<sl-button type="submit" variant="primary">Save Defaults</sl-button>
|
|
315
|
+
</div>
|
|
316
|
+
</form>
|
|
317
|
+
|
|
318
|
+
<h3>New Tier</h3>
|
|
319
|
+
<form id="new-tier-form">
|
|
320
|
+
<sl-input id="new-tier-id" label="Tier ID" placeholder="partners"></sl-input>
|
|
321
|
+
<sl-input id="new-tier-name" label="Name" placeholder="Partners"></sl-input>
|
|
322
|
+
<label>Copy from
|
|
323
|
+
<sl-select id="new-tier-copy-from" size="small">
|
|
324
|
+
<sl-option value="">None</sl-option>
|
|
325
|
+
</sl-select>
|
|
326
|
+
</label>
|
|
327
|
+
<div class="row">
|
|
328
|
+
<sl-button type="submit" variant="primary">Create Tier</sl-button>
|
|
329
|
+
</div>
|
|
330
|
+
</form>
|
|
331
|
+
|
|
332
|
+
<h3>Remote Callbook</h3>
|
|
333
|
+
<sl-card id="callbook-status"></sl-card>
|
|
334
|
+
|
|
335
|
+
<h3>Auto Update</h3>
|
|
336
|
+
<sl-card id="auto-update-status">Loading...</sl-card>
|
|
337
|
+
<div class="row">
|
|
338
|
+
<sl-button id="auto-update-check" size="small">Check now</sl-button>
|
|
339
|
+
<sl-button id="auto-update-now" size="small">Update now</sl-button>
|
|
340
|
+
<sl-button id="auto-update-toggle" size="small">Disable auto-update</sl-button>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<sl-card>
|
|
344
|
+
<form id="callbook-provision-form">
|
|
345
|
+
<div class="row">
|
|
346
|
+
<sl-button type="submit" variant="primary" size="small">Create Install Link (24h)</sl-button>
|
|
347
|
+
<sl-button id="callbook-logout" size="small" variant="default">Logout This Browser</sl-button>
|
|
348
|
+
</div>
|
|
349
|
+
<sl-input id="callbook-label" label="Device label" value="Callbook Remote"></sl-input>
|
|
350
|
+
<sl-textarea id="callbook-install-url" label="Install URL" rows="3" readonly></sl-textarea>
|
|
351
|
+
<div class="row">
|
|
352
|
+
<sl-button id="callbook-copy-url" size="small">Copy Link</sl-button>
|
|
353
|
+
</div>
|
|
354
|
+
<div id="callbook-warnings" class="mono"></div>
|
|
355
|
+
</form>
|
|
356
|
+
</sl-card>
|
|
357
|
+
|
|
358
|
+
<sl-card>
|
|
359
|
+
<div class="row">
|
|
360
|
+
<strong>Paired Devices</strong>
|
|
361
|
+
</div>
|
|
362
|
+
<table id="callbook-devices-table">
|
|
363
|
+
<thead>
|
|
364
|
+
<tr>
|
|
365
|
+
<th>Label</th>
|
|
366
|
+
<th>Created</th>
|
|
367
|
+
<th>Last Used</th>
|
|
368
|
+
<th>Sessions</th>
|
|
369
|
+
<th>Revoked</th>
|
|
370
|
+
<th>Action</th>
|
|
371
|
+
</tr>
|
|
372
|
+
</thead>
|
|
373
|
+
<tbody></tbody>
|
|
374
|
+
</table>
|
|
375
|
+
</sl-card>
|
|
376
|
+
</div>
|
|
348
377
|
</div>
|
|
349
378
|
</main>
|
|
350
379
|
|
|
@@ -1194,6 +1194,20 @@ sl-details::part(base) {
|
|
|
1194
1194
|
color: var(--ink);
|
|
1195
1195
|
}
|
|
1196
1196
|
|
|
1197
|
+
/* A2A-50: Glass styling for sl-dialog panels in the Permissions tab.
|
|
1198
|
+
Uses ::part(panel) to style the shadow DOM of Shoelace dialogs while
|
|
1199
|
+
preserving accessibility features (focus trap, ESC, ARIA). The codebase
|
|
1200
|
+
already uses ::part(base) on sl-card (line 205) and sl-details (line 1152). */
|
|
1201
|
+
#create-item-dialog::part(panel),
|
|
1202
|
+
#preview-dialog::part(panel),
|
|
1203
|
+
#delete-confirm-dialog::part(panel),
|
|
1204
|
+
#new-tier-dialog::part(panel) {
|
|
1205
|
+
background: rgba(30, 41, 59, 0.85);
|
|
1206
|
+
backdrop-filter: blur(16px);
|
|
1207
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
1208
|
+
border-radius: 16px;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1197
1211
|
/* ── A2A-48: Hide permissions sidebar below 1280px ─────────── */
|
|
1198
1212
|
@media (max-width: 1280px) {
|
|
1199
1213
|
.perm-sidebar {
|