a2acalling 0.6.64 ā 0.6.66
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/bin/cli.js +77 -9
- package/package.json +1 -1
- package/src/dashboard/public/app.js +220 -96
- package/src/dashboard/public/index.html +102 -71
- package/src/dashboard/public/style.css +33 -0
- package/src/lib/client.js +29 -4
- package/src/lib/config.js +22 -3
- package/src/lib/conversation-driver.js +7 -1
- package/src/lib/crypto.js +113 -0
- package/src/lib/tokens.js +4 -1
- package/src/routes/a2a.js +78 -4
- package/src/routes/dashboard.js +1 -1
- package/src/server.js +3 -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/bin/cli.js
CHANGED
|
@@ -1103,17 +1103,18 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1103
1103
|
console.log('Legend: š public š§ friends ā” family');
|
|
1104
1104
|
},
|
|
1105
1105
|
|
|
1106
|
-
'contacts:add': (args) => {
|
|
1106
|
+
'contacts:add': async (args) => {
|
|
1107
1107
|
const url = args._[2];
|
|
1108
1108
|
if (!url) {
|
|
1109
1109
|
console.error('Usage: a2a contacts add <invite_url> [options]');
|
|
1110
1110
|
console.error('Options:');
|
|
1111
|
-
console.error(' --name, -n
|
|
1112
|
-
console.error(' --owner, -o
|
|
1113
|
-
console.error(' --server-name
|
|
1114
|
-
console.error(' --notes
|
|
1115
|
-
console.error(' --tags
|
|
1116
|
-
console.error(' --link
|
|
1111
|
+
console.error(' --name, -n Agent name');
|
|
1112
|
+
console.error(' --owner, -o Owner name');
|
|
1113
|
+
console.error(' --server-name Server label (optional)');
|
|
1114
|
+
console.error(' --notes Notes about this contact');
|
|
1115
|
+
console.error(' --tags Comma-separated tags');
|
|
1116
|
+
console.error(' --link Link to token ID you gave them');
|
|
1117
|
+
console.error(' --public-key Ed25519 public key (base64, or "fetch" to get from /status)');
|
|
1117
1118
|
process.exit(1);
|
|
1118
1119
|
}
|
|
1119
1120
|
|
|
@@ -1126,6 +1127,24 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1126
1127
|
linkedTokenId: args.flags.link || null
|
|
1127
1128
|
};
|
|
1128
1129
|
|
|
1130
|
+
// A2A-52: fetch or accept public key for identity verification
|
|
1131
|
+
const pubKeyFlag = args.flags['public-key'] || args.flags.public_key || args.flags.publicKey;
|
|
1132
|
+
if (pubKeyFlag === 'fetch' || pubKeyFlag === true) {
|
|
1133
|
+
try {
|
|
1134
|
+
const client = new A2AClient({});
|
|
1135
|
+
const statusResult = await client.status(url);
|
|
1136
|
+
if (statusResult.public_key) {
|
|
1137
|
+
options.public_key = statusResult.public_key;
|
|
1138
|
+
const { fingerprint: fpFunc } = require('../src/lib/crypto');
|
|
1139
|
+
console.log(` Fetched public key: ${fpFunc(statusResult.public_key)}`);
|
|
1140
|
+
}
|
|
1141
|
+
} catch (fetchErr) {
|
|
1142
|
+
console.error(` Warning: could not fetch public key from /status: ${fetchErr.message}`);
|
|
1143
|
+
}
|
|
1144
|
+
} else if (pubKeyFlag && typeof pubKeyFlag === 'string') {
|
|
1145
|
+
options.public_key = pubKeyFlag;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1129
1148
|
try {
|
|
1130
1149
|
const result = store.addContact(url, options);
|
|
1131
1150
|
if (!result.success) {
|
|
@@ -1186,13 +1205,22 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1186
1205
|
console.log(`š No linked token (you haven't given them access yet)`);
|
|
1187
1206
|
}
|
|
1188
1207
|
|
|
1208
|
+
// A2A-52: show cryptographic identity verification status
|
|
1209
|
+
if (remote.public_key) {
|
|
1210
|
+
const { fingerprint: fpFunc } = require('../src/lib/crypto');
|
|
1211
|
+
console.log(`š Identity: verified`);
|
|
1212
|
+
console.log(` Fingerprint: ${fpFunc(remote.public_key)}`);
|
|
1213
|
+
} else {
|
|
1214
|
+
console.log(`š Identity: unverified (no public key pinned)`);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1189
1217
|
if (remote.tags && remote.tags.length > 0) {
|
|
1190
1218
|
console.log(`š·ļø Tags: ${remote.tags.join(', ')}`);
|
|
1191
1219
|
}
|
|
1192
1220
|
if (remote.notes) {
|
|
1193
1221
|
console.log(`š Notes: ${remote.notes}`);
|
|
1194
1222
|
}
|
|
1195
|
-
|
|
1223
|
+
|
|
1196
1224
|
console.log(`\nš
Added: ${new Date(remote.added_at).toLocaleDateString()}`);
|
|
1197
1225
|
if (remote.last_seen) {
|
|
1198
1226
|
console.log(`š Last seen: ${formatTimeAgo(new Date(remote.last_seen))}`);
|
|
@@ -1300,6 +1328,23 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1300
1328
|
console.log(`š¢ ${remote.name} is online`);
|
|
1301
1329
|
console.log(` Agent: ${result.name}`);
|
|
1302
1330
|
console.log(` Version: ${result.version}`);
|
|
1331
|
+
|
|
1332
|
+
// A2A-52: also fetch /status to refresh public key
|
|
1333
|
+
try {
|
|
1334
|
+
const statusResult = await client.status(url);
|
|
1335
|
+
if (statusResult.public_key) {
|
|
1336
|
+
const { fingerprint: fpFunc } = require('../src/lib/crypto');
|
|
1337
|
+
if (remote.public_key && remote.public_key !== statusResult.public_key) {
|
|
1338
|
+
console.log(` ā ļø Public key changed!`);
|
|
1339
|
+
console.log(` Old: ${fpFunc(remote.public_key)}`);
|
|
1340
|
+
console.log(` New: ${fpFunc(statusResult.public_key)}`);
|
|
1341
|
+
}
|
|
1342
|
+
store.updateContact(name, { public_key: statusResult.public_key });
|
|
1343
|
+
console.log(` š Fingerprint: ${fpFunc(statusResult.public_key)}`);
|
|
1344
|
+
}
|
|
1345
|
+
} catch (_) {
|
|
1346
|
+
// /status fetch is best-effort during ping
|
|
1347
|
+
}
|
|
1303
1348
|
} catch (err) {
|
|
1304
1349
|
store.updateContactStatus(name, 'offline', err.message);
|
|
1305
1350
|
console.log(`š“ ${remote.name} is offline`);
|
|
@@ -1535,6 +1580,8 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1535
1580
|
// Best effort
|
|
1536
1581
|
}
|
|
1537
1582
|
|
|
1583
|
+
// A2A-52: load keypair for request signing in multi-turn calls
|
|
1584
|
+
const _multiKeypair = config.getKeypair();
|
|
1538
1585
|
const driver = new ConversationDriver({
|
|
1539
1586
|
runtime,
|
|
1540
1587
|
agentContext,
|
|
@@ -1546,6 +1593,8 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1546
1593
|
maxTurns,
|
|
1547
1594
|
configTurnTimeoutMs,
|
|
1548
1595
|
ownerContext,
|
|
1596
|
+
privateKey: _multiKeypair ? _multiKeypair.privateKey : null,
|
|
1597
|
+
publicKey: _multiKeypair ? _multiKeypair.publicKey : null,
|
|
1549
1598
|
onTurn: (info) => {
|
|
1550
1599
|
const preview = info.messagePreview.length >= 80
|
|
1551
1600
|
? info.messagePreview + '...'
|
|
@@ -1586,8 +1635,12 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
1586
1635
|
}
|
|
1587
1636
|
|
|
1588
1637
|
// Single-shot call (existing behavior)
|
|
1638
|
+
// A2A-52: load keypair for request signing
|
|
1639
|
+
const _callKeypair = config.getKeypair();
|
|
1589
1640
|
const client = new A2AClient({
|
|
1590
|
-
caller: { name: callerName }
|
|
1641
|
+
caller: { name: callerName },
|
|
1642
|
+
privateKey: _callKeypair ? _callKeypair.privateKey : null,
|
|
1643
|
+
publicKey: _callKeypair ? _callKeypair.publicKey : null
|
|
1591
1644
|
});
|
|
1592
1645
|
|
|
1593
1646
|
try {
|
|
@@ -2292,6 +2345,21 @@ a2a add "${inviteUrl}" "${ownerText || 'friend'}" && a2a call "${ownerText || 'f
|
|
|
2292
2345
|
} catch (_) {}
|
|
2293
2346
|
}
|
|
2294
2347
|
|
|
2348
|
+
// A2A-52: Generate Ed25519 keypair for cryptographic identity (skip if already exists)
|
|
2349
|
+
const existingKeypair = config.getKeypair();
|
|
2350
|
+
if (!existingKeypair) {
|
|
2351
|
+
const { generateKeypair, fingerprint: fpFunc } = require('../src/lib/crypto');
|
|
2352
|
+
const keypair = generateKeypair();
|
|
2353
|
+
config.setKeypair(keypair.privateKey, keypair.publicKey);
|
|
2354
|
+
const fp = fpFunc(keypair.publicKey);
|
|
2355
|
+
console.log(`\n š Ed25519 identity generated`);
|
|
2356
|
+
console.log(` Fingerprint: ${fp}`);
|
|
2357
|
+
} else {
|
|
2358
|
+
const { fingerprint: fpFunc } = require('../src/lib/crypto');
|
|
2359
|
+
console.log(`\n š Ed25519 identity exists (not overwritten)`);
|
|
2360
|
+
console.log(` Fingerprint: ${fpFunc(existingKeypair.publicKey)}`);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2295
2363
|
// Save server config and advance onboarding state to awaiting_disclosure.
|
|
2296
2364
|
config.setAgent({ hostname: publicHost });
|
|
2297
2365
|
config.setOnboarding({ step: 'awaiting_disclosure' });
|
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,70 @@ 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) {
|
|
1539
|
+
renderSidebarLists(freshTier);
|
|
1540
|
+
// A2A-51: Re-bind drag listeners after innerHTML replacement in renderSidebarLists().
|
|
1541
|
+
// Without this, sidebar items lose dragstart/dragend handlers after the first drop.
|
|
1542
|
+
bindSidebarDrag();
|
|
1543
|
+
}
|
|
1544
|
+
}, 300);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// A2A-50: Shared helper to build an active-item card DOM element.
|
|
1548
|
+
// Used by both handleZoneDrop() and the create-item-submit handler
|
|
1549
|
+
// to avoid duplicating card HTML in two places.
|
|
1550
|
+
function buildItemCard(name, description, accentClass, typeLabel, removeAttr) {
|
|
1520
1551
|
const card = document.createElement('div');
|
|
1521
1552
|
card.className = `active-item-card ${accentClass}`;
|
|
1522
|
-
card.dataset.topic =
|
|
1523
|
-
card.dataset.description =
|
|
1553
|
+
card.dataset.topic = name;
|
|
1554
|
+
card.dataset.description = description;
|
|
1524
1555
|
card.innerHTML = `
|
|
1525
1556
|
<div>
|
|
1526
|
-
<div class="item-name">${esc(
|
|
1557
|
+
<div class="item-name">${esc(name)}</div>
|
|
1527
1558
|
<div class="item-type-label">${typeLabel}</div>
|
|
1528
1559
|
</div>
|
|
1529
|
-
<button class="item-close-btn" ${removeAttr}="${esc(
|
|
1560
|
+
<button class="item-close-btn" ${removeAttr}="${esc(name)}">
|
|
1530
1561
|
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1531
1562
|
</button>
|
|
1532
1563
|
`;
|
|
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);
|
|
1564
|
+
return card;
|
|
1542
1565
|
}
|
|
1543
1566
|
|
|
1544
|
-
// A2A-
|
|
1545
|
-
// #
|
|
1546
|
-
//
|
|
1567
|
+
// A2A-50: Populates tier select dropdowns: #invite-tier (Invites tab),
|
|
1568
|
+
// #new-tier-copy-from (Settings tab), and #new-tier-dialog-copy-from
|
|
1569
|
+
// (new tier modal in Permissions tab).
|
|
1547
1570
|
function populateInviteTierSelect() {
|
|
1548
1571
|
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1549
1572
|
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1573
|
+
const newTierDialogCopy = document.getElementById('new-tier-dialog-copy-from');
|
|
1550
1574
|
const inviteTier = document.getElementById('invite-tier');
|
|
1551
1575
|
|
|
1552
1576
|
const optionsHtml = tiers.map(tier => {
|
|
@@ -1556,6 +1580,8 @@ function populateInviteTierSelect() {
|
|
|
1556
1580
|
|
|
1557
1581
|
if (inviteTier) inviteTier.innerHTML = optionsHtml;
|
|
1558
1582
|
if (newTierCopy) newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1583
|
+
// A2A-50: Also populate the Copy From select inside the new-tier-dialog modal
|
|
1584
|
+
if (newTierDialogCopy) newTierDialogCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1559
1585
|
|
|
1560
1586
|
// A2A-48: Default invite tier to 'public'
|
|
1561
1587
|
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
@@ -1683,6 +1709,23 @@ function openCallerPreview() {
|
|
|
1683
1709
|
dialog.show();
|
|
1684
1710
|
}
|
|
1685
1711
|
|
|
1712
|
+
// A2A-50: Shows the delete confirmation dialog for a topic/goal card.
|
|
1713
|
+
// Uses a unique marker attribute on the card so the confirm handler can
|
|
1714
|
+
// find and remove it. This avoids storing DOM references in closure state.
|
|
1715
|
+
let _deleteIdCounter = 0;
|
|
1716
|
+
function showDeleteConfirm(itemName, itemType, cardElement) {
|
|
1717
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1718
|
+
if (!dialog) return;
|
|
1719
|
+
const label = itemType === 'topic' ? 'Topic' : 'Goal';
|
|
1720
|
+
const msgEl = document.getElementById('delete-confirm-message');
|
|
1721
|
+
if (msgEl) msgEl.textContent = `Remove ${label} "${itemName}" from this tier?`;
|
|
1722
|
+
// Tag the card with a unique ID so the confirm handler can find it
|
|
1723
|
+
const deleteId = `del-${++_deleteIdCounter}`;
|
|
1724
|
+
cardElement.setAttribute('data-delete-id', deleteId);
|
|
1725
|
+
dialog.dataset.deleteCardId = deleteId;
|
|
1726
|
+
dialog.show();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1686
1729
|
// A2A-48: Binds all event handlers for the permissions panel. Replaces old
|
|
1687
1730
|
// bindSettingsActions() ā removes handlers for deleted elements (tier-form,
|
|
1688
1731
|
// tier-select, copy-tier-btn, show-drag-columns, preview-caller-btn) and
|
|
@@ -1701,38 +1744,46 @@ function bindPermissionsActions() {
|
|
|
1701
1744
|
return;
|
|
1702
1745
|
}
|
|
1703
1746
|
|
|
1704
|
-
// A2A-
|
|
1747
|
+
// A2A-50: Close button on active topic cards opens delete confirmation dialog
|
|
1705
1748
|
const removeTopic = e.target.closest('[data-remove-topic]');
|
|
1706
1749
|
if (removeTopic) {
|
|
1707
1750
|
const card = removeTopic.closest('.active-item-card');
|
|
1708
|
-
if (card)
|
|
1709
|
-
|
|
1751
|
+
if (card) {
|
|
1752
|
+
const itemName = card.dataset.topic || '';
|
|
1753
|
+
showDeleteConfirm(itemName, 'topic', card);
|
|
1754
|
+
}
|
|
1710
1755
|
return;
|
|
1711
1756
|
}
|
|
1712
1757
|
|
|
1713
|
-
// A2A-
|
|
1758
|
+
// A2A-50: Close button on active goal cards opens delete confirmation dialog
|
|
1714
1759
|
const removeGoal = e.target.closest('[data-remove-goal]');
|
|
1715
1760
|
if (removeGoal) {
|
|
1716
1761
|
const card = removeGoal.closest('.active-item-card');
|
|
1717
|
-
if (card)
|
|
1718
|
-
|
|
1762
|
+
if (card) {
|
|
1763
|
+
const itemName = card.dataset.topic || '';
|
|
1764
|
+
showDeleteConfirm(itemName, 'goal', card);
|
|
1765
|
+
}
|
|
1719
1766
|
return;
|
|
1720
1767
|
}
|
|
1721
1768
|
|
|
1722
|
-
// A2A-
|
|
1769
|
+
// A2A-50: "+ New Tier" button opens glass-styled sl-dialog instead of
|
|
1770
|
+
// scrolling to inline form. Keeps focus trap and accessibility from Shoelace.
|
|
1723
1771
|
const newTierBtn = e.target.closest('#perm-new-tier-btn');
|
|
1724
1772
|
if (newTierBtn) {
|
|
1725
|
-
const
|
|
1726
|
-
if (
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
if (
|
|
1730
|
-
|
|
1773
|
+
const dialog = document.getElementById('new-tier-dialog');
|
|
1774
|
+
if (dialog) {
|
|
1775
|
+
const idInput = document.getElementById('new-tier-dialog-id');
|
|
1776
|
+
const nameInput = document.getElementById('new-tier-dialog-name');
|
|
1777
|
+
if (idInput) idInput.value = '';
|
|
1778
|
+
if (nameInput) nameInput.value = '';
|
|
1779
|
+
dialog.show();
|
|
1780
|
+
}
|
|
1731
1781
|
return;
|
|
1732
1782
|
}
|
|
1733
1783
|
|
|
1734
|
-
// A2A-48: Sidebar "Add Topic" / "Add Goal" buttons open create dialog
|
|
1735
|
-
|
|
1784
|
+
// A2A-48: Sidebar "Add Topic" / "Add Goal" buttons open create dialog.
|
|
1785
|
+
// A2A-51: Also matches .col-header-add-btn for narrow viewports where sidebar is hidden.
|
|
1786
|
+
const addBtn = e.target.closest('[data-add-type]');
|
|
1736
1787
|
if (addBtn) {
|
|
1737
1788
|
const type = addBtn.dataset.addType;
|
|
1738
1789
|
const dialog = document.getElementById('create-item-dialog');
|
|
@@ -1753,13 +1804,17 @@ function bindPermissionsActions() {
|
|
|
1753
1804
|
// (#active-topics-zone, #active-goals-zone) persist across renders. Only
|
|
1754
1805
|
// their innerHTML is replaced by renderActiveTopics/renderActiveGoals.
|
|
1755
1806
|
// Binding in bindSidebarDrag() would cause listener accumulation.
|
|
1807
|
+
// A2A-51: Uses dragenter/dragleave counter to prevent flickering when
|
|
1808
|
+
// cursor moves over child elements (cards, placeholder) inside the zone.
|
|
1756
1809
|
const topicZone = document.getElementById('active-topics-zone');
|
|
1757
1810
|
const goalZone = document.getElementById('active-goals-zone');
|
|
1758
1811
|
[topicZone, goalZone].forEach(zone => {
|
|
1759
1812
|
if (!zone) return;
|
|
1760
|
-
|
|
1761
|
-
zone.addEventListener('
|
|
1762
|
-
zone.addEventListener('
|
|
1813
|
+
let dragCounter = 0;
|
|
1814
|
+
zone.addEventListener('dragenter', (e) => { e.preventDefault(); dragCounter++; zone.classList.add('drag-over'); });
|
|
1815
|
+
zone.addEventListener('dragover', (e) => { e.preventDefault(); });
|
|
1816
|
+
zone.addEventListener('dragleave', () => { dragCounter--; if (dragCounter === 0) zone.classList.remove('drag-over'); });
|
|
1817
|
+
zone.addEventListener('drop', (e) => { dragCounter = 0; handleZoneDrop(zone, e); });
|
|
1763
1818
|
});
|
|
1764
1819
|
|
|
1765
1820
|
// A2A-48: Tool toggle change ā auto-save and update card styling
|
|
@@ -1775,7 +1830,8 @@ function bindPermissionsActions() {
|
|
|
1775
1830
|
}
|
|
1776
1831
|
});
|
|
1777
1832
|
|
|
1778
|
-
// A2A-
|
|
1833
|
+
// A2A-50: Create Item dialog ā submit handler. Uses shared buildItemCard()
|
|
1834
|
+
// helper to avoid duplicating card HTML with handleZoneDrop().
|
|
1779
1835
|
document.getElementById('create-item-submit')?.addEventListener('click', () => {
|
|
1780
1836
|
const dialog = document.getElementById('create-item-dialog');
|
|
1781
1837
|
const titleInput = document.getElementById('create-item-title');
|
|
@@ -1787,7 +1843,6 @@ function bindPermissionsActions() {
|
|
|
1787
1843
|
const desc = descInput?.value?.trim() || '';
|
|
1788
1844
|
const type = dialog.dataset.createType || 'topic';
|
|
1789
1845
|
|
|
1790
|
-
// A2A-48: Add item to the appropriate active zone
|
|
1791
1846
|
const zoneId = type === 'topic' ? 'active-topics-zone' : 'active-goals-zone';
|
|
1792
1847
|
const zone = document.getElementById(zoneId);
|
|
1793
1848
|
if (!zone) return;
|
|
@@ -1796,20 +1851,8 @@ function bindPermissionsActions() {
|
|
|
1796
1851
|
const typeLabel = type === 'topic' ? 'Topic' : 'Goal';
|
|
1797
1852
|
const removeAttr = type === 'topic' ? 'data-remove-topic' : 'data-remove-goal';
|
|
1798
1853
|
|
|
1854
|
+
const card = buildItemCard(title, desc, accentClass, typeLabel, removeAttr);
|
|
1799
1855
|
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
1856
|
if (placeholder) zone.insertBefore(card, placeholder);
|
|
1814
1857
|
else zone.appendChild(card);
|
|
1815
1858
|
|
|
@@ -1822,41 +1865,69 @@ function bindPermissionsActions() {
|
|
|
1822
1865
|
document.getElementById('create-item-dialog')?.hide();
|
|
1823
1866
|
});
|
|
1824
1867
|
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1868
|
+
// A2A-50: New Tier dialog ā submit handler (replaces inline form handler).
|
|
1869
|
+
// Creates tier via POST and switches to it.
|
|
1870
|
+
document.getElementById('new-tier-dialog-submit')?.addEventListener('click', async () => {
|
|
1871
|
+
const tierId = document.getElementById('new-tier-dialog-id')?.value?.trim();
|
|
1872
|
+
const name = document.getElementById('new-tier-dialog-name')?.value?.trim();
|
|
1873
|
+
const copyFrom = document.getElementById('new-tier-dialog-copy-from')?.value;
|
|
1874
|
+
if (!tierId) {
|
|
1875
|
+
document.getElementById('new-tier-dialog-id')?.focus();
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
try {
|
|
1879
|
+
await request('/settings/tiers', {
|
|
1880
|
+
method: 'POST',
|
|
1881
|
+
body: JSON.stringify({
|
|
1882
|
+
id: tierId,
|
|
1883
|
+
name: name || tierId,
|
|
1884
|
+
copy_from: copyFrom || undefined
|
|
1885
|
+
})
|
|
1886
|
+
});
|
|
1887
|
+
showNotice(`Created tier "${tierId}"`);
|
|
1888
|
+
document.getElementById('new-tier-dialog')?.hide();
|
|
1889
|
+
await loadSettings();
|
|
1890
|
+
state.activeTierId = tierId;
|
|
1891
|
+
renderPermissions();
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
showNotice(err.message);
|
|
1894
|
+
}
|
|
1837
1895
|
});
|
|
1838
1896
|
|
|
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
|
-
|
|
1897
|
+
// A2A-50: New Tier dialog ā cancel handler
|
|
1898
|
+
document.getElementById('new-tier-dialog-cancel')?.addEventListener('click', () => {
|
|
1899
|
+
document.getElementById('new-tier-dialog')?.hide();
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// A2A-50: Delete confirm dialog ā confirm handler. Removes the card that
|
|
1903
|
+
// was stored in dataset and triggers auto-save.
|
|
1904
|
+
document.getElementById('delete-confirm-yes')?.addEventListener('click', () => {
|
|
1905
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1906
|
+
if (!dialog) return;
|
|
1907
|
+
const cardId = dialog.dataset.deleteCardId;
|
|
1908
|
+
if (cardId) {
|
|
1909
|
+
const card = document.querySelector(`.active-item-card[data-delete-id="${cardId}"]`);
|
|
1910
|
+
if (card) {
|
|
1911
|
+
card.removeAttribute('data-delete-id');
|
|
1912
|
+
card.remove();
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
dialog.hide();
|
|
1916
|
+
autoSaveTier();
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
// A2A-50: Delete confirm dialog ā cancel handler
|
|
1920
|
+
document.getElementById('delete-confirm-no')?.addEventListener('click', () => {
|
|
1921
|
+
const dialog = document.getElementById('delete-confirm-dialog');
|
|
1922
|
+
if (dialog) {
|
|
1923
|
+
// Clean up the marker attribute from the card
|
|
1924
|
+
const cardId = dialog.dataset.deleteCardId;
|
|
1925
|
+
if (cardId) {
|
|
1926
|
+
const card = document.querySelector(`.active-item-card[data-delete-id="${cardId}"]`);
|
|
1927
|
+
if (card) card.removeAttribute('data-delete-id');
|
|
1928
|
+
}
|
|
1929
|
+
dialog.hide();
|
|
1930
|
+
}
|
|
1860
1931
|
});
|
|
1861
1932
|
|
|
1862
1933
|
// Preview dialog close ā unchanged
|
|
@@ -2243,6 +2314,8 @@ const tabLoaders = {
|
|
|
2243
2314
|
permissions: loadSettings,
|
|
2244
2315
|
invites: loadInvites,
|
|
2245
2316
|
health: loadHealth,
|
|
2317
|
+
// A2A-50: Settings tab loads dashboard status, auto-update, and callbook data
|
|
2318
|
+
settings: () => { loadDashboardStatus(); loadAutoUpdateStatus(); loadCallbookDevices(); },
|
|
2246
2319
|
};
|
|
2247
2320
|
|
|
2248
2321
|
function startPolling() {
|
|
@@ -2361,6 +2434,55 @@ function renderHealthHistory(history) {
|
|
|
2361
2434
|
}).join('');
|
|
2362
2435
|
}
|
|
2363
2436
|
|
|
2437
|
+
// A2A-50: Binds handlers for the Settings panel (defaults form, new-tier form).
|
|
2438
|
+
// These were previously inside bindPermissionsActions() but are now in the
|
|
2439
|
+
// separate #panel-settings panel.
|
|
2440
|
+
function bindSettingsActions() {
|
|
2441
|
+
// Defaults form
|
|
2442
|
+
document.getElementById('defaults-form')?.addEventListener('submit', async (e) => {
|
|
2443
|
+
e.preventDefault();
|
|
2444
|
+
try {
|
|
2445
|
+
await request('/settings/defaults', {
|
|
2446
|
+
method: 'PUT',
|
|
2447
|
+
body: JSON.stringify({
|
|
2448
|
+
expiration: document.getElementById('defaults-expiration').value,
|
|
2449
|
+
maxCalls: Number.parseInt(document.getElementById('defaults-max-calls').value, 10) || 100
|
|
2450
|
+
})
|
|
2451
|
+
});
|
|
2452
|
+
showNotice('Saved defaults');
|
|
2453
|
+
await loadSettings();
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
showNotice(err.message);
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// New Tier form (inline form in Settings tab, kept for non-modal access)
|
|
2460
|
+
document.getElementById('new-tier-form')?.addEventListener('submit', async (e) => {
|
|
2461
|
+
e.preventDefault();
|
|
2462
|
+
const tierId = document.getElementById('new-tier-id')?.value?.trim();
|
|
2463
|
+
const name = document.getElementById('new-tier-name')?.value?.trim();
|
|
2464
|
+
const copyFrom = document.getElementById('new-tier-copy-from')?.value;
|
|
2465
|
+
if (!tierId) return;
|
|
2466
|
+
try {
|
|
2467
|
+
await request('/settings/tiers', {
|
|
2468
|
+
method: 'POST',
|
|
2469
|
+
body: JSON.stringify({
|
|
2470
|
+
id: tierId,
|
|
2471
|
+
name: name || tierId,
|
|
2472
|
+
copy_from: copyFrom || undefined
|
|
2473
|
+
})
|
|
2474
|
+
});
|
|
2475
|
+
showNotice(`Created tier "${tierId}"`);
|
|
2476
|
+
document.getElementById('new-tier-form')?.reset();
|
|
2477
|
+
await loadSettings();
|
|
2478
|
+
state.activeTierId = tierId;
|
|
2479
|
+
renderPermissions();
|
|
2480
|
+
} catch (err) {
|
|
2481
|
+
showNotice(err.message);
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2364
2486
|
async function bootstrap() {
|
|
2365
2487
|
bindTabs();
|
|
2366
2488
|
bindContactsActions();
|
|
@@ -2368,6 +2490,8 @@ async function bootstrap() {
|
|
|
2368
2490
|
// bindItemListDelegation(). All tier/tool/topic/goal handlers are now
|
|
2369
2491
|
// inside bindPermissionsActions() using event delegation on #panel-permissions.
|
|
2370
2492
|
bindPermissionsActions();
|
|
2493
|
+
// A2A-50: Settings panel handlers (defaults, new-tier inline form)
|
|
2494
|
+
bindSettingsActions();
|
|
2371
2495
|
bindCallbookActions();
|
|
2372
2496
|
bindAutoUpdateActions();
|
|
2373
2497
|
bindInviteActions();
|