feat: settings UI renders per-user edit rights and owner controls
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
This commit is contained in:
@@ -572,11 +572,12 @@
|
|||||||
<option value="CRITICAL">CRITICAL only</option>
|
<option value="CRITICAL">CRITICAL only</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{% if not current_user or current_user.admin %}
|
||||||
<div class="ch-form-row">
|
<div class="ch-form-row">
|
||||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
<label>Owner <span style="font-weight:normal;color:#888">(empty = global)</span></label>
|
||||||
<input type="checkbox" id="ch-private"> Private — visible only to you
|
<input type="text" id="ch-owner" placeholder="(global)" autocomplete="off">
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div id="ch-modal-status" class="ch-status"></div>
|
<div id="ch-modal-status" class="ch-status"></div>
|
||||||
<div class="ch-modal-footer">
|
<div class="ch-modal-footer">
|
||||||
<button class="btn btn-secondary" onclick="closeChannelModal()">Cancel</button>
|
<button class="btn btn-secondary" onclick="closeChannelModal()">Cancel</button>
|
||||||
@@ -594,8 +595,10 @@
|
|||||||
{% for section in sections %}
|
{% for section in sections %}
|
||||||
<a href="#{{ section.id }}" onclick="closeSidebar()">{{ section.title }}</a>
|
<a href="#{{ section.id }}" onclick="closeSidebar()">{{ section.title }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if not current_user or current_user.admin %}
|
||||||
<hr style="margin: 8px 0; border: none; border-top: 1px solid #e8e8e8;">
|
<hr style="margin: 8px 0; border: none; border-top: 1px solid #e8e8e8;">
|
||||||
<a href="#" onclick="showRollbackModal(); return false;" style="color:#888;font-size:.82em">View backups / rollback</a>
|
<a href="#" onclick="showRollbackModal(); return false;" style="color:#888;font-size:.82em">View backups / rollback</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -709,12 +712,18 @@
|
|||||||
<td style="font-family:monospace;font-size:.9em;white-space:nowrap">{{ h.name | e }}</td>
|
<td style="font-family:monospace;font-size:.9em;white-space:nowrap">{{ h.name | e }}</td>
|
||||||
<td style="text-align:center"><input type="checkbox" class="host-watch" {% if h.watch %}checked{% endif %}></td>
|
<td style="text-align:center"><input type="checkbox" class="host-watch" {% if h.watch %}checked{% endif %}></td>
|
||||||
<td style="text-align:center"><input type="checkbox" class="host-dyndns" {% if h.dyndns %}checked{% endif %}></td>
|
<td style="text-align:center"><input type="checkbox" class="host-dyndns" {% if h.dyndns %}checked{% endif %}></td>
|
||||||
|
{% if h.is_owner %}
|
||||||
<td><input class="field-input host-owner" value="{{ h.owner | e }}" placeholder="(none)" style="min-width:90px"></td>
|
<td><input class="field-input host-owner" value="{{ h.owner | e }}" placeholder="(none)" style="min-width:90px"></td>
|
||||||
<td>{{ mpick(all_usernames, h.managers, 'host-managers') }}</td>
|
<td>{{ mpick(all_usernames, h.managers, 'host-managers') }}</td>
|
||||||
<td>{{ mpick(all_usernames, h.monitors, 'host-monitors') }}</td>
|
<td>{{ mpick(all_usernames, h.monitors, 'host-monitors') }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td><span class="val-tag">{{ h.owner | e }}</span></td>
|
||||||
|
<td>{% for m in h.managers %}<span class="val-tag">{{ m | e }}</span>{% else %}<span class="val-empty">(none)</span>{% endfor %}</td>
|
||||||
|
<td>{% for m in h.monitors %}<span class="val-tag">{{ m | e }}</span>{% else %}<span class="val-empty">(none)</span>{% endfor %}</td>
|
||||||
|
{% endif %}
|
||||||
<td>{{ mpick(all_threshold_configs, h.threshold_configs, 'host-tc') }}</td>
|
<td>{{ mpick(all_threshold_configs, h.threshold_configs, 'host-tc') }}</td>
|
||||||
<td>{{ mpick(all_channel_names, h.notification_channels, 'host-channels') }}</td>
|
<td>{{ mpick(all_channel_names, h.notification_channels, 'host-channels') }}</td>
|
||||||
<td><button class="btn-danger" onclick="toggleDeleteRow(this)">✕</button></td>
|
<td>{% if h.is_owner %}<button class="btn-danger" onclick="toggleDeleteRow(this)">✕</button>{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -748,12 +757,13 @@
|
|||||||
<span class="channel-name-text">{{ ch.name | e }}</span>
|
<span class="channel-name-text">{{ ch.name | e }}</span>
|
||||||
<span class="ch-type-badge">{{ ch.type_label | e }}</span>
|
<span class="ch-type-badge">{{ ch.type_label | e }}</span>
|
||||||
{% if ch.min_level and ch.min_level != 'WARNING' %}<span class="ch-level-badge">{{ ch.min_level | e }}+</span>{% endif %}
|
{% if ch.min_level and ch.min_level != 'WARNING' %}<span class="ch-level-badge">{{ ch.min_level | e }}+</span>{% endif %}
|
||||||
{% if ch.private %}<span class="ch-private-badge">private</span>{% endif %}
|
|
||||||
{% if ch.owner %}<span class="ch-owner-badge">{{ ch.owner | e }}</span>{% endif %}
|
{% if ch.owner %}<span class="ch-owner-badge">{{ ch.owner | e }}</span>{% endif %}
|
||||||
|
{% if ch.editable %}
|
||||||
<span class="channel-header-actions">
|
<span class="channel-header-actions">
|
||||||
<button class="btn btn-secondary" style="font-size:.78em;padding:2px 8px" onclick="openChannelModal('{{ ch.name | e }}')">Edit</button>
|
<button class="btn btn-secondary" style="font-size:.78em;padding:2px 8px" onclick="openChannelModal('{{ ch.name | e }}')">Edit</button>
|
||||||
<button class="btn-danger" onclick="deleteChannel('{{ ch.name | e }}')">✕</button>
|
<button class="btn-danger" onclick="deleteChannel('{{ ch.name | e }}')">✕</button>
|
||||||
</span>
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="channel-fields">
|
<div class="channel-fields">
|
||||||
{% for f in ch.fields %}
|
{% for f in ch.fields %}
|
||||||
@@ -790,13 +800,20 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div id="thresh-cfgs-{{ section.id }}" style="padding:8px 20px 0">
|
<div id="thresh-cfgs-{{ section.id }}" style="padding:8px 20px 0">
|
||||||
{% for tc in section.threshold_configs %}
|
{% for tc in section.threshold_configs %}
|
||||||
<div class="thresh-cfg-card" data-config-name="{{ tc.name | e }}">
|
<div class="thresh-cfg-card" data-config-name="{{ tc.name | e }}"{% if not tc.editable %} data-readonly="true"{% endif %}>
|
||||||
<div class="thresh-cfg-header">
|
<div class="thresh-cfg-header">
|
||||||
<span class="thresh-cfg-name-label">{{ tc.name | e }}</span>
|
<span class="thresh-cfg-name-label">{{ tc.name | e }}</span>
|
||||||
{% if tc.name != 'default' %}
|
{% if (not current_user or current_user.admin) and tc.name != 'default' %}
|
||||||
|
<input type="text" class="field-input thresh-owner" value="{{ tc.owner or '' }}"
|
||||||
|
placeholder="(global)" title="Owner — empty = global" style="max-width:140px;margin-left:10px">
|
||||||
|
{% elif tc.owner %}
|
||||||
|
<span class="ch-owner-badge" style="margin-left:10px">{{ tc.owner | e }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if tc.editable and tc.name != 'default' %}
|
||||||
<button class="btn-danger" style="margin-left:auto" onclick="deleteThresholdConfigCard(this)">✕ Delete</button>
|
<button class="btn-danger" style="margin-left:auto" onclick="deleteThresholdConfigCard(this)">✕ Delete</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<fieldset {% if not tc.editable %}disabled{% endif %} style="border:none;margin:0;padding:0;min-width:0">
|
||||||
<div style="overflow-x:auto">
|
<div style="overflow-x:auto">
|
||||||
<table class="crud-table thresh-metric-table">
|
<table class="crud-table thresh-metric-table">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
@@ -845,6 +862,7 @@
|
|||||||
<button class="btn btn-secondary" style="font-size:.8em;padding:3px 10px"
|
<button class="btn btn-secondary" style="font-size:.8em;padding:3px 10px"
|
||||||
onclick="addThresholdMetricRow(this.closest('.thresh-cfg-card').querySelector('tbody'))">+ Add metric</button>
|
onclick="addThresholdMetricRow(this.closest('.thresh-cfg-card').querySelector('tbody'))">+ Add metric</button>
|
||||||
</div>
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -928,6 +946,7 @@
|
|||||||
const _allChannels = {{ all_channel_names | tojson }};
|
const _allChannels = {{ all_channel_names | tojson }};
|
||||||
const _allUsers = {{ all_usernames | tojson }};
|
const _allUsers = {{ all_usernames | tojson }};
|
||||||
const _allThresholdConfigs = {{ all_threshold_configs | tojson }};
|
const _allThresholdConfigs = {{ all_threshold_configs | tojson }};
|
||||||
|
const _isAdmin = {{ 'true' if (not current_user or current_user.admin) else 'false' }};
|
||||||
|
|
||||||
// ---- Channel CRUD ----
|
// ---- Channel CRUD ----
|
||||||
let _channelSchemas = {};
|
let _channelSchemas = {};
|
||||||
@@ -981,7 +1000,8 @@
|
|||||||
document.getElementById('ch-type').value = '';
|
document.getElementById('ch-type').value = '';
|
||||||
document.getElementById('ch-type-fields').innerHTML = '';
|
document.getElementById('ch-type-fields').innerHTML = '';
|
||||||
document.getElementById('ch-min-level').value = 'WARNING';
|
document.getElementById('ch-min-level').value = 'WARNING';
|
||||||
document.getElementById('ch-private').checked = false;
|
const ownerInp = document.getElementById('ch-owner');
|
||||||
|
if (ownerInp) ownerInp.value = '';
|
||||||
|
|
||||||
if (name) {
|
if (name) {
|
||||||
// Load existing channel data via API
|
// Load existing channel data via API
|
||||||
@@ -993,7 +1013,7 @@
|
|||||||
document.getElementById('ch-type').value = ch.type;
|
document.getElementById('ch-type').value = ch.type;
|
||||||
onChTypeChange();
|
onChTypeChange();
|
||||||
document.getElementById('ch-min-level').value = ch.min_level || 'WARNING';
|
document.getElementById('ch-min-level').value = ch.min_level || 'WARNING';
|
||||||
document.getElementById('ch-private').checked = ch.private || false;
|
if (ownerInp) ownerInp.value = ch.owner || '';
|
||||||
(ch.fields || []).forEach(f => {
|
(ch.fields || []).forEach(f => {
|
||||||
const inp = document.getElementById('chf-' + f.key);
|
const inp = document.getElementById('chf-' + f.key);
|
||||||
if (inp) inp.value = f.value || '';
|
if (inp) inp.value = f.value || '';
|
||||||
@@ -1012,14 +1032,15 @@
|
|||||||
const name = document.getElementById('ch-name').value.trim();
|
const name = document.getElementById('ch-name').value.trim();
|
||||||
const type = document.getElementById('ch-type').value;
|
const type = document.getElementById('ch-type').value;
|
||||||
const minLevel = document.getElementById('ch-min-level').value;
|
const minLevel = document.getElementById('ch-min-level').value;
|
||||||
const isPrivate = document.getElementById('ch-private').checked;
|
|
||||||
const statusEl = document.getElementById('ch-modal-status');
|
const statusEl = document.getElementById('ch-modal-status');
|
||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
|
|
||||||
if (!name) { statusEl.textContent = 'Channel name is required.'; statusEl.style.color = '#c62828'; return; }
|
if (!name) { statusEl.textContent = 'Channel name is required.'; statusEl.style.color = '#c62828'; return; }
|
||||||
if (!type) { statusEl.textContent = 'Please select a type.'; statusEl.style.color = '#c62828'; return; }
|
if (!type) { statusEl.textContent = 'Please select a type.'; statusEl.style.color = '#c62828'; return; }
|
||||||
|
|
||||||
const body = { name, type, min_level: minLevel, private: isPrivate };
|
const body = { name, type, min_level: minLevel };
|
||||||
|
const ownerInp = document.getElementById('ch-owner');
|
||||||
|
if (ownerInp) body.owner = ownerInp.value.trim();
|
||||||
if (_channelSchemas[type]) {
|
if (_channelSchemas[type]) {
|
||||||
(_channelSchemas[type].fields || []).forEach(sf => {
|
(_channelSchemas[type].fields || []).forEach(sf => {
|
||||||
const inp = document.getElementById('chf-' + sf.key);
|
const inp = document.getElementById('chf-' + sf.key);
|
||||||
@@ -1177,8 +1198,11 @@
|
|||||||
watch: row.querySelector('.host-watch').checked,
|
watch: row.querySelector('.host-watch').checked,
|
||||||
dyndns: row.querySelector('.host-dyndns').checked,
|
dyndns: row.querySelector('.host-dyndns').checked,
|
||||||
};
|
};
|
||||||
const owner = row.querySelector('.host-owner').value.trim();
|
const ownerInput = row.querySelector('.host-owner');
|
||||||
if (owner) entry.owner = owner;
|
if (ownerInput) {
|
||||||
|
const owner = ownerInput.value.trim();
|
||||||
|
if (owner) entry.owner = owner;
|
||||||
|
}
|
||||||
const managers = [...(row.querySelector('.host-managers')?.selectedOptions || [])].map(o => o.value);
|
const managers = [...(row.querySelector('.host-managers')?.selectedOptions || [])].map(o => o.value);
|
||||||
if (managers.length) entry.managers = managers;
|
if (managers.length) entry.managers = managers;
|
||||||
const monitors = [...(row.querySelector('.host-monitors')?.selectedOptions || [])].map(o => o.value);
|
const monitors = [...(row.querySelector('.host-monitors')?.selectedOptions || [])].map(o => o.value);
|
||||||
@@ -1618,6 +1642,7 @@
|
|||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="thresh-cfg-header">
|
<div class="thresh-cfg-header">
|
||||||
<input type="text" class="field-input new-config-name" placeholder="Config name (e.g. servers)" style="max-width:220px">
|
<input type="text" class="field-input new-config-name" placeholder="Config name (e.g. servers)" style="max-width:220px">
|
||||||
|
${_isAdmin ? '<input type="text" class="field-input thresh-owner" placeholder="(global)" title="Owner — empty = global" style="max-width:140px;margin-left:10px">' : ''}
|
||||||
<button class="btn-danger" style="margin-left:auto" onclick="this.closest('.thresh-cfg-card').remove()">✕ Delete</button>
|
<button class="btn-danger" style="margin-left:auto" onclick="this.closest('.thresh-cfg-card').remove()">✕ Delete</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="overflow-x:auto">
|
<div style="overflow-x:auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user