diff --git a/hbd/server/templates/settings.html b/hbd/server/templates/settings.html
index d910644..1f35e89 100644
--- a/hbd/server/templates/settings.html
+++ b/hbd/server/templates/settings.html
@@ -382,9 +382,65 @@
.modal-box h3 { margin: 0 0 12px; font-size: 1em; }
.backup-row { display: flex; align-items: center; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: .87em; }
.backup-row:last-child { border-bottom: none; }
+
+ /* ---- Multi-picker ---- */
+ .mpick-wrapper { display: block; }
+ .mpick-display {
+ display: flex; align-items: center; gap: 4px; flex-wrap: nowrap;
+ cursor: pointer; padding: 3px 7px; border: 1px solid #ccc; border-radius: 4px;
+ min-height: 26px; min-width: 80px; width: 100%; box-sizing: border-box;
+ background: #fff; user-select: none; overflow: hidden;
+ }
+ .mpick-display:hover { border-color: #0066cc; background: #f8fbff; }
+ .mpick-tag {
+ padding: 1px 6px; background: #e8eaf6; color: #283593;
+ border-radius: 10px; font-size: 0.82em; white-space: nowrap; flex-shrink: 0;
+ }
+ .mpick-more { color: #888; font-size: 0.82em; white-space: nowrap; flex-shrink: 0; }
+ .mpick-empty { color: #bbb; font-style: italic; font-size: 0.82em; }
+ .mpick-panel {
+ position: fixed; background: #fff; border: 1px solid #d0d0d0;
+ border-radius: 6px; box-shadow: 0 4px 20px rgba(0,0,0,.18);
+ z-index: 2000; width: 360px; overflow: hidden;
+ }
+ .mpick-panel-header {
+ padding: 6px 12px; font-size: 0.78em; font-weight: 700;
+ text-transform: uppercase; letter-spacing: 0.04em; color: #555;
+ border-bottom: 1px solid #eee; display: flex;
+ justify-content: space-between; align-items: center; background: #f5f5f5;
+ }
+ .mpick-panel-body { display: flex; }
+ .mpick-col { flex: 1; min-width: 0; max-height: 200px; overflow-y: auto; }
+ .mpick-col-header {
+ padding: 4px 10px; font-size: 0.72em; font-weight: 700;
+ text-transform: uppercase; letter-spacing: 0.04em; color: #888;
+ border-bottom: 1px solid #f0f0f0; background: #fafafa;
+ position: sticky; top: 0; z-index: 1;
+ }
+ .mpick-col:first-child { border-right: 1px solid #eee; }
+ .mpick-item {
+ padding: 5px 10px; font-size: 0.85em; cursor: pointer;
+ display: flex; align-items: center; justify-content: space-between;
+ border-bottom: 1px solid #f8f8f8; gap: 4px;
+ }
+ .mpick-item:last-child { border-bottom: none; }
+ .mpick-item-avail:hover { background: #e8f5e9; }
+ .mpick-item-sel:hover { background: #fce4ec; }
+ .mpick-arrow { font-size: 1.1em; opacity: 0.4; flex-shrink: 0; line-height: 1; }
+ .mpick-item:hover .mpick-arrow { opacity: 1; }
+ .mpick-item-avail .mpick-arrow { color: #2a7a2a; }
+ .mpick-item-sel .mpick-arrow { color: #c62828; }
+ .mpick-panel-footer {
+ padding: 6px 10px; border-top: 1px solid #eee;
+ display: flex; justify-content: flex-end; background: #f8f8f8;
+ }
+ .mpick-none { padding: 10px; font-size: .82em; color: #aaa; text-align: center; }
+ {%- macro mpick(all_items, sel, cls) -%}
+
@@ -501,11 +557,7 @@
-
- {% for ch in all_channel_names %}
- {{ ch | e }}
- {% endfor %}
-
+ {{ mpick(all_channel_names, u.notification_channels, 'user-ch-select') }}
✕
@@ -576,34 +628,10 @@
-
-
- {% for u in all_usernames %}
- {{ u | e }}
- {% endfor %}
-
-
-
-
- {% for u in all_usernames %}
- {{ u | e }}
- {% endfor %}
-
-
-
-
- {% for tc in all_threshold_configs %}
- {{ tc | e }}
- {% endfor %}
-
-
-
-
- {% for ch in all_channel_names %}
- {{ ch | e }}
- {% endfor %}
-
-
+
{{ mpick(all_usernames, h.managers, 'host-managers') }}
+
{{ mpick(all_usernames, h.monitors, 'host-monitors') }}
+
{{ mpick(all_threshold_configs, h.threshold_configs, 'host-tc') }}
+
{{ mpick(all_channel_names, h.notification_channels, 'host-channels') }}
✕
{% endfor %}
@@ -1008,10 +1036,6 @@
function addHostRow() {
const tbody = document.getElementById('hosts-tbody');
- const sz = n => Math.min(Math.max(n, 1), 4);
- const usersOpts = _allUsers.map(u => `
${escHtml(u)} `).join('');
- const tcOpts = _allThresholdConfigs.map(t => `
${escHtml(t)} `).join('');
- const chOpts = _allChannels.map(c => `
${escHtml(c)} `).join('');
const row = document.createElement('tr');
row.setAttribute('data-new-host', 'true');
row.innerHTML = `
@@ -1019,10 +1043,10 @@
-
${usersOpts}
-
${usersOpts}
-
${tcOpts}
-
${chOpts}
+
${makeMpickHTML(_allUsers, [], 'host-managers')}
+
${makeMpickHTML(_allUsers, [], 'host-monitors')}
+
${makeMpickHTML(_allThresholdConfigs, [], 'host-tc')}
+
${makeMpickHTML(_allChannels, [], 'host-channels')}
✕ `;
tbody.appendChild(row);
}
@@ -1097,8 +1121,6 @@
function addUserRow() {
const tbody = document.getElementById('users-tbody');
- const opts = _allChannels.map(ch => `
${escHtml(ch)} `).join('');
- const chHtml = `
${opts} `;
const row = document.createElement('tr');
row.setAttribute('data-new-user', 'true');
row.innerHTML = `
@@ -1106,7 +1128,7 @@
-
${chHtml}
+
${makeMpickHTML(_allChannels, [], 'user-ch-select')}
✕ `;
tbody.appendChild(row);
@@ -1186,6 +1208,121 @@
return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');
}
+ // ---- Multi-picker ----
+ let _mpickPanel = null;
+ let _mpickTarget = null;
+
+ function _initMpickPanel() {
+ if (_mpickPanel) return;
+ const p = document.createElement('div');
+ p.className = 'mpick-panel';
+ p.style.display = 'none';
+ p.innerHTML = `
+
+
+ `;
+ document.body.appendChild(p);
+ _mpickPanel = p;
+ document.addEventListener('mousedown', e => {
+ if (!_mpickPanel || _mpickPanel.style.display === 'none') return;
+ if (!_mpickPanel.contains(e.target) && !e.target.closest('.mpick-display')) closeMpick();
+ }, true);
+ document.addEventListener('keydown', e => {
+ if (e.key === 'Escape' && _mpickPanel && _mpickPanel.style.display !== 'none') closeMpick();
+ });
+ }
+
+ function openMpick(displayEl) {
+ if (displayEl.closest('tr')?.dataset.deleted === 'true') return;
+ _initMpickPanel();
+ _mpickTarget = displayEl.closest('.mpick-wrapper');
+ _rerenderMpick();
+ _mpickPanel.style.display = 'block';
+ const rect = displayEl.getBoundingClientRect();
+ const pw = _mpickPanel.offsetWidth || 360;
+ const ph = _mpickPanel.offsetHeight || 280;
+ let top = rect.bottom + 4;
+ let left = rect.left;
+ if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8);
+ if (top + ph > window.innerHeight - 8) top = Math.max(8, rect.top - ph - 4);
+ _mpickPanel.style.top = top + 'px';
+ _mpickPanel.style.left = left + 'px';
+ }
+
+ function _rerenderMpick() {
+ const sel = _mpickTarget.querySelector('select');
+ const allOpts = [...sel.options];
+ const selVals = new Set([...sel.selectedOptions].map(o => o.value));
+ const avail = allOpts.filter(o => !selVals.has(o.value));
+ const chosen = allOpts.filter(o => selVals.has(o.value));
+ document.getElementById('mpick-avail').innerHTML = avail.length
+ ? avail.map(o => `
${escHtml(o.value)} +
`).join('')
+ : '
All selected
';
+ document.getElementById('mpick-sel').innerHTML = chosen.length
+ ? chosen.map(o => `
${escHtml(o.value)} −
`).join('')
+ : '
None selected
';
+ }
+
+ function _mpickToggle(itemEl, toSelected) {
+ const val = itemEl.dataset.val;
+ const sel = _mpickTarget.querySelector('select');
+ const opt = [...sel.options].find(o => o.value === val);
+ if (opt) opt.selected = toSelected;
+ _updateMpickDisplay(_mpickTarget);
+ _rerenderMpick();
+ }
+
+ function _updateMpickDisplay(wrapper) {
+ const sel = wrapper.querySelector('select');
+ const display = wrapper.querySelector('.mpick-display');
+ const selected = [...sel.selectedOptions].map(o => o.value);
+ if (!selected.length) {
+ display.innerHTML = '
(none) ';
+ display.title = '';
+ return;
+ }
+ const MAX = 2;
+ let html = selected.slice(0, MAX).map(v => `
${escHtml(v)} `).join('');
+ if (selected.length > MAX) html += `
+${selected.length - MAX} `;
+ display.innerHTML = html;
+ display.title = selected.join(', ');
+ }
+
+ function closeMpick() {
+ if (_mpickPanel) _mpickPanel.style.display = 'none';
+ _mpickTarget = null;
+ }
+
+ function makeMpickHTML(allItems, selectedItems, cls) {
+ const selSet = new Set(selectedItems);
+ const opts = allItems.map(v => `
${escHtml(v)} `).join('');
+ const selected = allItems.filter(v => selSet.has(v));
+ const MAX = 2;
+ let dispHtml;
+ if (!selected.length) {
+ dispHtml = '
(none) ';
+ } else {
+ dispHtml = selected.slice(0, MAX).map(v => `
${escHtml(v)} `).join('');
+ if (selected.length > MAX) dispHtml += `
+${selected.length - MAX} `;
+ }
+ const title = escHtml(selected.join(', '));
+ return `
`;
+ }
+
// Highlight sidebar link for the section currently in view
const sections = document.querySelectorAll('.section');
const navLinks = document.querySelectorAll('.sidebar-nav a');