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>
|
||||
</select>
|
||||
</div>
|
||||
{% if not current_user or current_user.admin %}
|
||||
<div class="ch-form-row">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="ch-private"> Private — visible only to you
|
||||
</label>
|
||||
<label>Owner <span style="font-weight:normal;color:#888">(empty = global)</span></label>
|
||||
<input type="text" id="ch-owner" placeholder="(global)" autocomplete="off">
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="ch-modal-status" class="ch-status"></div>
|
||||
<div class="ch-modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeChannelModal()">Cancel</button>
|
||||
@@ -594,8 +595,10 @@
|
||||
{% for section in sections %}
|
||||
<a href="#{{ section.id }}" onclick="closeSidebar()">{{ section.title }}</a>
|
||||
{% endfor %}
|
||||
{% if not current_user or current_user.admin %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -709,12 +712,18 @@
|
||||
<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-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>{{ mpick(all_usernames, h.managers, 'host-managers') }}</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_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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -748,12 +757,13 @@
|
||||
<span class="channel-name-text">{{ ch.name | 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.private %}<span class="ch-private-badge">private</span>{% endif %}
|
||||
{% if ch.owner %}<span class="ch-owner-badge">{{ ch.owner | e }}</span>{% endif %}
|
||||
{% if ch.editable %}
|
||||
<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-danger" onclick="deleteChannel('{{ ch.name | e }}')">✕</button>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="channel-fields">
|
||||
{% for f in ch.fields %}
|
||||
@@ -790,13 +800,20 @@
|
||||
{% endfor %}
|
||||
<div id="thresh-cfgs-{{ section.id }}" style="padding:8px 20px 0">
|
||||
{% 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">
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
<fieldset {% if not tc.editable %}disabled{% endif %} style="border:none;margin:0;padding:0;min-width:0">
|
||||
<div style="overflow-x:auto">
|
||||
<table class="crud-table thresh-metric-table">
|
||||
<thead><tr>
|
||||
@@ -845,6 +862,7 @@
|
||||
<button class="btn btn-secondary" style="font-size:.8em;padding:3px 10px"
|
||||
onclick="addThresholdMetricRow(this.closest('.thresh-cfg-card').querySelector('tbody'))">+ Add metric</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -928,6 +946,7 @@
|
||||
const _allChannels = {{ all_channel_names | tojson }};
|
||||
const _allUsers = {{ all_usernames | tojson }};
|
||||
const _allThresholdConfigs = {{ all_threshold_configs | tojson }};
|
||||
const _isAdmin = {{ 'true' if (not current_user or current_user.admin) else 'false' }};
|
||||
|
||||
// ---- Channel CRUD ----
|
||||
let _channelSchemas = {};
|
||||
@@ -981,7 +1000,8 @@
|
||||
document.getElementById('ch-type').value = '';
|
||||
document.getElementById('ch-type-fields').innerHTML = '';
|
||||
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) {
|
||||
// Load existing channel data via API
|
||||
@@ -993,7 +1013,7 @@
|
||||
document.getElementById('ch-type').value = ch.type;
|
||||
onChTypeChange();
|
||||
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 => {
|
||||
const inp = document.getElementById('chf-' + f.key);
|
||||
if (inp) inp.value = f.value || '';
|
||||
@@ -1012,14 +1032,15 @@
|
||||
const name = document.getElementById('ch-name').value.trim();
|
||||
const type = document.getElementById('ch-type').value;
|
||||
const minLevel = document.getElementById('ch-min-level').value;
|
||||
const isPrivate = document.getElementById('ch-private').checked;
|
||||
const statusEl = document.getElementById('ch-modal-status');
|
||||
statusEl.textContent = '';
|
||||
|
||||
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; }
|
||||
|
||||
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]) {
|
||||
(_channelSchemas[type].fields || []).forEach(sf => {
|
||||
const inp = document.getElementById('chf-' + sf.key);
|
||||
@@ -1177,8 +1198,11 @@
|
||||
watch: row.querySelector('.host-watch').checked,
|
||||
dyndns: row.querySelector('.host-dyndns').checked,
|
||||
};
|
||||
const owner = row.querySelector('.host-owner').value.trim();
|
||||
if (owner) entry.owner = owner;
|
||||
const ownerInput = row.querySelector('.host-owner');
|
||||
if (ownerInput) {
|
||||
const owner = ownerInput.value.trim();
|
||||
if (owner) entry.owner = owner;
|
||||
}
|
||||
const managers = [...(row.querySelector('.host-managers')?.selectedOptions || [])].map(o => o.value);
|
||||
if (managers.length) entry.managers = managers;
|
||||
const monitors = [...(row.querySelector('.host-monitors')?.selectedOptions || [])].map(o => o.value);
|
||||
@@ -1618,6 +1642,7 @@
|
||||
card.innerHTML = `
|
||||
<div class="thresh-cfg-header">
|
||||
<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>
|
||||
</div>
|
||||
<div style="overflow-x:auto">
|
||||
|
||||
Reference in New Issue
Block a user