a2acalling 0.6.63 → 0.6.64
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/.maestro/inbox/release-workflow-spam.md +25 -0
- package/docs/plans/2026-02-21-a2a-48-permissions-redesign.md +359 -0
- package/docs/plans/2026-02-21-a2a-49-remaining-tabs-polish.md +375 -0
- package/package.json +1 -1
- package/src/dashboard/public/app.js +564 -400
- package/src/dashboard/public/index.html +194 -125
- package/src/dashboard/public/style.css +919 -109
|
@@ -15,7 +15,12 @@ const state = {
|
|
|
15
15
|
realtime: {
|
|
16
16
|
connected: false,
|
|
17
17
|
lastEventId: null
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
|
+
// A2A-47: Track active panel for sidebar navigation (replaces sl-tab-group)
|
|
20
|
+
activeTab: 'contacts',
|
|
21
|
+
// A2A-48: Track currently selected tier for the permissions panel.
|
|
22
|
+
// Replaces the old #tier-select dropdown value.
|
|
23
|
+
activeTierId: 'public'
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
let dashboardEventSource = null;
|
|
@@ -249,37 +254,77 @@ async function copyText(value) {
|
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
// A2A-47: Panel name → section title mapping for the content header
|
|
258
|
+
const panelTitles = {
|
|
259
|
+
contacts: 'Contacts',
|
|
260
|
+
calls: 'Calls',
|
|
261
|
+
permissions: 'Permissions',
|
|
262
|
+
invites: 'Invites',
|
|
263
|
+
logs: 'Logs',
|
|
264
|
+
health: 'Health'
|
|
265
|
+
};
|
|
255
266
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
// A2A-47: Show a specific panel and update sidebar + header state.
|
|
268
|
+
// Replaces the old sl-tab-group navigation.
|
|
269
|
+
function showPanel(name) {
|
|
270
|
+
const validPanels = Object.keys(panelTitles);
|
|
271
|
+
if (!validPanels.includes(name)) name = 'contacts';
|
|
272
|
+
|
|
273
|
+
// Hide all panels, show the target
|
|
274
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
275
|
+
const target = document.getElementById('panel-' + name);
|
|
276
|
+
if (target) target.classList.add('active');
|
|
277
|
+
|
|
278
|
+
// Update sidebar active state
|
|
279
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
280
|
+
if (item.dataset.panel === name) {
|
|
281
|
+
item.classList.add('active');
|
|
282
|
+
} else {
|
|
283
|
+
item.classList.remove('active');
|
|
284
|
+
}
|
|
260
285
|
});
|
|
261
286
|
|
|
262
|
-
//
|
|
287
|
+
// Update header title
|
|
288
|
+
const titleEl = document.getElementById('section-title');
|
|
289
|
+
if (titleEl) titleEl.textContent = panelTitles[name] || name;
|
|
290
|
+
|
|
291
|
+
// Update state and hash
|
|
292
|
+
state.activeTab = name;
|
|
293
|
+
try {
|
|
294
|
+
if (window.location.hash.slice(1) !== name) {
|
|
295
|
+
window.location.hash = name;
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {}
|
|
298
|
+
|
|
299
|
+
// Trigger data loading for the active tab
|
|
300
|
+
if (typeof onTabSwitch === 'function') onTabSwitch(name);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function bindTabs() {
|
|
304
|
+
// A2A-47: Sidebar nav click handler
|
|
305
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
306
|
+
item.addEventListener('click', (e) => {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
const panel = item.dataset.panel;
|
|
309
|
+
if (panel) showPanel(panel);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Deep-link support: activate the panel matching the URL hash
|
|
263
314
|
const activateFromHash = () => {
|
|
264
315
|
let hash = window.location.hash.slice(1);
|
|
265
316
|
// A2A-41: backward-compat alias — old bookmarks/links using #settings
|
|
266
317
|
// still work after rename to #permissions
|
|
267
318
|
if (hash === 'settings') hash = 'permissions';
|
|
268
319
|
if (hash) {
|
|
269
|
-
|
|
270
|
-
try { tabGroup.show(hash); } catch (err) {}
|
|
320
|
+
showPanel(hash);
|
|
271
321
|
}
|
|
272
322
|
};
|
|
273
323
|
|
|
274
324
|
window.addEventListener('hashchange', activateFromHash);
|
|
275
325
|
|
|
276
|
-
// On initial load, activate from hash
|
|
277
|
-
|
|
278
|
-
tabGroup.updateComplete.then(activateFromHash);
|
|
279
|
-
} else {
|
|
280
|
-
// Fallback: try after a short delay
|
|
281
|
-
setTimeout(activateFromHash, 100);
|
|
282
|
-
}
|
|
326
|
+
// On initial load, activate from hash
|
|
327
|
+
activateFromHash();
|
|
283
328
|
}
|
|
284
329
|
|
|
285
330
|
function norm(value) {
|
|
@@ -393,7 +438,7 @@ function renderContacts() {
|
|
|
393
438
|
</td>
|
|
394
439
|
${locationCell}
|
|
395
440
|
${ownerCell}
|
|
396
|
-
<td>${esc(c?.status || '-')}</td>
|
|
441
|
+
<td><span class="contact-status" data-status="${esc(c?.status || 'unknown')}">${esc(c?.status || '-')}</span></td>
|
|
397
442
|
<td>${esc(String(calls))}</td>
|
|
398
443
|
<td>${esc(lastCallAt)}</td>
|
|
399
444
|
${summaryCell}
|
|
@@ -562,8 +607,8 @@ function bindContactsActions() {
|
|
|
562
607
|
}
|
|
563
608
|
});
|
|
564
609
|
|
|
565
|
-
// Event delegation on the contacts tab panel
|
|
566
|
-
const panel = document.querySelector('
|
|
610
|
+
// A2A-47: Event delegation on the contacts panel (was sl-tab-panel, now div#panel-contacts)
|
|
611
|
+
const panel = document.querySelector('#panel-contacts');
|
|
567
612
|
panel?.addEventListener('click', async (e) => {
|
|
568
613
|
const pinBtn = e.target.closest('[data-pin-contact]');
|
|
569
614
|
if (pinBtn) {
|
|
@@ -1062,7 +1107,7 @@ function renderLogs() {
|
|
|
1062
1107
|
const trace = row.trace_id || '';
|
|
1063
1108
|
tr.innerHTML = `
|
|
1064
1109
|
<td>${esc(fmtDate(row.timestamp))}</td>
|
|
1065
|
-
<td>${esc(row.level || '-')}</td>
|
|
1110
|
+
<td><span class="log-level" data-level="${esc(row.level || '')}">${esc(row.level || '-')}</span></td>
|
|
1066
1111
|
<td>${esc(row.component || '-')}</td>
|
|
1067
1112
|
<td>${esc(row.event || '-')}</td>
|
|
1068
1113
|
<td title="${esc(row.message || '')}">${esc(String(row.message || '').slice(0, 120) || '-')}</td>
|
|
@@ -1117,53 +1162,113 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1117
1162
|
// A2A-41: standard tier order for inheritance. Custom tiers are not in this list.
|
|
1118
1163
|
const TIER_ORDER = ['public', 'friends', 'family'];
|
|
1119
1164
|
|
|
1120
|
-
// A2A-
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1123
|
-
|
|
1165
|
+
// A2A-48: Material icon mapping for tier cards. Standard tiers get recognizable
|
|
1166
|
+
// icons; custom tiers get a wrench icon. Used by renderTierCards().
|
|
1167
|
+
const TIER_ICONS = { public: 'public', friends: 'group', family: 'family_restroom' };
|
|
1168
|
+
|
|
1169
|
+
// A2A-48: Color mapping for tool icons in toggle cards. Gives each tool a
|
|
1170
|
+
// distinct color matching the concept mock's visual differentiation.
|
|
1171
|
+
const TOOL_ICON_MAP = {
|
|
1172
|
+
'Bash': { icon: 'terminal', bg: 'rgba(99,102,241,0.2)', color: '#818CF8', border: 'rgba(99,102,241,0.2)' },
|
|
1173
|
+
'Bash(readonly)': { icon: 'terminal', bg: 'rgba(99,102,241,0.15)', color: '#A5B4FC', border: 'rgba(99,102,241,0.15)' },
|
|
1174
|
+
'Read': { icon: 'visibility', bg: 'rgba(59,130,246,0.2)', color: '#60A5FA', border: 'rgba(59,130,246,0.2)' },
|
|
1175
|
+
'Grep': { icon: 'search', bg: 'rgba(139,92,246,0.2)', color: '#A78BFA', border: 'rgba(139,92,246,0.2)' },
|
|
1176
|
+
'Glob': { icon: 'folder_open', bg: 'rgba(16,185,129,0.2)', color: '#34D399', border: 'rgba(16,185,129,0.2)' },
|
|
1177
|
+
'WebSearch': { icon: 'public', bg: 'rgba(245,158,11,0.2)', color: '#FBBF24', border: 'rgba(245,158,11,0.2)' },
|
|
1178
|
+
'WebFetch': { icon: 'language', bg: 'rgba(236,72,153,0.2)', color: '#F472B6', border: 'rgba(236,72,153,0.2)' }
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// A2A-48: Renders tier cards grid. Active card gets .active class with glow.
|
|
1182
|
+
function renderTierCards() {
|
|
1183
|
+
const container = document.getElementById('tier-cards');
|
|
1184
|
+
if (!container) return;
|
|
1185
|
+
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => {
|
|
1186
|
+
const aIdx = TIER_ORDER.indexOf(a.id);
|
|
1187
|
+
const bIdx = TIER_ORDER.indexOf(b.id);
|
|
1188
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
1189
|
+
if (aIdx >= 0) return -1;
|
|
1190
|
+
if (bIdx >= 0) return 1;
|
|
1191
|
+
return a.id.localeCompare(b.id);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
container.innerHTML = tiers.map(tier => {
|
|
1195
|
+
const isActive = tier.id === state.activeTierId;
|
|
1196
|
+
const icon = TIER_ICONS[tier.id] || 'build';
|
|
1197
|
+
const iconColor = isActive ? '#60A5FA' : '#6B7280';
|
|
1198
|
+
return `
|
|
1199
|
+
<div class="tier-card${isActive ? ' active' : ''}" data-tier-id="${esc(tier.id)}">
|
|
1200
|
+
<span class="material-symbols-outlined tier-card-icon" style="color:${iconColor};">${icon}</span>
|
|
1201
|
+
<span class="tier-card-name">${esc(tier.name || tier.id)}</span>
|
|
1202
|
+
${isActive ? '<div class="status-dot status-dot--green"></div>' : ''}
|
|
1203
|
+
</div>
|
|
1204
|
+
`;
|
|
1205
|
+
}).join('');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// A2A-48: Renders tool toggle cards (replaces checkboxes). Each tool is a
|
|
1209
|
+
// glass-panel card with icon, name, description, and a toggle switch.
|
|
1210
|
+
// Toggle change triggers autoSaveTier() for immediate persistence.
|
|
1211
|
+
function renderToolToggles(allowedTools) {
|
|
1212
|
+
const container = document.getElementById('tool-toggles');
|
|
1213
|
+
if (!container) return;
|
|
1124
1214
|
container.innerHTML = Object.entries(TOOL_DESCRIPTIONS).map(([tool, desc]) => {
|
|
1125
|
-
const checked = (allowedTools || []).includes(tool)
|
|
1126
|
-
|
|
1215
|
+
const checked = (allowedTools || []).includes(tool);
|
|
1216
|
+
const iconInfo = TOOL_ICON_MAP[tool] || { icon: 'extension', bg: 'rgba(100,116,139,0.2)', color: '#94A3B8', border: 'rgba(100,116,139,0.2)' };
|
|
1217
|
+
return `
|
|
1218
|
+
<div class="tool-toggle-card${checked ? ' enabled' : ''}">
|
|
1219
|
+
<div class="tool-toggle-info">
|
|
1220
|
+
<div class="tool-icon" style="background:${iconInfo.bg};color:${iconInfo.color};border:1px solid ${iconInfo.border};">
|
|
1221
|
+
<span class="material-symbols-outlined">${iconInfo.icon}</span>
|
|
1222
|
+
</div>
|
|
1223
|
+
<div>
|
|
1224
|
+
<h3>${esc(tool)}</h3>
|
|
1225
|
+
<p>${esc(desc)}</p>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
<label class="toggle-switch">
|
|
1229
|
+
<input type="checkbox" data-tool="${esc(tool)}" ${checked ? 'checked' : ''}>
|
|
1230
|
+
<span class="slider"></span>
|
|
1231
|
+
</label>
|
|
1232
|
+
</div>
|
|
1233
|
+
`;
|
|
1127
1234
|
}).join('');
|
|
1128
1235
|
}
|
|
1129
1236
|
|
|
1130
|
-
// A2A-
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1237
|
+
// A2A-48: Renders active topics in the drop zone as teal-accented cards.
|
|
1238
|
+
// Each card has a close button for removal. Updates #topic-count badge.
|
|
1239
|
+
function renderActiveTopics(tier) {
|
|
1240
|
+
const container = document.getElementById('active-topics-zone');
|
|
1241
|
+
if (!container) return;
|
|
1135
1242
|
const manifestTopics = tier.manifest?.topics || [];
|
|
1136
1243
|
const flatTopics = tier.topics || [];
|
|
1137
1244
|
|
|
1138
|
-
// A2A-41: prefer manifest data (has descriptions), fall back to flat array
|
|
1139
1245
|
const allTopics = manifestTopics.length > 0
|
|
1140
1246
|
? manifestTopics.map(t => ({ label: t.topic, desc: t.description || '' }))
|
|
1141
1247
|
: flatTopics.map(t => ({ label: t, desc: '' }));
|
|
1142
1248
|
|
|
1143
|
-
const
|
|
1144
|
-
<div class="
|
|
1145
|
-
<
|
|
1146
|
-
|
|
1147
|
-
<div class="
|
|
1148
|
-
<strong class="topic-label">${esc(t.label)}</strong>
|
|
1149
|
-
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1150
|
-
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1151
|
-
</div>
|
|
1152
|
-
<div class="topic-description" style="display:none;">
|
|
1153
|
-
<p class="topic-desc-text">${esc(t.desc) || '<em>No description</em>'}</p>
|
|
1154
|
-
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(t.desc)}"></sl-input>
|
|
1155
|
-
</div>
|
|
1249
|
+
const cardsHtml = allTopics.map(t => `
|
|
1250
|
+
<div class="active-item-card active-item-card--teal" data-topic="${esc(t.label)}" data-description="${esc(t.desc)}">
|
|
1251
|
+
<div>
|
|
1252
|
+
<div class="item-name">${esc(t.label)}</div>
|
|
1253
|
+
<div class="item-type-label">Topic</div>
|
|
1156
1254
|
</div>
|
|
1255
|
+
<button class="item-close-btn" data-remove-topic="${esc(t.label)}">
|
|
1256
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1257
|
+
</button>
|
|
1157
1258
|
</div>
|
|
1158
1259
|
`).join('');
|
|
1159
1260
|
|
|
1160
|
-
container.innerHTML =
|
|
1261
|
+
container.innerHTML = cardsHtml + '<div class="drop-placeholder"><span>+ Drop Topic</span></div>';
|
|
1262
|
+
|
|
1263
|
+
const badge = document.getElementById('topic-count');
|
|
1264
|
+
if (badge) badge.textContent = `${allTopics.length} Active`;
|
|
1161
1265
|
}
|
|
1162
1266
|
|
|
1163
|
-
// A2A-
|
|
1164
|
-
//
|
|
1165
|
-
function
|
|
1166
|
-
const container = document.getElementById('
|
|
1267
|
+
// A2A-48: Renders active goals in the drop zone as yellow-accented cards.
|
|
1268
|
+
// Same pattern as topics but with yellow color variant.
|
|
1269
|
+
function renderActiveGoals(tier) {
|
|
1270
|
+
const container = document.getElementById('active-goals-zone');
|
|
1271
|
+
if (!container) return;
|
|
1167
1272
|
const manifestGoals = tier.manifest?.objectives || [];
|
|
1168
1273
|
const flatGoals = tier.goals || [];
|
|
1169
1274
|
|
|
@@ -1171,302 +1276,290 @@ function renderGoalList(tier) {
|
|
|
1171
1276
|
? manifestGoals.map(g => ({ label: g.objective || g.topic, desc: g.description || '' }))
|
|
1172
1277
|
: flatGoals.map(g => ({ label: g, desc: '' }));
|
|
1173
1278
|
|
|
1174
|
-
const
|
|
1175
|
-
<div class="
|
|
1176
|
-
<
|
|
1177
|
-
|
|
1178
|
-
<div class="
|
|
1179
|
-
<strong class="topic-label">${esc(g.label)}</strong>
|
|
1180
|
-
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1181
|
-
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1182
|
-
</div>
|
|
1183
|
-
<div class="topic-description" style="display:none;">
|
|
1184
|
-
<p class="topic-desc-text">${esc(g.desc) || '<em>No description</em>'}</p>
|
|
1185
|
-
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(g.desc)}"></sl-input>
|
|
1186
|
-
</div>
|
|
1279
|
+
const cardsHtml = allGoals.map(g => `
|
|
1280
|
+
<div class="active-item-card active-item-card--yellow" data-topic="${esc(g.label)}" data-description="${esc(g.desc)}">
|
|
1281
|
+
<div>
|
|
1282
|
+
<div class="item-name">${esc(g.label)}</div>
|
|
1283
|
+
<div class="item-type-label">Goal</div>
|
|
1187
1284
|
</div>
|
|
1285
|
+
<button class="item-close-btn" data-remove-goal="${esc(g.label)}">
|
|
1286
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1287
|
+
</button>
|
|
1188
1288
|
</div>
|
|
1189
1289
|
`).join('');
|
|
1190
1290
|
|
|
1191
|
-
container.innerHTML =
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// A2A-41: event delegation for topic and goal list interactions.
|
|
1195
|
-
// Uses a single click handler on each container instead of per-row binding,
|
|
1196
|
-
// preventing listener accumulation when topics are added dynamically.
|
|
1197
|
-
function bindItemListDelegation() {
|
|
1198
|
-
['tier-topics-list', 'tier-goals-list'].forEach(containerId => {
|
|
1199
|
-
const container = document.getElementById(containerId);
|
|
1200
|
-
if (!container) return;
|
|
1201
|
-
|
|
1202
|
-
container.addEventListener('click', (e) => {
|
|
1203
|
-
// Expand/collapse
|
|
1204
|
-
const expandBtn = e.target.closest('.topic-expand-btn');
|
|
1205
|
-
if (expandBtn) {
|
|
1206
|
-
const row = expandBtn.closest('.topic-row');
|
|
1207
|
-
const desc = row.querySelector('.topic-description');
|
|
1208
|
-
if (desc) {
|
|
1209
|
-
const isHidden = desc.style.display === 'none';
|
|
1210
|
-
desc.style.display = isHidden ? '' : 'none';
|
|
1211
|
-
expandBtn.name = isHidden ? 'chevron-up' : 'chevron-down';
|
|
1212
|
-
}
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1291
|
+
container.innerHTML = cardsHtml + '<div class="drop-placeholder"><span>+ Drop Goal</span></div>';
|
|
1215
1292
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (deleteBtn) {
|
|
1219
|
-
deleteBtn.closest('.topic-row').remove();
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
// Add new item
|
|
1224
|
-
const addBtn = e.target.closest('.add-item-btn');
|
|
1225
|
-
if (addBtn) {
|
|
1226
|
-
const type = addBtn.dataset.type;
|
|
1227
|
-
const label = type === 'topic' ? 'Topic name' : 'Goal name';
|
|
1228
|
-
const newRow = document.createElement('div');
|
|
1229
|
-
newRow.className = 'topic-row';
|
|
1230
|
-
newRow.dataset.type = type;
|
|
1231
|
-
newRow.innerHTML = `
|
|
1232
|
-
<span class="drag-handle">\u2807</span>
|
|
1233
|
-
<div class="topic-content">
|
|
1234
|
-
<sl-input class="new-item-label" size="small" placeholder="${label}" autofocus></sl-input>
|
|
1235
|
-
<sl-input class="new-item-desc" size="small" placeholder="Description (optional)"></sl-input>
|
|
1236
|
-
<div class="row" style="margin-top:0.3rem;">
|
|
1237
|
-
<sl-button size="small" variant="primary" class="confirm-add-btn">Add</sl-button>
|
|
1238
|
-
<sl-button size="small" class="cancel-add-btn">Cancel</sl-button>
|
|
1239
|
-
</div>
|
|
1240
|
-
</div>
|
|
1241
|
-
`;
|
|
1242
|
-
container.insertBefore(newRow, addBtn);
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// Confirm add
|
|
1247
|
-
const confirmBtn = e.target.closest('.confirm-add-btn');
|
|
1248
|
-
if (confirmBtn) {
|
|
1249
|
-
const row = confirmBtn.closest('.topic-row');
|
|
1250
|
-
const nameInput = row.querySelector('.new-item-label');
|
|
1251
|
-
const descInput = row.querySelector('.new-item-desc');
|
|
1252
|
-
const name = nameInput.value.trim();
|
|
1253
|
-
if (!name) { nameInput.focus(); return; }
|
|
1254
|
-
|
|
1255
|
-
row.dataset.topic = name;
|
|
1256
|
-
row.innerHTML = `
|
|
1257
|
-
<span class="drag-handle">\u2807</span>
|
|
1258
|
-
<div class="topic-content">
|
|
1259
|
-
<div class="topic-header">
|
|
1260
|
-
<strong class="topic-label">${esc(name)}</strong>
|
|
1261
|
-
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1262
|
-
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1263
|
-
</div>
|
|
1264
|
-
<div class="topic-description" style="display:none;">
|
|
1265
|
-
<p class="topic-desc-text">${esc(descInput.value)}</p>
|
|
1266
|
-
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(descInput.value)}"></sl-input>
|
|
1267
|
-
</div>
|
|
1268
|
-
</div>
|
|
1269
|
-
`;
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Cancel add
|
|
1274
|
-
const cancelBtn = e.target.closest('.cancel-add-btn');
|
|
1275
|
-
if (cancelBtn) {
|
|
1276
|
-
cancelBtn.closest('.topic-row').remove();
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
});
|
|
1280
|
-
|
|
1281
|
-
// Description edit via sl-change (Shoelace event, delegated)
|
|
1282
|
-
container.addEventListener('sl-change', (e) => {
|
|
1283
|
-
const input = e.target.closest('.topic-desc-edit');
|
|
1284
|
-
if (input) {
|
|
1285
|
-
const row = input.closest('.topic-row');
|
|
1286
|
-
const textEl = row.querySelector('.topic-desc-text');
|
|
1287
|
-
if (textEl) textEl.textContent = input.value || '';
|
|
1288
|
-
}
|
|
1289
|
-
});
|
|
1290
|
-
});
|
|
1293
|
+
const badge = document.getElementById('goal-count');
|
|
1294
|
+
if (badge) badge.textContent = `${allGoals.length} Active`;
|
|
1291
1295
|
}
|
|
1292
1296
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1298
|
-
const inviteTier = document.getElementById('invite-tier');
|
|
1299
|
-
|
|
1300
|
-
const optionsHtml = tiers.map(tier => {
|
|
1301
|
-
const emoji = TIER_EMOJIS[tier.id] || '\u{1F527}';
|
|
1302
|
-
return `<sl-option value="${esc(tier.id)}">${emoji} ${esc(tier.name || tier.id)}</sl-option>`;
|
|
1303
|
-
}).join('');
|
|
1304
|
-
|
|
1305
|
-
tierSelect.innerHTML = optionsHtml;
|
|
1306
|
-
copyFrom.innerHTML = optionsHtml;
|
|
1307
|
-
inviteTier.innerHTML = optionsHtml;
|
|
1308
|
-
newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1309
|
-
|
|
1310
|
-
// A2A-41: default to 'public' — it's the base tier and most commonly edited
|
|
1311
|
-
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
1312
|
-
if (defaultTier) {
|
|
1313
|
-
tierSelect.value = defaultTier;
|
|
1314
|
-
copyFrom.value = defaultTier;
|
|
1315
|
-
inviteTier.value = defaultTier;
|
|
1316
|
-
renderTierEditor(defaultTier);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function renderTierEditor(tierId) {
|
|
1321
|
-
const tier = (state.settings?.tiers || []).find(t => t.id === tierId);
|
|
1297
|
+
// A2A-48: Orchestrator that renders the entire permissions panel.
|
|
1298
|
+
// Uses state.activeTierId instead of reading a dropdown value.
|
|
1299
|
+
function renderPermissions() {
|
|
1300
|
+
const tier = (state.settings?.tiers || []).find(t => t.id === state.activeTierId);
|
|
1322
1301
|
if (!tier) return;
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
renderTopicList(tier);
|
|
1328
|
-
renderGoalList(tier);
|
|
1302
|
+
renderTierCards();
|
|
1303
|
+
renderActiveTopics(tier);
|
|
1304
|
+
renderActiveGoals(tier);
|
|
1305
|
+
renderToolToggles(tier.allowed_tools);
|
|
1329
1306
|
renderTierWarnings(tier);
|
|
1330
|
-
|
|
1307
|
+
renderSidebarPreview(state.activeTierId);
|
|
1308
|
+
renderSidebarLists(tier);
|
|
1309
|
+
bindSidebarDrag();
|
|
1331
1310
|
}
|
|
1332
1311
|
|
|
1333
|
-
// A2A-
|
|
1334
|
-
//
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
function renderTierColumns() {
|
|
1338
|
-
const container = document.getElementById('tier-columns');
|
|
1312
|
+
// A2A-48: Renders the inline "Preview as Caller" card in the right sidebar.
|
|
1313
|
+
// Reuses getPreviewData() to show merged topics, goals, and tool count.
|
|
1314
|
+
function renderSidebarPreview(tierId) {
|
|
1315
|
+
const container = document.getElementById('perm-preview');
|
|
1339
1316
|
if (!container) return;
|
|
1340
|
-
const
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1317
|
+
const data = getPreviewData(tierId);
|
|
1318
|
+
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
1319
|
+
const topicNames = data.topics.map(t => t.topic).filter(Boolean);
|
|
1320
|
+
const goalNames = data.objectives.map(g => g.objective || g.topic).filter(Boolean);
|
|
1321
|
+
const toolCount = data.tools.size;
|
|
1322
|
+
|
|
1323
|
+
const topicText = topicNames.length > 0 ? `<strong style="color:#2DD4BF;">${esc(topicNames.join(', '))}</strong>` : '<em>no topics</em>';
|
|
1324
|
+
const goalText = goalNames.length > 0 ? `<strong style="color:#FBBF24;">${esc(goalNames.join(', '))}</strong>` : '<em>no goals</em>';
|
|
1325
|
+
|
|
1326
|
+
container.innerHTML = `
|
|
1327
|
+
<div class="preview-card-inner">
|
|
1328
|
+
<div class="sidebar-list-header">Preview as Caller</div>
|
|
1329
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
|
|
1330
|
+
<span class="material-symbols-outlined" style="color:#60A5FA;">smart_toy</span>
|
|
1331
|
+
<div>
|
|
1332
|
+
<div style="font-weight:600;font-size:0.85rem;color:var(--ink);">Agent Permission</div>
|
|
1333
|
+
<span class="preview-tier-badge">${esc(tierName)} Tier</span>
|
|
1334
|
+
</div>
|
|
1335
|
+
</div>
|
|
1336
|
+
<div class="preview-summary">
|
|
1337
|
+
This agent can discuss ${topicText} to help ${goalText} using <strong>${toolCount} tool${toolCount !== 1 ? 's' : ''}</strong>.
|
|
1338
|
+
</div>
|
|
1339
|
+
<div class="preview-footer">
|
|
1340
|
+
<span style="display:flex;align-items:center;gap:0.3rem;">
|
|
1341
|
+
<span class="status-dot status-dot--green"></span> Active
|
|
1342
|
+
</span>
|
|
1343
|
+
<span style="font-family:monospace;opacity:0.7;">JSON Valid</span>
|
|
1344
|
+
</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// A2A-48: Renders all topics and goals in the right sidebar as draggable items.
|
|
1350
|
+
// Items active in the current tier are dimmed with an "(Active)" label.
|
|
1351
|
+
function renderSidebarLists(tier) {
|
|
1352
|
+
const topicContainer = document.getElementById('sidebar-topics');
|
|
1353
|
+
const goalContainer = document.getElementById('sidebar-goals');
|
|
1354
|
+
if (!topicContainer || !goalContainer) return;
|
|
1355
|
+
|
|
1356
|
+
// Collect ALL topics across ALL tiers for the sidebar
|
|
1357
|
+
const allTiers = state.settings?.tiers || [];
|
|
1358
|
+
const allTopicMap = new Map();
|
|
1359
|
+
const allGoalMap = new Map();
|
|
1360
|
+
for (const t of allTiers) {
|
|
1361
|
+
const mTopics = t.manifest?.topics || [];
|
|
1362
|
+
const fTopics = t.topics || [];
|
|
1363
|
+
const topics = mTopics.length > 0 ? mTopics : fTopics.map(x => ({ topic: x, description: '' }));
|
|
1364
|
+
for (const item of topics) {
|
|
1365
|
+
if (item.topic && !allTopicMap.has(item.topic)) {
|
|
1366
|
+
allTopicMap.set(item.topic, item.description || '');
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
const mGoals = t.manifest?.objectives || [];
|
|
1370
|
+
const fGoals = t.goals || [];
|
|
1371
|
+
const goals = mGoals.length > 0 ? mGoals : fGoals.map(x => ({ topic: x, description: '' }));
|
|
1372
|
+
for (const item of goals) {
|
|
1373
|
+
const label = item.objective || item.topic;
|
|
1374
|
+
if (label && !allGoalMap.has(label)) {
|
|
1375
|
+
allGoalMap.set(label, item.description || '');
|
|
1376
|
+
}
|
|
1370
1377
|
}
|
|
1378
|
+
}
|
|
1371
1379
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1380
|
+
// Determine which are active in the current tier
|
|
1381
|
+
const activeTopicSet = new Set((tier.topics || []).concat(
|
|
1382
|
+
(tier.manifest?.topics || []).map(t => t.topic)
|
|
1383
|
+
).filter(Boolean));
|
|
1384
|
+
const activeGoalSet = new Set((tier.goals || []).concat(
|
|
1385
|
+
(tier.manifest?.objectives || []).map(g => g.objective || g.topic)
|
|
1386
|
+
).filter(Boolean));
|
|
1387
|
+
|
|
1388
|
+
// Render topics sidebar
|
|
1389
|
+
const topicItems = Array.from(allTopicMap.entries()).map(([name, desc]) => {
|
|
1390
|
+
const isActive = activeTopicSet.has(name);
|
|
1391
|
+
return `
|
|
1392
|
+
<div class="sidebar-item${isActive ? ' active-in-zone' : ''}" draggable="${isActive ? 'false' : 'true'}" data-sidebar-topic="${esc(name)}" data-description="${esc(desc)}" data-item-type="topic">
|
|
1393
|
+
<div style="display:flex;align-items:center;gap:0.4rem;">
|
|
1394
|
+
${isActive ? '' : '<span class="material-symbols-outlined" style="color:#4B5563;font-size:1rem;cursor:grab;">drag_indicator</span>'}
|
|
1395
|
+
<span class="sidebar-item-name">${esc(name)}${isActive ? ' <span class="sidebar-item-active-label">(Active)</span>' : ''}</span>
|
|
1383
1396
|
</div>
|
|
1384
1397
|
</div>
|
|
1385
|
-
|
|
1398
|
+
`;
|
|
1399
|
+
}).join('');
|
|
1400
|
+
|
|
1401
|
+
topicContainer.innerHTML = `
|
|
1402
|
+
<div class="sidebar-list-header">Topics</div>
|
|
1403
|
+
${topicItems}
|
|
1404
|
+
<button class="sidebar-add-btn" data-add-type="topic">
|
|
1405
|
+
<span class="material-symbols-outlined" style="font-size:14px;">add</span> Add Topic
|
|
1406
|
+
</button>
|
|
1407
|
+
`;
|
|
1386
1408
|
|
|
1409
|
+
// Render goals sidebar
|
|
1410
|
+
const goalItems = Array.from(allGoalMap.entries()).map(([name, desc]) => {
|
|
1411
|
+
const isActive = activeGoalSet.has(name);
|
|
1387
1412
|
return `
|
|
1388
|
-
<div class="
|
|
1389
|
-
<
|
|
1390
|
-
|
|
1391
|
-
|
|
1413
|
+
<div class="sidebar-item${isActive ? ' active-in-zone' : ''}" draggable="${isActive ? 'false' : 'true'}" data-sidebar-goal="${esc(name)}" data-description="${esc(desc)}" data-item-type="goal">
|
|
1414
|
+
<div style="display:flex;align-items:center;gap:0.4rem;">
|
|
1415
|
+
${isActive ? '' : '<span class="material-symbols-outlined" style="color:#4B5563;font-size:1rem;cursor:grab;">drag_indicator</span>'}
|
|
1416
|
+
<span class="sidebar-item-name">${esc(name)}${isActive ? ' <span class="sidebar-item-active-label">(Active)</span>' : ''}</span>
|
|
1392
1417
|
</div>
|
|
1393
|
-
</div
|
|
1418
|
+
</div>
|
|
1419
|
+
`;
|
|
1394
1420
|
}).join('');
|
|
1395
1421
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1422
|
+
goalContainer.innerHTML = `
|
|
1423
|
+
<div class="sidebar-list-header">Goals & Objectives</div>
|
|
1424
|
+
${goalItems}
|
|
1425
|
+
<button class="sidebar-add-btn" data-add-type="goal">
|
|
1426
|
+
<span class="material-symbols-outlined" style="font-size:14px;">add</span> Add Goal
|
|
1427
|
+
</button>
|
|
1428
|
+
`;
|
|
1398
1429
|
}
|
|
1399
1430
|
|
|
1400
|
-
// A2A-
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1431
|
+
// A2A-48: Debounced auto-save replaces explicit Save Tier button.
|
|
1432
|
+
// 250ms delay prevents excessive API calls during rapid changes.
|
|
1433
|
+
let _autoSaveTimer = null;
|
|
1434
|
+
function autoSaveTier() {
|
|
1435
|
+
clearTimeout(_autoSaveTimer);
|
|
1436
|
+
_autoSaveTimer = setTimeout(async () => {
|
|
1437
|
+
const tierId = state.activeTierId;
|
|
1438
|
+
if (!tierId) return;
|
|
1439
|
+
|
|
1440
|
+
// Collect tools from toggle states
|
|
1441
|
+
const toggles = document.querySelectorAll('#tool-toggles .toggle-switch input');
|
|
1442
|
+
const allowed_tools = Array.from(toggles).filter(t => t.checked).map(t => t.dataset.tool);
|
|
1443
|
+
|
|
1444
|
+
// Collect topics from active zone
|
|
1445
|
+
const topicCards = document.querySelectorAll('#active-topics-zone .active-item-card');
|
|
1446
|
+
// A2A-48: uses dataset.topic for BOTH topics and goals (NOT dataset.objective)
|
|
1447
|
+
// because parseTopicObjects() in dashboard.js:160 only reads entry.topic.
|
|
1448
|
+
// The semantic distinction (objective vs topic) is UI-only; storage layer
|
|
1449
|
+
// uses {topic, description} uniformly for both types.
|
|
1450
|
+
const topics = Array.from(topicCards).map(c => c.dataset.topic).filter(Boolean);
|
|
1451
|
+
const manifestTopics = Array.from(topicCards).map(c => ({
|
|
1452
|
+
topic: c.dataset.topic,
|
|
1453
|
+
description: c.dataset.description || ''
|
|
1454
|
+
})).filter(t => t.topic);
|
|
1455
|
+
|
|
1456
|
+
// Collect goals from active zone
|
|
1457
|
+
const goalCards = document.querySelectorAll('#active-goals-zone .active-item-card');
|
|
1458
|
+
const goals = Array.from(goalCards).map(c => c.dataset.topic).filter(Boolean);
|
|
1459
|
+
const manifestObjectives = Array.from(goalCards).map(c => ({
|
|
1460
|
+
topic: c.dataset.topic,
|
|
1461
|
+
description: c.dataset.description || ''
|
|
1462
|
+
})).filter(g => g.topic);
|
|
1463
|
+
|
|
1464
|
+
const body = { allowed_tools, topics, goals, manifest: { topics: manifestTopics, objectives: manifestObjectives } };
|
|
1465
|
+
// A2A-48: Refresh state inside try block so that a failed PUT does not
|
|
1466
|
+
// trigger an unhandled rejection from the subsequent GET.
|
|
1467
|
+
try {
|
|
1468
|
+
await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
|
|
1469
|
+
method: 'PUT', body: JSON.stringify(body)
|
|
1470
|
+
});
|
|
1471
|
+
showNotice('Saved');
|
|
1472
|
+
// Refresh state from server to stay in sync after auto-save
|
|
1473
|
+
const payload = await request('/settings');
|
|
1474
|
+
state.settings = payload;
|
|
1475
|
+
} catch (err) {
|
|
1476
|
+
showNotice(`Save failed: ${err.message}`);
|
|
1477
|
+
}
|
|
1478
|
+
}, 250);
|
|
1479
|
+
}
|
|
1405
1480
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1481
|
+
// A2A-48: Binds dragstart/dragend on sidebar items (re-created each render).
|
|
1482
|
+
// Zone listeners (dragover/dragleave/drop) are bound ONCE in
|
|
1483
|
+
// bindPermissionsActions() to avoid listener accumulation — the zone
|
|
1484
|
+
// containers persist across renders while only their innerHTML changes.
|
|
1485
|
+
function bindSidebarDrag() {
|
|
1486
|
+
document.querySelectorAll('.sidebar-item[draggable="true"]').forEach(item => {
|
|
1487
|
+
item.addEventListener('dragstart', (e) => {
|
|
1488
|
+
const itemType = item.dataset.itemType || 'topic';
|
|
1489
|
+
const name = item.dataset.sidebarTopic || item.dataset.sidebarGoal || '';
|
|
1490
|
+
const desc = item.dataset.description || '';
|
|
1491
|
+
e.dataTransfer.setData('application/json', JSON.stringify({ name, description: desc, type: itemType }));
|
|
1492
|
+
item.style.opacity = '0.5';
|
|
1413
1493
|
});
|
|
1414
|
-
|
|
1494
|
+
item.addEventListener('dragend', () => { item.style.opacity = ''; });
|
|
1415
1495
|
});
|
|
1496
|
+
}
|
|
1416
1497
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1498
|
+
// A2A-48: Drop handler for active topic/goal zones. Extracted from
|
|
1499
|
+
// bindSidebarDrag() to be called once in bindPermissionsActions().
|
|
1500
|
+
function handleZoneDrop(zone, e) {
|
|
1501
|
+
e.preventDefault();
|
|
1502
|
+
zone.classList.remove('drag-over');
|
|
1503
|
+
let data;
|
|
1504
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
1505
|
+
if (!data.name) return;
|
|
1506
|
+
|
|
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';
|
|
1511
|
+
|
|
1512
|
+
// Check if already in zone
|
|
1513
|
+
const existing = zone.querySelectorAll('.active-item-card');
|
|
1514
|
+
for (const card of existing) {
|
|
1515
|
+
if (card.dataset.topic === data.name) return; // already active
|
|
1516
|
+
}
|
|
1426
1517
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1518
|
+
// Insert before the placeholder
|
|
1519
|
+
const placeholder = zone.querySelector('.drop-placeholder');
|
|
1520
|
+
const card = document.createElement('div');
|
|
1521
|
+
card.className = `active-item-card ${accentClass}`;
|
|
1522
|
+
card.dataset.topic = data.name;
|
|
1523
|
+
card.dataset.description = data.description || '';
|
|
1524
|
+
card.innerHTML = `
|
|
1525
|
+
<div>
|
|
1526
|
+
<div class="item-name">${esc(data.name)}</div>
|
|
1527
|
+
<div class="item-type-label">${typeLabel}</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
<button class="item-close-btn" ${removeAttr}="${esc(data.name)}">
|
|
1530
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1531
|
+
</button>
|
|
1532
|
+
`;
|
|
1533
|
+
zone.insertBefore(card, placeholder);
|
|
1534
|
+
autoSaveTier();
|
|
1431
1535
|
|
|
1432
|
-
|
|
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);
|
|
1542
|
+
}
|
|
1433
1543
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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.
|
|
1547
|
+
function populateInviteTierSelect() {
|
|
1548
|
+
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1549
|
+
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1550
|
+
const inviteTier = document.getElementById('invite-tier');
|
|
1437
1551
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const targetManifestTopics = [...(targetTierData.manifest?.topics || []), movedManifest || { topic, description: '' }];
|
|
1552
|
+
const optionsHtml = tiers.map(tier => {
|
|
1553
|
+
const emoji = TIER_EMOJIS[tier.id] || '\u{1F527}';
|
|
1554
|
+
return `<sl-option value="${esc(tier.id)}">${emoji} ${esc(tier.name || tier.id)}</sl-option>`;
|
|
1555
|
+
}).join('');
|
|
1443
1556
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
body: JSON.stringify({
|
|
1451
|
-
topics: sourceTopics,
|
|
1452
|
-
manifest: { topics: sourceManifestTopics, objectives: sourceTierData.manifest?.objectives || [] }
|
|
1453
|
-
})
|
|
1454
|
-
}),
|
|
1455
|
-
request(`/settings/tiers/${encodeURIComponent(targetTier)}`, {
|
|
1456
|
-
method: 'PUT',
|
|
1457
|
-
body: JSON.stringify({
|
|
1458
|
-
topics: targetTopics,
|
|
1459
|
-
manifest: { topics: targetManifestTopics, objectives: targetTierData.manifest?.objectives || [] }
|
|
1460
|
-
})
|
|
1461
|
-
})
|
|
1462
|
-
]);
|
|
1463
|
-
showNotice(`Moved "${topic}" from ${sourceTier} to ${targetTier}`);
|
|
1464
|
-
} catch (err) {
|
|
1465
|
-
showNotice(`Move failed: ${err.message}. Reloading...`);
|
|
1466
|
-
}
|
|
1467
|
-
await loadSettings();
|
|
1468
|
-
});
|
|
1469
|
-
});
|
|
1557
|
+
if (inviteTier) inviteTier.innerHTML = optionsHtml;
|
|
1558
|
+
if (newTierCopy) newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1559
|
+
|
|
1560
|
+
// A2A-48: Default invite tier to 'public'
|
|
1561
|
+
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
1562
|
+
if (defaultTier && inviteTier) inviteTier.value = defaultTier;
|
|
1470
1563
|
}
|
|
1471
1564
|
|
|
1472
1565
|
// A2A-41: contextual validation warnings for the currently selected tier.
|
|
@@ -1543,10 +1636,10 @@ function getPreviewData(tierId) {
|
|
|
1543
1636
|
return merged;
|
|
1544
1637
|
}
|
|
1545
1638
|
|
|
1546
|
-
// A2A-
|
|
1547
|
-
// for the selected tier.
|
|
1639
|
+
// A2A-48: opens the caller preview dialog showing the merged effective view
|
|
1640
|
+
// for the selected tier. Uses state.activeTierId instead of removed #tier-select.
|
|
1548
1641
|
function openCallerPreview() {
|
|
1549
|
-
const tierId =
|
|
1642
|
+
const tierId = state.activeTierId;
|
|
1550
1643
|
const data = getPreviewData(tierId);
|
|
1551
1644
|
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
1552
1645
|
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
@@ -1590,72 +1683,147 @@ function openCallerPreview() {
|
|
|
1590
1683
|
dialog.show();
|
|
1591
1684
|
}
|
|
1592
1685
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1686
|
+
// A2A-48: Binds all event handlers for the permissions panel. Replaces old
|
|
1687
|
+
// bindSettingsActions() — removes handlers for deleted elements (tier-form,
|
|
1688
|
+
// tier-select, copy-tier-btn, show-drag-columns, preview-caller-btn) and
|
|
1689
|
+
// adds handlers for tier cards, tool toggles, close buttons, and sidebar.
|
|
1690
|
+
function bindPermissionsActions() {
|
|
1691
|
+
const panel = document.getElementById('panel-permissions');
|
|
1692
|
+
if (!panel) return;
|
|
1693
|
+
|
|
1694
|
+
// A2A-48: Tier card click — switch active tier and re-render.
|
|
1695
|
+
// No autoSaveTier() here: switching tiers is a read operation, not a write.
|
|
1696
|
+
panel.addEventListener('click', (e) => {
|
|
1697
|
+
const card = e.target.closest('.tier-card[data-tier-id]');
|
|
1698
|
+
if (card) {
|
|
1699
|
+
state.activeTierId = card.dataset.tierId;
|
|
1700
|
+
renderPermissions();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1597
1703
|
|
|
1598
|
-
|
|
1599
|
-
e.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
.map(cb => cb.value);
|
|
1607
|
-
|
|
1608
|
-
// A2A-41: collect topics from row elements
|
|
1609
|
-
const topicRows = document.querySelectorAll('#tier-topics-list .topic-row[data-topic]');
|
|
1610
|
-
const topics = Array.from(topicRows).map(row => row.dataset.topic).filter(Boolean);
|
|
1611
|
-
const manifestTopics = Array.from(topicRows).map(row => ({
|
|
1612
|
-
topic: row.dataset.topic,
|
|
1613
|
-
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
1614
|
-
})).filter(t => t.topic);
|
|
1704
|
+
// A2A-48: Close button on active topic cards
|
|
1705
|
+
const removeTopic = e.target.closest('[data-remove-topic]');
|
|
1706
|
+
if (removeTopic) {
|
|
1707
|
+
const card = removeTopic.closest('.active-item-card');
|
|
1708
|
+
if (card) card.remove();
|
|
1709
|
+
autoSaveTier();
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1615
1712
|
|
|
1616
|
-
// A2A-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
1625
|
-
})).filter(g => g.topic);
|
|
1713
|
+
// A2A-48: Close button on active goal cards
|
|
1714
|
+
const removeGoal = e.target.closest('[data-remove-goal]');
|
|
1715
|
+
if (removeGoal) {
|
|
1716
|
+
const card = removeGoal.closest('.active-item-card');
|
|
1717
|
+
if (card) card.remove();
|
|
1718
|
+
autoSaveTier();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1626
1721
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1722
|
+
// A2A-48: "+ New Tier" button scrolls to the new-tier form inside Settings details
|
|
1723
|
+
const newTierBtn = e.target.closest('#perm-new-tier-btn');
|
|
1724
|
+
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);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// A2A-48: Sidebar "Add Topic" / "Add Goal" buttons open create dialog
|
|
1735
|
+
const addBtn = e.target.closest('.sidebar-add-btn[data-add-type]');
|
|
1736
|
+
if (addBtn) {
|
|
1737
|
+
const type = addBtn.dataset.addType;
|
|
1738
|
+
const dialog = document.getElementById('create-item-dialog');
|
|
1739
|
+
if (dialog) {
|
|
1740
|
+
dialog.label = `Create New ${type === 'topic' ? 'Topic' : 'Goal'}`;
|
|
1741
|
+
dialog.dataset.createType = type;
|
|
1742
|
+
const titleInput = document.getElementById('create-item-title');
|
|
1743
|
+
const descInput = document.getElementById('create-item-desc');
|
|
1744
|
+
if (titleInput) titleInput.value = '';
|
|
1745
|
+
if (descInput) descInput.value = '';
|
|
1746
|
+
dialog.show();
|
|
1636
1747
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
method: 'PUT',
|
|
1640
|
-
body: JSON.stringify(body)
|
|
1641
|
-
});
|
|
1642
|
-
showNotice(`Saved tier "${tierId}"`);
|
|
1643
|
-
await loadSettings();
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1644
1750
|
});
|
|
1645
1751
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1752
|
+
// A2A-48: Drop zone listeners — bound ONCE here because the zone containers
|
|
1753
|
+
// (#active-topics-zone, #active-goals-zone) persist across renders. Only
|
|
1754
|
+
// their innerHTML is replaced by renderActiveTopics/renderActiveGoals.
|
|
1755
|
+
// Binding in bindSidebarDrag() would cause listener accumulation.
|
|
1756
|
+
const topicZone = document.getElementById('active-topics-zone');
|
|
1757
|
+
const goalZone = document.getElementById('active-goals-zone');
|
|
1758
|
+
[topicZone, goalZone].forEach(zone => {
|
|
1759
|
+
if (!zone) return;
|
|
1760
|
+
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('drag-over'); });
|
|
1761
|
+
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
1762
|
+
zone.addEventListener('drop', (e) => handleZoneDrop(zone, e));
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// A2A-48: Tool toggle change — auto-save and update card styling
|
|
1766
|
+
panel.addEventListener('change', (e) => {
|
|
1767
|
+
const toggle = e.target.closest('#tool-toggles .toggle-switch input');
|
|
1768
|
+
if (toggle) {
|
|
1769
|
+
const card = toggle.closest('.tool-toggle-card');
|
|
1770
|
+
if (card) {
|
|
1771
|
+
card.classList.toggle('enabled', toggle.checked);
|
|
1772
|
+
}
|
|
1773
|
+
autoSaveTier();
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1656
1776
|
});
|
|
1657
1777
|
|
|
1658
|
-
|
|
1778
|
+
// A2A-48: Create Item dialog — submit handler
|
|
1779
|
+
document.getElementById('create-item-submit')?.addEventListener('click', () => {
|
|
1780
|
+
const dialog = document.getElementById('create-item-dialog');
|
|
1781
|
+
const titleInput = document.getElementById('create-item-title');
|
|
1782
|
+
const descInput = document.getElementById('create-item-desc');
|
|
1783
|
+
if (!dialog || !titleInput) return;
|
|
1784
|
+
|
|
1785
|
+
const title = titleInput.value.trim();
|
|
1786
|
+
if (!title) { titleInput.focus(); return; }
|
|
1787
|
+
const desc = descInput?.value?.trim() || '';
|
|
1788
|
+
const type = dialog.dataset.createType || 'topic';
|
|
1789
|
+
|
|
1790
|
+
// A2A-48: Add item to the appropriate active zone
|
|
1791
|
+
const zoneId = type === 'topic' ? 'active-topics-zone' : 'active-goals-zone';
|
|
1792
|
+
const zone = document.getElementById(zoneId);
|
|
1793
|
+
if (!zone) return;
|
|
1794
|
+
|
|
1795
|
+
const accentClass = type === 'topic' ? 'active-item-card--teal' : 'active-item-card--yellow';
|
|
1796
|
+
const typeLabel = type === 'topic' ? 'Topic' : 'Goal';
|
|
1797
|
+
const removeAttr = type === 'topic' ? 'data-remove-topic' : 'data-remove-goal';
|
|
1798
|
+
|
|
1799
|
+
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
|
+
if (placeholder) zone.insertBefore(card, placeholder);
|
|
1814
|
+
else zone.appendChild(card);
|
|
1815
|
+
|
|
1816
|
+
dialog.hide();
|
|
1817
|
+
autoSaveTier();
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
// A2A-48: Create Item dialog — cancel handler
|
|
1821
|
+
document.getElementById('create-item-cancel')?.addEventListener('click', () => {
|
|
1822
|
+
document.getElementById('create-item-dialog')?.hide();
|
|
1823
|
+
});
|
|
1824
|
+
|
|
1825
|
+
// Defaults form — unchanged from A2A-41
|
|
1826
|
+
document.getElementById('defaults-form')?.addEventListener('submit', async (e) => {
|
|
1659
1827
|
e.preventDefault();
|
|
1660
1828
|
await request('/settings/defaults', {
|
|
1661
1829
|
method: 'PUT',
|
|
@@ -1668,11 +1836,8 @@ function bindSettingsActions() {
|
|
|
1668
1836
|
await loadSettings();
|
|
1669
1837
|
});
|
|
1670
1838
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
document.getElementById('new-tier-form').addEventListener('submit', async (e) => {
|
|
1839
|
+
// A2A-48: New Tier form — uses state.activeTierId instead of removed #tier-select
|
|
1840
|
+
document.getElementById('new-tier-form')?.addEventListener('submit', async (e) => {
|
|
1676
1841
|
e.preventDefault();
|
|
1677
1842
|
const tierId = document.getElementById('new-tier-id').value.trim();
|
|
1678
1843
|
const name = document.getElementById('new-tier-name').value.trim();
|
|
@@ -1689,26 +1854,24 @@ function bindSettingsActions() {
|
|
|
1689
1854
|
showNotice(`Created tier "${tierId}"`);
|
|
1690
1855
|
document.getElementById('new-tier-form').reset();
|
|
1691
1856
|
await loadSettings();
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
// A2A-41: toggle for three-column tier view
|
|
1697
|
-
document.getElementById('show-drag-columns')?.addEventListener('sl-change', () => {
|
|
1698
|
-
renderTierColumns();
|
|
1857
|
+
// A2A-48: Switch to the newly created tier (replaces old tier-select.value = tierId)
|
|
1858
|
+
state.activeTierId = tierId;
|
|
1859
|
+
renderPermissions();
|
|
1699
1860
|
});
|
|
1700
1861
|
|
|
1701
|
-
|
|
1862
|
+
// Preview dialog close — unchanged
|
|
1702
1863
|
document.getElementById('preview-close-btn')?.addEventListener('click', () => {
|
|
1703
1864
|
document.getElementById('preview-dialog').hide();
|
|
1704
1865
|
});
|
|
1705
1866
|
}
|
|
1706
1867
|
|
|
1868
|
+
// A2A-48: Load settings and render permissions. Replaces fillTierSelects() and
|
|
1869
|
+
// renderTierColumns() calls with populateInviteTierSelect() + renderPermissions().
|
|
1707
1870
|
async function loadSettings() {
|
|
1708
1871
|
const payload = await request('/settings');
|
|
1709
1872
|
state.settings = payload;
|
|
1710
|
-
|
|
1711
|
-
|
|
1873
|
+
populateInviteTierSelect();
|
|
1874
|
+
renderPermissions();
|
|
1712
1875
|
document.getElementById('defaults-expiration').value = payload.defaults?.expiration || '7d';
|
|
1713
1876
|
document.getElementById('defaults-max-calls').value = payload.defaults?.maxCalls || 100;
|
|
1714
1877
|
}
|
|
@@ -2066,19 +2229,18 @@ function bindLogFilterRefresh() {
|
|
|
2066
2229
|
|
|
2067
2230
|
let pollTimer = null;
|
|
2068
2231
|
|
|
2232
|
+
// A2A-47: Simply return tracked state instead of querying sl-tab-group
|
|
2069
2233
|
function getActiveTab() {
|
|
2070
|
-
|
|
2071
|
-
if (!tabGroup) return 'contacts';
|
|
2072
|
-
// Shoelace tab group: find the active tab by checking which tab has the active attribute
|
|
2073
|
-
const activeTab = tabGroup.querySelector('sl-tab[active]');
|
|
2074
|
-
return activeTab ? activeTab.getAttribute('panel') : 'contacts';
|
|
2234
|
+
return state.activeTab || 'contacts';
|
|
2075
2235
|
}
|
|
2076
2236
|
|
|
2077
2237
|
const tabLoaders = {
|
|
2078
2238
|
contacts: loadContacts,
|
|
2079
2239
|
calls: loadCalls,
|
|
2080
2240
|
logs: () => { loadLogs(); loadLogStats(); },
|
|
2081
|
-
|
|
2241
|
+
// A2A-48: Load fresh settings data when switching to Permissions tab.
|
|
2242
|
+
// Previously a no-op — now ensures data is current on tab switch.
|
|
2243
|
+
permissions: loadSettings,
|
|
2082
2244
|
invites: loadInvites,
|
|
2083
2245
|
health: loadHealth,
|
|
2084
2246
|
};
|
|
@@ -2202,8 +2364,10 @@ function renderHealthHistory(history) {
|
|
|
2202
2364
|
async function bootstrap() {
|
|
2203
2365
|
bindTabs();
|
|
2204
2366
|
bindContactsActions();
|
|
2205
|
-
bindSettingsActions()
|
|
2206
|
-
bindItemListDelegation()
|
|
2367
|
+
// A2A-48: bindPermissionsActions() replaces old bindSettingsActions() +
|
|
2368
|
+
// bindItemListDelegation(). All tier/tool/topic/goal handlers are now
|
|
2369
|
+
// inside bindPermissionsActions() using event delegation on #panel-permissions.
|
|
2370
|
+
bindPermissionsActions();
|
|
2207
2371
|
bindCallbookActions();
|
|
2208
2372
|
bindAutoUpdateActions();
|
|
2209
2373
|
bindInviteActions();
|