a2acalling 0.6.63 → 0.6.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.a2a-manifest.json +2 -2
- package/.maestro/inbox/release-workflow-spam.md +25 -0
- package/CONVENTIONS.md +3 -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 +702 -424
- package/src/dashboard/public/index.html +223 -125
- package/src/dashboard/public/style.css +933 -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,78 @@ 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
|
+
// A2A-50: Added 'settings' panel for relocated admin settings
|
|
259
|
+
const panelTitles = {
|
|
260
|
+
contacts: 'Contacts',
|
|
261
|
+
calls: 'Calls',
|
|
262
|
+
permissions: 'Permissions',
|
|
263
|
+
invites: 'Invites',
|
|
264
|
+
logs: 'Logs',
|
|
265
|
+
health: 'Health',
|
|
266
|
+
settings: 'Settings'
|
|
267
|
+
};
|
|
255
268
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
269
|
+
// A2A-47: Show a specific panel and update sidebar + header state.
|
|
270
|
+
// Replaces the old sl-tab-group navigation.
|
|
271
|
+
function showPanel(name) {
|
|
272
|
+
const validPanels = Object.keys(panelTitles);
|
|
273
|
+
if (!validPanels.includes(name)) name = 'contacts';
|
|
274
|
+
|
|
275
|
+
// Hide all panels, show the target
|
|
276
|
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
|
277
|
+
const target = document.getElementById('panel-' + name);
|
|
278
|
+
if (target) target.classList.add('active');
|
|
279
|
+
|
|
280
|
+
// Update sidebar active state
|
|
281
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
282
|
+
if (item.dataset.panel === name) {
|
|
283
|
+
item.classList.add('active');
|
|
284
|
+
} else {
|
|
285
|
+
item.classList.remove('active');
|
|
286
|
+
}
|
|
260
287
|
});
|
|
261
288
|
|
|
262
|
-
//
|
|
289
|
+
// Update header title
|
|
290
|
+
const titleEl = document.getElementById('section-title');
|
|
291
|
+
if (titleEl) titleEl.textContent = panelTitles[name] || name;
|
|
292
|
+
|
|
293
|
+
// Update state and hash
|
|
294
|
+
state.activeTab = name;
|
|
295
|
+
try {
|
|
296
|
+
if (window.location.hash.slice(1) !== name) {
|
|
297
|
+
window.location.hash = name;
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {}
|
|
300
|
+
|
|
301
|
+
// Trigger data loading for the active tab
|
|
302
|
+
if (typeof onTabSwitch === 'function') onTabSwitch(name);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function bindTabs() {
|
|
306
|
+
// A2A-47: Sidebar nav click handler
|
|
307
|
+
document.querySelectorAll('.nav-item').forEach(item => {
|
|
308
|
+
item.addEventListener('click', (e) => {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
const panel = item.dataset.panel;
|
|
311
|
+
if (panel) showPanel(panel);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Deep-link support: activate the panel matching the URL hash
|
|
263
316
|
const activateFromHash = () => {
|
|
264
317
|
let hash = window.location.hash.slice(1);
|
|
265
|
-
// A2A-
|
|
266
|
-
//
|
|
267
|
-
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).
|
|
268
320
|
if (hash) {
|
|
269
|
-
|
|
270
|
-
try { tabGroup.show(hash); } catch (err) {}
|
|
321
|
+
showPanel(hash);
|
|
271
322
|
}
|
|
272
323
|
};
|
|
273
324
|
|
|
274
325
|
window.addEventListener('hashchange', activateFromHash);
|
|
275
326
|
|
|
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
|
-
}
|
|
327
|
+
// On initial load, activate from hash
|
|
328
|
+
activateFromHash();
|
|
283
329
|
}
|
|
284
330
|
|
|
285
331
|
function norm(value) {
|
|
@@ -393,7 +439,7 @@ function renderContacts() {
|
|
|
393
439
|
</td>
|
|
394
440
|
${locationCell}
|
|
395
441
|
${ownerCell}
|
|
396
|
-
<td>${esc(c?.status || '-')}</td>
|
|
442
|
+
<td><span class="contact-status" data-status="${esc(c?.status || 'unknown')}">${esc(c?.status || '-')}</span></td>
|
|
397
443
|
<td>${esc(String(calls))}</td>
|
|
398
444
|
<td>${esc(lastCallAt)}</td>
|
|
399
445
|
${summaryCell}
|
|
@@ -562,8 +608,8 @@ function bindContactsActions() {
|
|
|
562
608
|
}
|
|
563
609
|
});
|
|
564
610
|
|
|
565
|
-
// Event delegation on the contacts tab panel
|
|
566
|
-
const panel = document.querySelector('
|
|
611
|
+
// A2A-47: Event delegation on the contacts panel (was sl-tab-panel, now div#panel-contacts)
|
|
612
|
+
const panel = document.querySelector('#panel-contacts');
|
|
567
613
|
panel?.addEventListener('click', async (e) => {
|
|
568
614
|
const pinBtn = e.target.closest('[data-pin-contact]');
|
|
569
615
|
if (pinBtn) {
|
|
@@ -1062,7 +1108,7 @@ function renderLogs() {
|
|
|
1062
1108
|
const trace = row.trace_id || '';
|
|
1063
1109
|
tr.innerHTML = `
|
|
1064
1110
|
<td>${esc(fmtDate(row.timestamp))}</td>
|
|
1065
|
-
<td>${esc(row.level || '-')}</td>
|
|
1111
|
+
<td><span class="log-level" data-level="${esc(row.level || '')}">${esc(row.level || '-')}</span></td>
|
|
1066
1112
|
<td>${esc(row.component || '-')}</td>
|
|
1067
1113
|
<td>${esc(row.event || '-')}</td>
|
|
1068
1114
|
<td title="${esc(row.message || '')}">${esc(String(row.message || '').slice(0, 120) || '-')}</td>
|
|
@@ -1117,53 +1163,113 @@ const TOOL_DESCRIPTIONS = {
|
|
|
1117
1163
|
// A2A-41: standard tier order for inheritance. Custom tiers are not in this list.
|
|
1118
1164
|
const TIER_ORDER = ['public', 'friends', 'family'];
|
|
1119
1165
|
|
|
1120
|
-
// A2A-
|
|
1121
|
-
//
|
|
1122
|
-
|
|
1123
|
-
|
|
1166
|
+
// A2A-48: Material icon mapping for tier cards. Standard tiers get recognizable
|
|
1167
|
+
// icons; custom tiers get a wrench icon. Used by renderTierCards().
|
|
1168
|
+
const TIER_ICONS = { public: 'public', friends: 'group', family: 'family_restroom' };
|
|
1169
|
+
|
|
1170
|
+
// A2A-48: Color mapping for tool icons in toggle cards. Gives each tool a
|
|
1171
|
+
// distinct color matching the concept mock's visual differentiation.
|
|
1172
|
+
const TOOL_ICON_MAP = {
|
|
1173
|
+
'Bash': { icon: 'terminal', bg: 'rgba(99,102,241,0.2)', color: '#818CF8', border: 'rgba(99,102,241,0.2)' },
|
|
1174
|
+
'Bash(readonly)': { icon: 'terminal', bg: 'rgba(99,102,241,0.15)', color: '#A5B4FC', border: 'rgba(99,102,241,0.15)' },
|
|
1175
|
+
'Read': { icon: 'visibility', bg: 'rgba(59,130,246,0.2)', color: '#60A5FA', border: 'rgba(59,130,246,0.2)' },
|
|
1176
|
+
'Grep': { icon: 'search', bg: 'rgba(139,92,246,0.2)', color: '#A78BFA', border: 'rgba(139,92,246,0.2)' },
|
|
1177
|
+
'Glob': { icon: 'folder_open', bg: 'rgba(16,185,129,0.2)', color: '#34D399', border: 'rgba(16,185,129,0.2)' },
|
|
1178
|
+
'WebSearch': { icon: 'public', bg: 'rgba(245,158,11,0.2)', color: '#FBBF24', border: 'rgba(245,158,11,0.2)' },
|
|
1179
|
+
'WebFetch': { icon: 'language', bg: 'rgba(236,72,153,0.2)', color: '#F472B6', border: 'rgba(236,72,153,0.2)' }
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
// A2A-48: Renders tier cards grid. Active card gets .active class with glow.
|
|
1183
|
+
function renderTierCards() {
|
|
1184
|
+
const container = document.getElementById('tier-cards');
|
|
1185
|
+
if (!container) return;
|
|
1186
|
+
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => {
|
|
1187
|
+
const aIdx = TIER_ORDER.indexOf(a.id);
|
|
1188
|
+
const bIdx = TIER_ORDER.indexOf(b.id);
|
|
1189
|
+
if (aIdx >= 0 && bIdx >= 0) return aIdx - bIdx;
|
|
1190
|
+
if (aIdx >= 0) return -1;
|
|
1191
|
+
if (bIdx >= 0) return 1;
|
|
1192
|
+
return a.id.localeCompare(b.id);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
container.innerHTML = tiers.map(tier => {
|
|
1196
|
+
const isActive = tier.id === state.activeTierId;
|
|
1197
|
+
const icon = TIER_ICONS[tier.id] || 'build';
|
|
1198
|
+
const iconColor = isActive ? '#60A5FA' : '#6B7280';
|
|
1199
|
+
return `
|
|
1200
|
+
<div class="tier-card${isActive ? ' active' : ''}" data-tier-id="${esc(tier.id)}">
|
|
1201
|
+
<span class="material-symbols-outlined tier-card-icon" style="color:${iconColor};">${icon}</span>
|
|
1202
|
+
<span class="tier-card-name">${esc(tier.name || tier.id)}</span>
|
|
1203
|
+
${isActive ? '<div class="status-dot status-dot--green"></div>' : ''}
|
|
1204
|
+
</div>
|
|
1205
|
+
`;
|
|
1206
|
+
}).join('');
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// A2A-48: Renders tool toggle cards (replaces checkboxes). Each tool is a
|
|
1210
|
+
// glass-panel card with icon, name, description, and a toggle switch.
|
|
1211
|
+
// Toggle change triggers autoSaveTier() for immediate persistence.
|
|
1212
|
+
function renderToolToggles(allowedTools) {
|
|
1213
|
+
const container = document.getElementById('tool-toggles');
|
|
1214
|
+
if (!container) return;
|
|
1124
1215
|
container.innerHTML = Object.entries(TOOL_DESCRIPTIONS).map(([tool, desc]) => {
|
|
1125
|
-
const checked = (allowedTools || []).includes(tool)
|
|
1126
|
-
|
|
1216
|
+
const checked = (allowedTools || []).includes(tool);
|
|
1217
|
+
const iconInfo = TOOL_ICON_MAP[tool] || { icon: 'extension', bg: 'rgba(100,116,139,0.2)', color: '#94A3B8', border: 'rgba(100,116,139,0.2)' };
|
|
1218
|
+
return `
|
|
1219
|
+
<div class="tool-toggle-card${checked ? ' enabled' : ''}">
|
|
1220
|
+
<div class="tool-toggle-info">
|
|
1221
|
+
<div class="tool-icon" style="background:${iconInfo.bg};color:${iconInfo.color};border:1px solid ${iconInfo.border};">
|
|
1222
|
+
<span class="material-symbols-outlined">${iconInfo.icon}</span>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div>
|
|
1225
|
+
<h3>${esc(tool)}</h3>
|
|
1226
|
+
<p>${esc(desc)}</p>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
<label class="toggle-switch">
|
|
1230
|
+
<input type="checkbox" data-tool="${esc(tool)}" ${checked ? 'checked' : ''}>
|
|
1231
|
+
<span class="slider"></span>
|
|
1232
|
+
</label>
|
|
1233
|
+
</div>
|
|
1234
|
+
`;
|
|
1127
1235
|
}).join('');
|
|
1128
1236
|
}
|
|
1129
1237
|
|
|
1130
|
-
// A2A-
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1238
|
+
// A2A-48: Renders active topics in the drop zone as teal-accented cards.
|
|
1239
|
+
// Each card has a close button for removal. Updates #topic-count badge.
|
|
1240
|
+
function renderActiveTopics(tier) {
|
|
1241
|
+
const container = document.getElementById('active-topics-zone');
|
|
1242
|
+
if (!container) return;
|
|
1135
1243
|
const manifestTopics = tier.manifest?.topics || [];
|
|
1136
1244
|
const flatTopics = tier.topics || [];
|
|
1137
1245
|
|
|
1138
|
-
// A2A-41: prefer manifest data (has descriptions), fall back to flat array
|
|
1139
1246
|
const allTopics = manifestTopics.length > 0
|
|
1140
1247
|
? manifestTopics.map(t => ({ label: t.topic, desc: t.description || '' }))
|
|
1141
1248
|
: flatTopics.map(t => ({ label: t, desc: '' }));
|
|
1142
1249
|
|
|
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>
|
|
1250
|
+
const cardsHtml = allTopics.map(t => `
|
|
1251
|
+
<div class="active-item-card active-item-card--teal" data-topic="${esc(t.label)}" data-description="${esc(t.desc)}">
|
|
1252
|
+
<div>
|
|
1253
|
+
<div class="item-name">${esc(t.label)}</div>
|
|
1254
|
+
<div class="item-type-label">Topic</div>
|
|
1156
1255
|
</div>
|
|
1256
|
+
<button class="item-close-btn" data-remove-topic="${esc(t.label)}">
|
|
1257
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1258
|
+
</button>
|
|
1157
1259
|
</div>
|
|
1158
1260
|
`).join('');
|
|
1159
1261
|
|
|
1160
|
-
container.innerHTML =
|
|
1262
|
+
container.innerHTML = cardsHtml + '<div class="drop-placeholder"><span>+ Drop Topic</span></div>';
|
|
1263
|
+
|
|
1264
|
+
const badge = document.getElementById('topic-count');
|
|
1265
|
+
if (badge) badge.textContent = `${allTopics.length} Active`;
|
|
1161
1266
|
}
|
|
1162
1267
|
|
|
1163
|
-
// A2A-
|
|
1164
|
-
//
|
|
1165
|
-
function
|
|
1166
|
-
const container = document.getElementById('
|
|
1268
|
+
// A2A-48: Renders active goals in the drop zone as yellow-accented cards.
|
|
1269
|
+
// Same pattern as topics but with yellow color variant.
|
|
1270
|
+
function renderActiveGoals(tier) {
|
|
1271
|
+
const container = document.getElementById('active-goals-zone');
|
|
1272
|
+
if (!container) return;
|
|
1167
1273
|
const manifestGoals = tier.manifest?.objectives || [];
|
|
1168
1274
|
const flatGoals = tier.goals || [];
|
|
1169
1275
|
|
|
@@ -1171,302 +1277,310 @@ function renderGoalList(tier) {
|
|
|
1171
1277
|
? manifestGoals.map(g => ({ label: g.objective || g.topic, desc: g.description || '' }))
|
|
1172
1278
|
: flatGoals.map(g => ({ label: g, desc: '' }));
|
|
1173
1279
|
|
|
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>
|
|
1280
|
+
const cardsHtml = allGoals.map(g => `
|
|
1281
|
+
<div class="active-item-card active-item-card--yellow" data-topic="${esc(g.label)}" data-description="${esc(g.desc)}">
|
|
1282
|
+
<div>
|
|
1283
|
+
<div class="item-name">${esc(g.label)}</div>
|
|
1284
|
+
<div class="item-type-label">Goal</div>
|
|
1187
1285
|
</div>
|
|
1286
|
+
<button class="item-close-btn" data-remove-goal="${esc(g.label)}">
|
|
1287
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1288
|
+
</button>
|
|
1188
1289
|
</div>
|
|
1189
1290
|
`).join('');
|
|
1190
1291
|
|
|
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
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Delete
|
|
1217
|
-
const deleteBtn = e.target.closest('.topic-delete-btn');
|
|
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
|
-
});
|
|
1292
|
+
container.innerHTML = cardsHtml + '<div class="drop-placeholder"><span>+ Drop Goal</span></div>';
|
|
1280
1293
|
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
});
|
|
1294
|
+
const badge = document.getElementById('goal-count');
|
|
1295
|
+
if (badge) badge.textContent = `${allGoals.length} Active`;
|
|
1291
1296
|
}
|
|
1292
1297
|
|
|
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);
|
|
1298
|
+
// A2A-48: Orchestrator that renders the entire permissions panel.
|
|
1299
|
+
// Uses state.activeTierId instead of reading a dropdown value.
|
|
1300
|
+
function renderPermissions() {
|
|
1301
|
+
const tier = (state.settings?.tiers || []).find(t => t.id === state.activeTierId);
|
|
1322
1302
|
if (!tier) return;
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
renderTopicList(tier);
|
|
1328
|
-
renderGoalList(tier);
|
|
1303
|
+
renderTierCards();
|
|
1304
|
+
renderActiveTopics(tier);
|
|
1305
|
+
renderActiveGoals(tier);
|
|
1306
|
+
renderToolToggles(tier.allowed_tools);
|
|
1329
1307
|
renderTierWarnings(tier);
|
|
1330
|
-
|
|
1308
|
+
renderSidebarPreview(state.activeTierId);
|
|
1309
|
+
renderSidebarLists(tier);
|
|
1310
|
+
bindSidebarDrag();
|
|
1331
1311
|
}
|
|
1332
1312
|
|
|
1333
|
-
// A2A-
|
|
1334
|
-
//
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
function renderTierColumns() {
|
|
1338
|
-
const container = document.getElementById('tier-columns');
|
|
1313
|
+
// A2A-48: Renders the inline "Preview as Caller" card in the right sidebar.
|
|
1314
|
+
// Reuses getPreviewData() to show merged topics, goals, and tool count.
|
|
1315
|
+
function renderSidebarPreview(tierId) {
|
|
1316
|
+
const container = document.getElementById('perm-preview');
|
|
1339
1317
|
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
|
-
|
|
1318
|
+
const data = getPreviewData(tierId);
|
|
1319
|
+
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
1320
|
+
const topicNames = data.topics.map(t => t.topic).filter(Boolean);
|
|
1321
|
+
const goalNames = data.objectives.map(g => g.objective || g.topic).filter(Boolean);
|
|
1322
|
+
const toolCount = data.tools.size;
|
|
1323
|
+
|
|
1324
|
+
const topicText = topicNames.length > 0 ? `<strong style="color:#2DD4BF;">${esc(topicNames.join(', '))}</strong>` : '<em>no topics</em>';
|
|
1325
|
+
const goalText = goalNames.length > 0 ? `<strong style="color:#FBBF24;">${esc(goalNames.join(', '))}</strong>` : '<em>no goals</em>';
|
|
1326
|
+
|
|
1327
|
+
container.innerHTML = `
|
|
1328
|
+
<div class="preview-card-inner">
|
|
1329
|
+
<div class="sidebar-list-header">Preview as Caller</div>
|
|
1330
|
+
<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.5rem;">
|
|
1331
|
+
<span class="material-symbols-outlined" style="color:#60A5FA;">smart_toy</span>
|
|
1332
|
+
<div>
|
|
1333
|
+
<div style="font-weight:600;font-size:0.85rem;color:var(--ink);">Agent Permission</div>
|
|
1334
|
+
<span class="preview-tier-badge">${esc(tierName)} Tier</span>
|
|
1335
|
+
</div>
|
|
1336
|
+
</div>
|
|
1337
|
+
<div class="preview-summary">
|
|
1338
|
+
This agent can discuss ${topicText} to help ${goalText} using <strong>${toolCount} tool${toolCount !== 1 ? 's' : ''}</strong>.
|
|
1339
|
+
</div>
|
|
1340
|
+
<div class="preview-footer">
|
|
1341
|
+
<span style="display:flex;align-items:center;gap:0.3rem;">
|
|
1342
|
+
<span class="status-dot status-dot--green"></span> Active
|
|
1343
|
+
</span>
|
|
1344
|
+
<span style="font-family:monospace;opacity:0.7;">JSON Valid</span>
|
|
1345
|
+
</div>
|
|
1346
|
+
</div>
|
|
1347
|
+
`;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// A2A-48: Renders all topics and goals in the right sidebar as draggable items.
|
|
1351
|
+
// Items active in the current tier are dimmed with an "(Active)" label.
|
|
1352
|
+
function renderSidebarLists(tier) {
|
|
1353
|
+
const topicContainer = document.getElementById('sidebar-topics');
|
|
1354
|
+
const goalContainer = document.getElementById('sidebar-goals');
|
|
1355
|
+
if (!topicContainer || !goalContainer) return;
|
|
1356
|
+
|
|
1357
|
+
// Collect ALL topics across ALL tiers for the sidebar
|
|
1358
|
+
const allTiers = state.settings?.tiers || [];
|
|
1359
|
+
const allTopicMap = new Map();
|
|
1360
|
+
const allGoalMap = new Map();
|
|
1361
|
+
for (const t of allTiers) {
|
|
1362
|
+
const mTopics = t.manifest?.topics || [];
|
|
1363
|
+
const fTopics = t.topics || [];
|
|
1364
|
+
const topics = mTopics.length > 0 ? mTopics : fTopics.map(x => ({ topic: x, description: '' }));
|
|
1365
|
+
for (const item of topics) {
|
|
1366
|
+
if (item.topic && !allTopicMap.has(item.topic)) {
|
|
1367
|
+
allTopicMap.set(item.topic, item.description || '');
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const mGoals = t.manifest?.objectives || [];
|
|
1371
|
+
const fGoals = t.goals || [];
|
|
1372
|
+
const goals = mGoals.length > 0 ? mGoals : fGoals.map(x => ({ topic: x, description: '' }));
|
|
1373
|
+
for (const item of goals) {
|
|
1374
|
+
const label = item.objective || item.topic;
|
|
1375
|
+
if (label && !allGoalMap.has(label)) {
|
|
1376
|
+
allGoalMap.set(label, item.description || '');
|
|
1377
|
+
}
|
|
1370
1378
|
}
|
|
1379
|
+
}
|
|
1371
1380
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1381
|
+
// Determine which are active in the current tier
|
|
1382
|
+
const activeTopicSet = new Set((tier.topics || []).concat(
|
|
1383
|
+
(tier.manifest?.topics || []).map(t => t.topic)
|
|
1384
|
+
).filter(Boolean));
|
|
1385
|
+
const activeGoalSet = new Set((tier.goals || []).concat(
|
|
1386
|
+
(tier.manifest?.objectives || []).map(g => g.objective || g.topic)
|
|
1387
|
+
).filter(Boolean));
|
|
1388
|
+
|
|
1389
|
+
// Render topics sidebar
|
|
1390
|
+
const topicItems = Array.from(allTopicMap.entries()).map(([name, desc]) => {
|
|
1391
|
+
const isActive = activeTopicSet.has(name);
|
|
1392
|
+
return `
|
|
1393
|
+
<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">
|
|
1394
|
+
<div style="display:flex;align-items:center;gap:0.4rem;">
|
|
1395
|
+
${isActive ? '' : '<span class="material-symbols-outlined" style="color:#4B5563;font-size:1rem;cursor:grab;">drag_indicator</span>'}
|
|
1396
|
+
<span class="sidebar-item-name">${esc(name)}${isActive ? ' <span class="sidebar-item-active-label">(Active)</span>' : ''}</span>
|
|
1383
1397
|
</div>
|
|
1384
1398
|
</div>
|
|
1385
|
-
|
|
1399
|
+
`;
|
|
1400
|
+
}).join('');
|
|
1401
|
+
|
|
1402
|
+
topicContainer.innerHTML = `
|
|
1403
|
+
<div class="sidebar-list-header">Topics</div>
|
|
1404
|
+
${topicItems}
|
|
1405
|
+
<button class="sidebar-add-btn" data-add-type="topic">
|
|
1406
|
+
<span class="material-symbols-outlined" style="font-size:14px;">add</span> Add Topic
|
|
1407
|
+
</button>
|
|
1408
|
+
`;
|
|
1386
1409
|
|
|
1410
|
+
// Render goals sidebar
|
|
1411
|
+
const goalItems = Array.from(allGoalMap.entries()).map(([name, desc]) => {
|
|
1412
|
+
const isActive = activeGoalSet.has(name);
|
|
1387
1413
|
return `
|
|
1388
|
-
<div class="
|
|
1389
|
-
<
|
|
1390
|
-
|
|
1391
|
-
|
|
1414
|
+
<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">
|
|
1415
|
+
<div style="display:flex;align-items:center;gap:0.4rem;">
|
|
1416
|
+
${isActive ? '' : '<span class="material-symbols-outlined" style="color:#4B5563;font-size:1rem;cursor:grab;">drag_indicator</span>'}
|
|
1417
|
+
<span class="sidebar-item-name">${esc(name)}${isActive ? ' <span class="sidebar-item-active-label">(Active)</span>' : ''}</span>
|
|
1392
1418
|
</div>
|
|
1393
|
-
</div
|
|
1419
|
+
</div>
|
|
1420
|
+
`;
|
|
1394
1421
|
}).join('');
|
|
1395
1422
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1423
|
+
goalContainer.innerHTML = `
|
|
1424
|
+
<div class="sidebar-list-header">Goals & Objectives</div>
|
|
1425
|
+
${goalItems}
|
|
1426
|
+
<button class="sidebar-add-btn" data-add-type="goal">
|
|
1427
|
+
<span class="material-symbols-outlined" style="font-size:14px;">add</span> Add Goal
|
|
1428
|
+
</button>
|
|
1429
|
+
`;
|
|
1398
1430
|
}
|
|
1399
1431
|
|
|
1400
|
-
// A2A-
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
function
|
|
1404
|
-
|
|
1432
|
+
// A2A-48: Debounced auto-save replaces explicit Save Tier button.
|
|
1433
|
+
// 250ms delay prevents excessive API calls during rapid changes.
|
|
1434
|
+
let _autoSaveTimer = null;
|
|
1435
|
+
function autoSaveTier() {
|
|
1436
|
+
clearTimeout(_autoSaveTimer);
|
|
1437
|
+
_autoSaveTimer = setTimeout(async () => {
|
|
1438
|
+
const tierId = state.activeTierId;
|
|
1439
|
+
if (!tierId) return;
|
|
1440
|
+
|
|
1441
|
+
// Collect tools from toggle states
|
|
1442
|
+
const toggles = document.querySelectorAll('#tool-toggles .toggle-switch input');
|
|
1443
|
+
const allowed_tools = Array.from(toggles).filter(t => t.checked).map(t => t.dataset.tool);
|
|
1444
|
+
|
|
1445
|
+
// Collect topics from active zone
|
|
1446
|
+
const topicCards = document.querySelectorAll('#active-topics-zone .active-item-card');
|
|
1447
|
+
// A2A-48: uses dataset.topic for BOTH topics and goals (NOT dataset.objective)
|
|
1448
|
+
// because parseTopicObjects() in dashboard.js:160 only reads entry.topic.
|
|
1449
|
+
// The semantic distinction (objective vs topic) is UI-only; storage layer
|
|
1450
|
+
// uses {topic, description} uniformly for both types.
|
|
1451
|
+
const topics = Array.from(topicCards).map(c => c.dataset.topic).filter(Boolean);
|
|
1452
|
+
const manifestTopics = Array.from(topicCards).map(c => ({
|
|
1453
|
+
topic: c.dataset.topic,
|
|
1454
|
+
description: c.dataset.description || ''
|
|
1455
|
+
})).filter(t => t.topic);
|
|
1456
|
+
|
|
1457
|
+
// Collect goals from active zone
|
|
1458
|
+
const goalCards = document.querySelectorAll('#active-goals-zone .active-item-card');
|
|
1459
|
+
const goals = Array.from(goalCards).map(c => c.dataset.topic).filter(Boolean);
|
|
1460
|
+
const manifestObjectives = Array.from(goalCards).map(c => ({
|
|
1461
|
+
topic: c.dataset.topic,
|
|
1462
|
+
description: c.dataset.description || ''
|
|
1463
|
+
})).filter(g => g.topic);
|
|
1464
|
+
|
|
1465
|
+
const body = { allowed_tools, topics, goals, manifest: { topics: manifestTopics, objectives: manifestObjectives } };
|
|
1466
|
+
// A2A-48: Refresh state inside try block so that a failed PUT does not
|
|
1467
|
+
// trigger an unhandled rejection from the subsequent GET.
|
|
1468
|
+
try {
|
|
1469
|
+
await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
|
|
1470
|
+
method: 'PUT', body: JSON.stringify(body)
|
|
1471
|
+
});
|
|
1472
|
+
showNotice('Saved');
|
|
1473
|
+
// Refresh state from server to stay in sync after auto-save
|
|
1474
|
+
const payload = await request('/settings');
|
|
1475
|
+
state.settings = payload;
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
showNotice(`Save failed: ${err.message}`);
|
|
1478
|
+
}
|
|
1479
|
+
}, 250);
|
|
1480
|
+
}
|
|
1405
1481
|
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1482
|
+
// A2A-48: Binds dragstart/dragend on sidebar items (re-created each render).
|
|
1483
|
+
// Zone listeners (dragover/dragleave/drop) are bound ONCE in
|
|
1484
|
+
// bindPermissionsActions() to avoid listener accumulation — the zone
|
|
1485
|
+
// containers persist across renders while only their innerHTML changes.
|
|
1486
|
+
function bindSidebarDrag() {
|
|
1487
|
+
document.querySelectorAll('.sidebar-item[draggable="true"]').forEach(item => {
|
|
1488
|
+
item.addEventListener('dragstart', (e) => {
|
|
1489
|
+
const itemType = item.dataset.itemType || 'topic';
|
|
1490
|
+
const name = item.dataset.sidebarTopic || item.dataset.sidebarGoal || '';
|
|
1491
|
+
const desc = item.dataset.description || '';
|
|
1492
|
+
e.dataTransfer.setData('application/json', JSON.stringify({ name, description: desc, type: itemType }));
|
|
1493
|
+
item.style.opacity = '0.5';
|
|
1413
1494
|
});
|
|
1414
|
-
|
|
1495
|
+
item.addEventListener('dragend', () => { item.style.opacity = ''; });
|
|
1415
1496
|
});
|
|
1497
|
+
}
|
|
1416
1498
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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.
|
|
1503
|
+
function handleZoneDrop(zone, e) {
|
|
1504
|
+
e.preventDefault();
|
|
1505
|
+
zone.classList.remove('drag-over');
|
|
1506
|
+
let data;
|
|
1507
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
1508
|
+
if (!data.name) return;
|
|
1509
|
+
|
|
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;
|
|
1516
|
+
|
|
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');
|
|
1523
|
+
for (const card of existing) {
|
|
1524
|
+
if (card.dataset.topic === data.name) return; // already active
|
|
1525
|
+
}
|
|
1426
1526
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
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();
|
|
1431
1533
|
|
|
1432
|
-
|
|
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) {
|
|
1546
|
+
const card = document.createElement('div');
|
|
1547
|
+
card.className = `active-item-card ${accentClass}`;
|
|
1548
|
+
card.dataset.topic = name;
|
|
1549
|
+
card.dataset.description = description;
|
|
1550
|
+
card.innerHTML = `
|
|
1551
|
+
<div>
|
|
1552
|
+
<div class="item-name">${esc(name)}</div>
|
|
1553
|
+
<div class="item-type-label">${typeLabel}</div>
|
|
1554
|
+
</div>
|
|
1555
|
+
<button class="item-close-btn" ${removeAttr}="${esc(name)}">
|
|
1556
|
+
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
|
|
1557
|
+
</button>
|
|
1558
|
+
`;
|
|
1559
|
+
return card;
|
|
1560
|
+
}
|
|
1433
1561
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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).
|
|
1565
|
+
function populateInviteTierSelect() {
|
|
1566
|
+
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1567
|
+
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1568
|
+
const newTierDialogCopy = document.getElementById('new-tier-dialog-copy-from');
|
|
1569
|
+
const inviteTier = document.getElementById('invite-tier');
|
|
1437
1570
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
const targetManifestTopics = [...(targetTierData.manifest?.topics || []), movedManifest || { topic, description: '' }];
|
|
1571
|
+
const optionsHtml = tiers.map(tier => {
|
|
1572
|
+
const emoji = TIER_EMOJIS[tier.id] || '\u{1F527}';
|
|
1573
|
+
return `<sl-option value="${esc(tier.id)}">${emoji} ${esc(tier.name || tier.id)}</sl-option>`;
|
|
1574
|
+
}).join('');
|
|
1443
1575
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
-
});
|
|
1576
|
+
if (inviteTier) inviteTier.innerHTML = optionsHtml;
|
|
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}`;
|
|
1580
|
+
|
|
1581
|
+
// A2A-48: Default invite tier to 'public'
|
|
1582
|
+
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
1583
|
+
if (defaultTier && inviteTier) inviteTier.value = defaultTier;
|
|
1470
1584
|
}
|
|
1471
1585
|
|
|
1472
1586
|
// A2A-41: contextual validation warnings for the currently selected tier.
|
|
@@ -1543,10 +1657,10 @@ function getPreviewData(tierId) {
|
|
|
1543
1657
|
return merged;
|
|
1544
1658
|
}
|
|
1545
1659
|
|
|
1546
|
-
// A2A-
|
|
1547
|
-
// for the selected tier.
|
|
1660
|
+
// A2A-48: opens the caller preview dialog showing the merged effective view
|
|
1661
|
+
// for the selected tier. Uses state.activeTierId instead of removed #tier-select.
|
|
1548
1662
|
function openCallerPreview() {
|
|
1549
|
-
const tierId =
|
|
1663
|
+
const tierId = state.activeTierId;
|
|
1550
1664
|
const data = getPreviewData(tierId);
|
|
1551
1665
|
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
1552
1666
|
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
@@ -1590,125 +1704,235 @@ function openCallerPreview() {
|
|
|
1590
1704
|
dialog.show();
|
|
1591
1705
|
}
|
|
1592
1706
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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
|
+
}
|
|
1597
1723
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
}
|
|
1724
|
+
// A2A-48: Binds all event handlers for the permissions panel. Replaces old
|
|
1725
|
+
// bindSettingsActions() — removes handlers for deleted elements (tier-form,
|
|
1726
|
+
// tier-select, copy-tier-btn, show-drag-columns, preview-caller-btn) and
|
|
1727
|
+
// adds handlers for tier cards, tool toggles, close buttons, and sidebar.
|
|
1728
|
+
function bindPermissionsActions() {
|
|
1729
|
+
const panel = document.getElementById('panel-permissions');
|
|
1730
|
+
if (!panel) return;
|
|
1731
|
+
|
|
1732
|
+
// A2A-48: Tier card click — switch active tier and re-render.
|
|
1733
|
+
// No autoSaveTier() here: switching tiers is a read operation, not a write.
|
|
1734
|
+
panel.addEventListener('click', (e) => {
|
|
1735
|
+
const card = e.target.closest('.tier-card[data-tier-id]');
|
|
1736
|
+
if (card) {
|
|
1737
|
+
state.activeTierId = card.dataset.tierId;
|
|
1738
|
+
renderPermissions();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1615
1741
|
|
|
1616
|
-
// A2A-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
}
|
|
1742
|
+
// A2A-50: Close button on active topic cards opens delete confirmation dialog
|
|
1743
|
+
const removeTopic = e.target.closest('[data-remove-topic]');
|
|
1744
|
+
if (removeTopic) {
|
|
1745
|
+
const card = removeTopic.closest('.active-item-card');
|
|
1746
|
+
if (card) {
|
|
1747
|
+
const itemName = card.dataset.topic || '';
|
|
1748
|
+
showDeleteConfirm(itemName, 'topic', card);
|
|
1749
|
+
}
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1626
1752
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
topics: manifestTopics,
|
|
1635
|
-
objectives: manifestObjectives
|
|
1753
|
+
// A2A-50: Close button on active goal cards opens delete confirmation dialog
|
|
1754
|
+
const removeGoal = e.target.closest('[data-remove-goal]');
|
|
1755
|
+
if (removeGoal) {
|
|
1756
|
+
const card = removeGoal.closest('.active-item-card');
|
|
1757
|
+
if (card) {
|
|
1758
|
+
const itemName = card.dataset.topic || '';
|
|
1759
|
+
showDeleteConfirm(itemName, 'goal', card);
|
|
1636
1760
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
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.
|
|
1766
|
+
const newTierBtn = e.target.closest('#perm-new-tier-btn');
|
|
1767
|
+
if (newTierBtn) {
|
|
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
|
+
}
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// A2A-48: Sidebar "Add Topic" / "Add Goal" buttons open create dialog
|
|
1780
|
+
const addBtn = e.target.closest('.sidebar-add-btn[data-add-type]');
|
|
1781
|
+
if (addBtn) {
|
|
1782
|
+
const type = addBtn.dataset.addType;
|
|
1783
|
+
const dialog = document.getElementById('create-item-dialog');
|
|
1784
|
+
if (dialog) {
|
|
1785
|
+
dialog.label = `Create New ${type === 'topic' ? 'Topic' : 'Goal'}`;
|
|
1786
|
+
dialog.dataset.createType = type;
|
|
1787
|
+
const titleInput = document.getElementById('create-item-title');
|
|
1788
|
+
const descInput = document.getElementById('create-item-desc');
|
|
1789
|
+
if (titleInput) titleInput.value = '';
|
|
1790
|
+
if (descInput) descInput.value = '';
|
|
1791
|
+
dialog.show();
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1644
1795
|
});
|
|
1645
1796
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1797
|
+
// A2A-48: Drop zone listeners — bound ONCE here because the zone containers
|
|
1798
|
+
// (#active-topics-zone, #active-goals-zone) persist across renders. Only
|
|
1799
|
+
// their innerHTML is replaced by renderActiveTopics/renderActiveGoals.
|
|
1800
|
+
// Binding in bindSidebarDrag() would cause listener accumulation.
|
|
1801
|
+
const topicZone = document.getElementById('active-topics-zone');
|
|
1802
|
+
const goalZone = document.getElementById('active-goals-zone');
|
|
1803
|
+
[topicZone, goalZone].forEach(zone => {
|
|
1804
|
+
if (!zone) return;
|
|
1805
|
+
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('drag-over'); });
|
|
1806
|
+
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
1807
|
+
zone.addEventListener('drop', (e) => handleZoneDrop(zone, e));
|
|
1656
1808
|
});
|
|
1657
1809
|
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1810
|
+
// A2A-48: Tool toggle change — auto-save and update card styling
|
|
1811
|
+
panel.addEventListener('change', (e) => {
|
|
1812
|
+
const toggle = e.target.closest('#tool-toggles .toggle-switch input');
|
|
1813
|
+
if (toggle) {
|
|
1814
|
+
const card = toggle.closest('.tool-toggle-card');
|
|
1815
|
+
if (card) {
|
|
1816
|
+
card.classList.toggle('enabled', toggle.checked);
|
|
1817
|
+
}
|
|
1818
|
+
autoSaveTier();
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1669
1821
|
});
|
|
1670
1822
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1823
|
+
// A2A-50: Create Item dialog — submit handler. Uses shared buildItemCard()
|
|
1824
|
+
// helper to avoid duplicating card HTML with handleZoneDrop().
|
|
1825
|
+
document.getElementById('create-item-submit')?.addEventListener('click', () => {
|
|
1826
|
+
const dialog = document.getElementById('create-item-dialog');
|
|
1827
|
+
const titleInput = document.getElementById('create-item-title');
|
|
1828
|
+
const descInput = document.getElementById('create-item-desc');
|
|
1829
|
+
if (!dialog || !titleInput) return;
|
|
1830
|
+
|
|
1831
|
+
const title = titleInput.value.trim();
|
|
1832
|
+
if (!title) { titleInput.focus(); return; }
|
|
1833
|
+
const desc = descInput?.value?.trim() || '';
|
|
1834
|
+
const type = dialog.dataset.createType || 'topic';
|
|
1835
|
+
|
|
1836
|
+
const zoneId = type === 'topic' ? 'active-topics-zone' : 'active-goals-zone';
|
|
1837
|
+
const zone = document.getElementById(zoneId);
|
|
1838
|
+
if (!zone) return;
|
|
1839
|
+
|
|
1840
|
+
const accentClass = type === 'topic' ? 'active-item-card--teal' : 'active-item-card--yellow';
|
|
1841
|
+
const typeLabel = type === 'topic' ? 'Topic' : 'Goal';
|
|
1842
|
+
const removeAttr = type === 'topic' ? 'data-remove-topic' : 'data-remove-goal';
|
|
1843
|
+
|
|
1844
|
+
const card = buildItemCard(title, desc, accentClass, typeLabel, removeAttr);
|
|
1845
|
+
const placeholder = zone.querySelector('.drop-placeholder');
|
|
1846
|
+
if (placeholder) zone.insertBefore(card, placeholder);
|
|
1847
|
+
else zone.appendChild(card);
|
|
1848
|
+
|
|
1849
|
+
dialog.hide();
|
|
1850
|
+
autoSaveTier();
|
|
1673
1851
|
});
|
|
1674
1852
|
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const name = document.getElementById('new-tier-name').value.trim();
|
|
1679
|
-
const copyFrom = document.getElementById('new-tier-copy-from').value;
|
|
1680
|
-
if (!tierId) return;
|
|
1681
|
-
await request('/settings/tiers', {
|
|
1682
|
-
method: 'POST',
|
|
1683
|
-
body: JSON.stringify({
|
|
1684
|
-
id: tierId,
|
|
1685
|
-
name: name || tierId,
|
|
1686
|
-
copy_from: copyFrom || undefined
|
|
1687
|
-
})
|
|
1688
|
-
});
|
|
1689
|
-
showNotice(`Created tier "${tierId}"`);
|
|
1690
|
-
document.getElementById('new-tier-form').reset();
|
|
1691
|
-
await loadSettings();
|
|
1692
|
-
document.getElementById('tier-select').value = tierId;
|
|
1693
|
-
renderTierEditor(tierId);
|
|
1853
|
+
// A2A-48: Create Item dialog — cancel handler
|
|
1854
|
+
document.getElementById('create-item-cancel')?.addEventListener('click', () => {
|
|
1855
|
+
document.getElementById('create-item-dialog')?.hide();
|
|
1694
1856
|
});
|
|
1695
1857
|
|
|
1696
|
-
// A2A-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
// A2A-50: New Tier dialog — cancel handler
|
|
1888
|
+
document.getElementById('new-tier-dialog-cancel')?.addEventListener('click', () => {
|
|
1889
|
+
document.getElementById('new-tier-dialog')?.hide();
|
|
1699
1890
|
});
|
|
1700
1891
|
|
|
1701
|
-
|
|
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
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
// Preview dialog close — unchanged
|
|
1702
1924
|
document.getElementById('preview-close-btn')?.addEventListener('click', () => {
|
|
1703
1925
|
document.getElementById('preview-dialog').hide();
|
|
1704
1926
|
});
|
|
1705
1927
|
}
|
|
1706
1928
|
|
|
1929
|
+
// A2A-48: Load settings and render permissions. Replaces fillTierSelects() and
|
|
1930
|
+
// renderTierColumns() calls with populateInviteTierSelect() + renderPermissions().
|
|
1707
1931
|
async function loadSettings() {
|
|
1708
1932
|
const payload = await request('/settings');
|
|
1709
1933
|
state.settings = payload;
|
|
1710
|
-
|
|
1711
|
-
|
|
1934
|
+
populateInviteTierSelect();
|
|
1935
|
+
renderPermissions();
|
|
1712
1936
|
document.getElementById('defaults-expiration').value = payload.defaults?.expiration || '7d';
|
|
1713
1937
|
document.getElementById('defaults-max-calls').value = payload.defaults?.maxCalls || 100;
|
|
1714
1938
|
}
|
|
@@ -2066,21 +2290,22 @@ function bindLogFilterRefresh() {
|
|
|
2066
2290
|
|
|
2067
2291
|
let pollTimer = null;
|
|
2068
2292
|
|
|
2293
|
+
// A2A-47: Simply return tracked state instead of querying sl-tab-group
|
|
2069
2294
|
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';
|
|
2295
|
+
return state.activeTab || 'contacts';
|
|
2075
2296
|
}
|
|
2076
2297
|
|
|
2077
2298
|
const tabLoaders = {
|
|
2078
2299
|
contacts: loadContacts,
|
|
2079
2300
|
calls: loadCalls,
|
|
2080
2301
|
logs: () => { loadLogs(); loadLogStats(); },
|
|
2081
|
-
|
|
2302
|
+
// A2A-48: Load fresh settings data when switching to Permissions tab.
|
|
2303
|
+
// Previously a no-op — now ensures data is current on tab switch.
|
|
2304
|
+
permissions: loadSettings,
|
|
2082
2305
|
invites: loadInvites,
|
|
2083
2306
|
health: loadHealth,
|
|
2307
|
+
// A2A-50: Settings tab loads dashboard status, auto-update, and callbook data
|
|
2308
|
+
settings: () => { loadDashboardStatus(); loadAutoUpdateStatus(); loadCallbookDevices(); },
|
|
2084
2309
|
};
|
|
2085
2310
|
|
|
2086
2311
|
function startPolling() {
|
|
@@ -2199,11 +2424,64 @@ function renderHealthHistory(history) {
|
|
|
2199
2424
|
}).join('');
|
|
2200
2425
|
}
|
|
2201
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
|
+
|
|
2202
2476
|
async function bootstrap() {
|
|
2203
2477
|
bindTabs();
|
|
2204
2478
|
bindContactsActions();
|
|
2479
|
+
// A2A-48: bindPermissionsActions() replaces old bindSettingsActions() +
|
|
2480
|
+
// bindItemListDelegation(). All tier/tool/topic/goal handlers are now
|
|
2481
|
+
// inside bindPermissionsActions() using event delegation on #panel-permissions.
|
|
2482
|
+
bindPermissionsActions();
|
|
2483
|
+
// A2A-50: Settings panel handlers (defaults, new-tier inline form)
|
|
2205
2484
|
bindSettingsActions();
|
|
2206
|
-
bindItemListDelegation();
|
|
2207
2485
|
bindCallbookActions();
|
|
2208
2486
|
bindAutoUpdateActions();
|
|
2209
2487
|
bindInviteActions();
|