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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.6.64",
3
- "installed_at": "2026-02-21T20:17:36.374Z",
2
+ "version": "0.6.65",
3
+ "installed_at": "2026-02-25T07:27:45.973Z",
4
4
  "files": [
5
5
  {
6
6
  "path": "CLAUDE.md",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.64",
3
+ "version": "0.6.65",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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-41: backward-compat alias old bookmarks/links using #settings
317
- // still work after rename to #permissions
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-48: Drop handler for active topic/goal zones. Extracted from
1499
- // bindSidebarDrag() to be called once in bindPermissionsActions().
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
- const isTopicZone = zone.id === 'active-topics-zone';
1508
- const accentClass = isTopicZone ? 'active-item-card--teal' : 'active-item-card--yellow';
1509
- const typeLabel = isTopicZone ? 'Topic' : 'Goal';
1510
- const removeAttr = isTopicZone ? 'data-remove-topic' : 'data-remove-goal';
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
- // Check if already in zone
1513
- const existing = zone.querySelectorAll('.active-item-card');
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
- // Insert before the placeholder
1519
- const placeholder = zone.querySelector('.drop-placeholder');
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 = data.name;
1523
- card.dataset.description = data.description || '';
1548
+ card.dataset.topic = name;
1549
+ card.dataset.description = description;
1524
1550
  card.innerHTML = `
1525
1551
  <div>
1526
- <div class="item-name">${esc(data.name)}</div>
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(data.name)}">
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
- zone.insertBefore(card, placeholder);
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-48: Extracted from old fillTierSelects(). Populates only the
1545
- // #invite-tier (Invites tab) and #new-tier-copy-from (Settings details).
1546
- // Does NOT populate the removed #tier-select or #copy-from-tier.
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-48: Close button on active topic cards
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) card.remove();
1709
- autoSaveTier();
1746
+ if (card) {
1747
+ const itemName = card.dataset.topic || '';
1748
+ showDeleteConfirm(itemName, 'topic', card);
1749
+ }
1710
1750
  return;
1711
1751
  }
1712
1752
 
1713
- // A2A-48: Close button on active goal cards
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) card.remove();
1718
- autoSaveTier();
1757
+ if (card) {
1758
+ const itemName = card.dataset.topic || '';
1759
+ showDeleteConfirm(itemName, 'goal', card);
1760
+ }
1719
1761
  return;
1720
1762
  }
1721
1763
 
1722
- // A2A-48: "+ New Tier" button scrolls to the new-tier form inside Settings details
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 details = panel.querySelector('sl-details');
1726
- if (details) details.open = true;
1727
- setTimeout(() => {
1728
- const el = document.getElementById('new-tier-id');
1729
- if (el) { el.scrollIntoView({ behavior: 'smooth' }); el.focus(); }
1730
- }, 200);
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-48: Create Item dialog — submit handler
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
- // Defaults formunchanged from A2A-41
1826
- document.getElementById('defaults-form')?.addEventListener('submit', async (e) => {
1827
- e.preventDefault();
1828
- await request('/settings/defaults', {
1829
- method: 'PUT',
1830
- body: JSON.stringify({
1831
- expiration: document.getElementById('defaults-expiration').value,
1832
- maxCalls: Number.parseInt(document.getElementById('defaults-max-calls').value, 10) || 100
1833
- })
1834
- });
1835
- showNotice('Saved defaults');
1836
- await loadSettings();
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-48: New Tier formuses state.activeTierId instead of removed #tier-select
1840
- document.getElementById('new-tier-form')?.addEventListener('submit', async (e) => {
1841
- e.preventDefault();
1842
- const tierId = document.getElementById('new-tier-id').value.trim();
1843
- const name = document.getElementById('new-tier-name').value.trim();
1844
- const copyFrom = document.getElementById('new-tier-copy-from').value;
1845
- if (!tierId) return;
1846
- await request('/settings/tiers', {
1847
- method: 'POST',
1848
- body: JSON.stringify({
1849
- id: tierId,
1850
- name: name || tierId,
1851
- copy_from: copyFrom || undefined
1852
- })
1853
- });
1854
- showNotice(`Created tier "${tierId}"`);
1855
- document.getElementById('new-tier-form').reset();
1856
- await loadSettings();
1857
- // A2A-48: Switch to the newly created tier (replaces old tier-select.value = tierId)
1858
- state.activeTierId = tierId;
1859
- renderPermissions();
1887
+ // A2A-50: New Tier dialogcancel 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 &amp; 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 {