Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-28 07:24:56

0001 {% extends 'base.html' %}
0002 {% load static %}
0003 {% load swf_fmt %}
0004 
0005 {% block title %}Edit {{ alarm_entry_id }} — Alarms{% endblock %}
0006 
0007 {% block content %}
0008 <link rel="stylesheet" href="{% static 'css/state-colors.css' %}">
0009 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/codemirror.min.css">
0010 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/theme/material-darker.min.css">
0011 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/codemirror.min.js"></script>
0012 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/mode/markdown/markdown.min.js"></script>
0013 <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.18/mode/javascript/javascript.min.js"></script>
0014 
0015 <style>
0016   .CodeMirror { height: auto !important; border: 1px solid #333; font-family: "SF Mono", Monaco, Menlo, monospace; font-size: 13px; }
0017   .CodeMirror-scroll { min-height: 200px; max-height: 60vh; }
0018   #content-editor .CodeMirror { min-height: 260px; }
0019   #data-editor   .CodeMirror { min-height: 220px; }
0020   .autosave-indicator { font-size: 12px; color: #888; margin-left: 1em; }
0021   .autosave-indicator.saving { color: #daa520; }
0022   .autosave-indicator.saved  { color: #4CAF50; }
0023   .autosave-indicator.error  { color: #dc3545; }
0024   .version-row.current td { background: #2a4a2a; }
0025   .version-row td { padding: 4px 8px; font-size: 12.5px; border-top: 1px solid #2a2a2a; }
0026   .version-row .vnum { color: #daa520; white-space: nowrap; }
0027   .version-row .vts { color: #8a8a8a; white-space: nowrap; }
0028   .version-row .vwho { color: #8a8a8a; white-space: nowrap; }
0029   .version-row .vlines { color: #888; white-space: nowrap; }
0030   .version-row .vprev { color: #bbb; max-width: 600px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
0031   .local-backup-row td { background: #2a2200 !important; color: #ff9800; }
0032 </style>
0033 
0034 <div class="container mt-3" style="max-width: 1100px;">
0035     <p><a href="{% url 'monitor_app:alarms_dashboard' %}">&larr; All alarms</a></p>
0036     <h2 class="mb-3">Edit <code>{{ alarm_entry_id }}</code></h2>
0037 
0038     <div class="d-flex align-items-center mb-2">
0039         <button id="save-btn" class="btn btn-sm btn-secondary" disabled>Save</button>
0040         <span id="autosave-indicator" class="autosave-indicator"></span>
0041     </div>
0042 
0043     <h3 style="font-size:1em;" class="mt-3">Title</h3>
0044     <input type="text" id="title-input" class="form-control" value="{{ title }}"
0045            maxlength="255" placeholder="Short human-readable title">
0046 
0047     <div class="row mt-3 g-3" style="max-width: 1000px;">
0048         <div class="col-auto">
0049             <label class="form-label text-muted" style="font-size:0.85em;"
0050                    title="When OFF, this alarm's detection still runs and events still appear on the dashboard. Only email notifications are suppressed.">Emails</label><br>
0051             <div class="form-check form-switch"
0052                  title="When OFF, this alarm's detection still runs and events still appear on the dashboard. Only email notifications are suppressed.">
0053                 <input class="form-check-input" type="checkbox" id="enabled-input"
0054                        {% if enabled %}checked{% endif %}>
0055             </div>
0056         </div>
0057         <div class="col-auto">
0058             <label class="form-label text-muted" style="font-size:0.85em;">Renotification window (hours)</label>
0059             <input type="number" id="renotification-input" class="form-control form-control-sm"
0060                    value="{{ renotification_window_hours }}" min="0" step="1" style="width:170px;"
0061                    title="While an event is active, we don't re-email the same entity until this many hours have passed since the last notification. 0 = one email per event lifecycle, silent until clear.">
0062         </div>
0063     </div>
0064 
0065     <h3 style="font-size:1em;" class="mt-3">Recipients</h3>
0066     <p class="text-muted" style="font-size:0.85em;">
0067       Email addresses and <code>@team</code> names, separated by commas or whitespace. Examples:
0068       <code>@prodops</code>, <code>alice@example.com, @prodops</code>.
0069     </p>
0070     <input type="text" id="recipients-input" class="form-control" value="{{ recipients_text }}"
0071            placeholder="@team1, user@example.com, …">
0072 
0073     <h3 style="font-size:1em;" class="mt-3">Description / email body (markdown)</h3>
0074     <div id="content-editor">
0075         <textarea id="editor">{{ content }}</textarea>
0076     </div>
0077 
0078     <h3 style="font-size:1em;" class="mt-4">Params</h3>
0079     {% if params_meta %}
0080     <table class="table table-sm" style="max-width:1000px; font-size:0.9em;">
0081       <thead>
0082         <tr><th>Param</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr>
0083       </thead>
0084       <tbody>
0085         {% for p in params_meta %}
0086         <tr{% if p.required %} style="font-weight:600;"{% endif %}>
0087           <td><code>{{ p.name }}</code></td>
0088           <td><small>{{ p.type }}</small></td>
0089           <td>{% if p.required %}<span class="text-danger">yes</span>{% else %}no{% endif %}</td>
0090           <td><small>{% if p.has_default %}{{ p.default|default_if_none:"—" }}{% else %}—{% endif %}</small></td>
0091           <td><small>{{ p.description }}</small></td>
0092         </tr>
0093         {% endfor %}
0094       </tbody>
0095     </table>
0096     {% else %}
0097     <p class="text-muted" style="font-size:0.85em;">
0098       (This alarm's module does not declare <code>PARAMS</code>, so no
0099       help table is rendered. Check the module source.)
0100     </p>
0101     {% endif %}
0102     <div id="data-editor">
0103         <textarea id="data-json">{{ data_json }}</textarea>
0104     </div>
0105 
0106     <div class="mt-3 d-flex gap-2 align-items-center">
0107         <button id="test-btn" class="btn btn-sm btn-outline-info">Test (live, no email)</button>
0108         <span id="test-status" class="text-muted" style="font-size:0.85em;"></span>
0109     </div>
0110     <div id="test-output" class="mt-2" style="display:none;">
0111       <h4 style="font-size:0.95em;">Test result
0112         <small id="test-meta" class="text-muted"></small>
0113       </h4>
0114       <pre id="test-body" style="white-space:pre-wrap; background:#111; color:#ddd; padding:10px; border-radius:4px; max-height:50vh; overflow:auto; font-size:12.5px;"></pre>
0115     </div>
0116 
0117     <div id="version-history" class="mt-4"></div>
0118 </div>
0119 
0120 <script>
0121 (function() {
0122     var ALARM_ENTRY_ID = "{{ alarm_entry_id|escapejs }}";
0123     var SAVE_URL    = "{% url 'monitor_app:alarm_config_save' entry_id=alarm_entry_id %}";
0124     var VERSION_URL = "{% url 'monitor_app:alarm_config_version' entry_id=alarm_entry_id version_num=0 %}".slice(0, -2);  // append <num>/
0125     var TEST_URL    = "{% url 'monitor_app:alarm_test' entry_id=alarm_entry_id %}";
0126     var LOCAL_KEY   = 'swf-alarms-autosave-' + ALARM_ENTRY_ID;
0127     var AUTOSAVE_MS = 10000;
0128 
0129     var cmContent = CodeMirror.fromTextArea(document.getElementById('editor'), {
0130         mode: 'markdown', theme: 'material-darker', lineNumbers: true,
0131         lineWrapping: true, tabSize: 2,
0132         extraKeys: {
0133             'Cmd-S': function() { save(false); },
0134             'Ctrl-S': function() { save(false); },
0135         },
0136     });
0137     var cmData = CodeMirror.fromTextArea(document.getElementById('data-json'), {
0138         mode: {name: 'javascript', json: true}, theme: 'material-darker',
0139         lineNumbers: true, tabSize: 2,
0140     });
0141 
0142     var titleInput        = document.getElementById('title-input');
0143     var enabledInput      = document.getElementById('enabled-input');
0144     var renotificationInp = document.getElementById('renotification-input');
0145     var recipientsInput   = document.getElementById('recipients-input');
0146 
0147     var lastSavedTitle         = titleInput.value;
0148     var lastSavedEnabled       = enabledInput.checked;
0149     var lastSavedRenotification = renotificationInp.value;
0150     var lastSavedRecipients    = recipientsInput.value;
0151     var lastSavedContent = cmContent.getValue();
0152     var lastSavedData    = cmData.getValue();
0153 
0154     var indicator = document.getElementById('autosave-indicator');
0155     function setIndicator(cls, text) {
0156         indicator.className = 'autosave-indicator ' + cls;
0157         indicator.textContent = text;
0158     }
0159 
0160     var saveBtn = document.getElementById('save-btn');
0161     function isDirty() {
0162         return titleInput.value !== lastSavedTitle
0163             || enabledInput.checked !== lastSavedEnabled
0164             || renotificationInp.value !== lastSavedRenotification
0165             || recipientsInput.value !== lastSavedRecipients
0166             || cmContent.getValue() !== lastSavedContent
0167             || cmData.getValue() !== lastSavedData;
0168     }
0169     function updateSaveState() {
0170         if (isDirty()) {
0171             saveBtn.classList.remove('btn-secondary');
0172             saveBtn.classList.add('btn-primary');
0173             saveBtn.disabled = false;
0174         } else {
0175             saveBtn.classList.remove('btn-primary');
0176             saveBtn.classList.add('btn-secondary');
0177             saveBtn.disabled = true;
0178         }
0179     }
0180 
0181     function save(isAutosave) {
0182         var titleVal = titleInput.value;
0183         var enabledVal = enabledInput.checked;
0184         var renotificationVal = renotificationInp.value;
0185         var recipientsVal = recipientsInput.value;
0186         var contentVal = cmContent.getValue();
0187         var dataStr = cmData.getValue();
0188         var parsed;
0189         try { parsed = JSON.parse(dataStr); }
0190         catch (e) {
0191             setIndicator('error', 'Invalid JSON in params: ' + e.message);
0192             return Promise.reject(e);
0193         }
0194         if (isAutosave &&
0195             titleVal === lastSavedTitle &&
0196             enabledVal === lastSavedEnabled &&
0197             renotificationVal === lastSavedRenotification &&
0198             recipientsVal === lastSavedRecipients &&
0199             contentVal === lastSavedContent &&
0200             dataStr === lastSavedData) {
0201             return Promise.resolve();
0202         }
0203         setIndicator('saving', isAutosave ? 'Autosaving…' : 'Saving…');
0204         return fetch(SAVE_URL, {
0205             method: 'POST',
0206             headers: { 'Content-Type': 'application/json' },
0207             body: JSON.stringify({
0208                 title: titleVal,
0209                 enabled: enabledVal,
0210                 renotification_window_hours: parseInt(renotificationVal || '0', 10),
0211                 recipients: recipientsVal,
0212                 content: contentVal,
0213                 data: parsed,
0214                 autosave: !!isAutosave,
0215             }),
0216         }).then(function(r) {
0217             if (!r.ok) return r.json().then(function(j) { throw new Error(j.error || ('HTTP ' + r.status)); });
0218             return r.json();
0219         }).then(function(j) {
0220             lastSavedTitle         = titleVal;
0221             lastSavedEnabled       = enabledVal;
0222             lastSavedRenotification = renotificationVal;
0223             lastSavedRecipients    = recipientsVal;
0224             lastSavedContent = contentVal;
0225             lastSavedData    = dataStr;
0226             try { localStorage.removeItem(LOCAL_KEY); } catch (e) {}
0227             setIndicator('saved', (isAutosave ? 'Autosaved' : 'Saved') +
0228                 ' — v' + (j.version_num || 0) +
0229                 ' at ' + new Date((j.modified || 0) * 1000).toLocaleTimeString());
0230             updateSaveState();
0231             renderVersions();
0232         }).catch(function(e) {
0233             setIndicator('error', 'Save failed: ' + e.message);
0234         });
0235     }
0236 
0237     document.getElementById('save-btn').addEventListener('click', function() { save(false); });
0238 
0239     // Autosave loop.
0240     setInterval(function() { save(true); }, AUTOSAVE_MS);
0241 
0242     // localStorage backup on every change (crash-proof).
0243     function backup() {
0244         try {
0245             localStorage.setItem(LOCAL_KEY, JSON.stringify({
0246                 title: titleInput.value,
0247                 enabled: enabledInput.checked,
0248                 renotification_window_hours: renotificationInp.value,
0249                 recipients: recipientsInput.value,
0250                 content: cmContent.getValue(),
0251                 data: cmData.getValue(),
0252                 ts: Date.now(),
0253             }));
0254         } catch (e) {}
0255     }
0256     function onChange() { backup(); updateSaveState(); }
0257     cmContent.on('change', onChange);
0258     cmData.on('change', onChange);
0259     titleInput.addEventListener('input', onChange);
0260     enabledInput.addEventListener('change', onChange);
0261     renotificationInp.addEventListener('input', onChange);
0262     recipientsInput.addEventListener('input', onChange);
0263     updateSaveState();
0264 
0265     // Flush on unload (best-effort).
0266     window.addEventListener('beforeunload', function() {
0267         var titleVal = titleInput.value;
0268         var contentVal = cmContent.getValue();
0269         var dataStr = cmData.getValue();
0270         if (titleVal === lastSavedTitle &&
0271             enabledInput.checked === lastSavedEnabled &&
0272             renotificationInp.value === lastSavedRenotification &&
0273             recipientsInput.value === lastSavedRecipients &&
0274             contentVal === lastSavedContent && dataStr === lastSavedData) return;
0275         try {
0276             var blob = new Blob([JSON.stringify({
0277                 title: titleVal,
0278                 enabled: enabledInput.checked,
0279                 renotification_window_hours: parseInt(renotificationInp.value || '0', 10),
0280                 recipients: recipientsInput.value,
0281                 content: contentVal,
0282                 data: JSON.parse(dataStr),
0283                 autosave: true,
0284             })], { type: 'application/json' });
0285             navigator.sendBeacon(SAVE_URL, blob);
0286         } catch (e) {}
0287     });
0288 
0289     // Version history table
0290     var versions = JSON.parse('{{ versions_json|escapejs }}');
0291 
0292     function renderVersions() {
0293         // Reload versions from the server after a save.
0294         fetch(window.location.pathname, { headers: {'X-Fragment': 'versions'} })
0295             .then(function(r) { return r.text(); })
0296             .then(function(html) {
0297                 // Simple scrape: we re-render from the server-side list only on page load.
0298                 // For post-save, show last version as provisional; the server rendered row
0299                 // will appear on next full page load.
0300             })
0301             .catch(function() {});
0302     }
0303 
0304     function renderStaticVersions() {
0305         var root = document.getElementById('version-history');
0306         root.innerHTML = '';
0307         var hasLocal = false;
0308         try { hasLocal = !!localStorage.getItem(LOCAL_KEY); } catch (e) {}
0309 
0310         var html = '<h3 style="font-size:1em;">Version history ' +
0311                    '<span class="text-muted">(' + versions.length + ')</span></h3>';
0312         html += '<table class="table table-sm"><tbody>';
0313 
0314         // Local-backup row
0315         if (hasLocal) {
0316             try {
0317                 var local = JSON.parse(localStorage.getItem(LOCAL_KEY));
0318                 html += '<tr class="local-backup-row">';
0319                 html += '<td class="vnum">local</td>';
0320                 html += '<td class="vts">' + new Date(local.ts).toISOString().replace('T', ' ').slice(0, 19) + '</td>';
0321                 html += '<td class="vwho">autosave (browser)</td>';
0322                 html += '<td class="vlines">' + ((local.content || '').split('\n').length) + ' lines</td>';
0323                 html += '<td class="vprev">' + escapeHtml((local.content || '').split('\n')[0].slice(0, 120)) + '</td>';
0324                 html += '<td><button class="btn btn-sm btn-outline-warning" onclick="swfAlarmsRestoreLocal()">Restore</button></td>';
0325                 html += '</tr>';
0326             } catch (e) {}
0327         }
0328 
0329         for (var i = 0; i < versions.length; i++) {
0330             var v = versions[i];
0331             html += '<tr class="version-row">';
0332             html += '<td class="vnum">v' + v.version_num + '</td>';
0333             html += '<td class="vts">' + fmtTs(v.timestamp) + '</td>';
0334             html += '<td class="vwho">' + escapeHtml(v.changed_by || '') + '</td>';
0335             html += '<td class="vlines">' + (v.line_count || 0) + ' lines</td>';
0336             html += '<td class="vprev">' + escapeHtml((v.preview || '').slice(0, 120)) + '</td>';
0337             html += '<td><button class="btn btn-sm btn-outline-secondary" onclick="swfAlarmsRestoreVersion(' +
0338                     v.version_num + ')">Load</button></td>';
0339             html += '</tr>';
0340         }
0341         html += '</tbody></table>';
0342         root.innerHTML = html;
0343     }
0344     renderStaticVersions();
0345 
0346     function fmtTs(t) {
0347         if (!t) return '';
0348         return new Date(t * 1000).toISOString().replace('T', ' ').slice(0, 19);
0349     }
0350     function escapeHtml(s) {
0351         return String(s || '').replace(/[&<>"']/g, function(c) {
0352             return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
0353         });
0354     }
0355 
0356     window.swfAlarmsRestoreVersion = function(n) {
0357         if (!confirm('Load v' + n + ' into the editor? Unsaved changes will be lost.')) return;
0358         fetch(VERSION_URL + n + '/')
0359             .then(function(r) { return r.json(); })
0360             .then(function(j) {
0361                 var d = j.data || {};
0362                 titleInput.value = j.title || '';
0363                 enabledInput.checked = !!d.enabled;
0364                 if (d.renotification_window_hours !== undefined)
0365                     renotificationInp.value = d.renotification_window_hours;
0366                 if (Array.isArray(d.recipients)) {
0367                     recipientsInput.value = d.recipients.join(', ');
0368                 } else if (d.recipients) {
0369                     recipientsInput.value = String(d.recipients);
0370                 }
0371                 cmContent.setValue(j.content || '');
0372                 try { cmData.setValue(JSON.stringify({
0373                     kind: d.kind,
0374                     params: d.params,
0375                 }, null, 2)); } catch (e) {}
0376                 setIndicator('saved', 'v' + n + ' loaded — edit and save to commit.');
0377                 updateSaveState();
0378             })
0379             .catch(function(e) { setIndicator('error', 'Restore failed: ' + e.message); });
0380     };
0381     // ── Test + Preview ───────────────────────────────────────────────
0382     var testOut   = document.getElementById('test-output');
0383     var testBody  = document.getElementById('test-body');
0384     var testMeta  = document.getElementById('test-meta');
0385     var testStat  = document.getElementById('test-status');
0386 
0387     function currentParamsParsed() {
0388         try { return JSON.parse(cmData.getValue()); }
0389         catch (e) { return null; }
0390     }
0391 
0392     document.getElementById('test-btn').addEventListener('click', function() {
0393         var params = currentParamsParsed();
0394         if (params === null) {
0395             testStat.textContent = 'Invalid JSON in params — fix before testing.';
0396             return;
0397         }
0398         testStat.textContent = 'Running (live fetch)…';
0399         testOut.style.display = 'none';
0400         fetch(TEST_URL, {
0401             method: 'POST',
0402             headers: {'Content-Type': 'application/json'},
0403             body: JSON.stringify({params: params}),
0404         }).then(function(r) { return r.json(); })
0405           .then(function(j) {
0406             testStat.textContent = '';
0407             testOut.style.display = '';
0408             if (j.error) {
0409                 testMeta.textContent = ' — error after ' + (j.elapsed_ms||0) + ' ms';
0410                 testBody.textContent = j.error;
0411                 return;
0412             }
0413             testMeta.textContent = ' — ' + j.count + ' detection(s) in ' + (j.elapsed_ms||0) + ' ms';
0414             if (!j.detections.length) {
0415                 testBody.textContent = '(no detections — conditions did not match any entity)';
0416                 return;
0417             }
0418             var lines = [];
0419             for (var i = 0; i < j.detections.length; i++) {
0420                 var d = j.detections[i];
0421                 lines.push('[' + (i+1) + '] ' + d.subject);
0422                 if (d.body_context) lines.push('  ---');
0423                 if (d.body_context) lines.push(d.body_context.replace(/^/gm, '  '));
0424                 lines.push('');
0425             }
0426             testBody.textContent = lines.join('\n');
0427           })
0428           .catch(function(e) {
0429             testStat.textContent = 'Test failed: ' + e.message;
0430           });
0431     });
0432 
0433     window.swfAlarmsRestoreLocal = function() {
0434         try {
0435             var local = JSON.parse(localStorage.getItem(LOCAL_KEY));
0436             if (!local) return;
0437             if (!confirm('Restore from local browser backup? Unsaved server state will be overwritten on save.')) return;
0438             titleInput.value = local.title || '';
0439             if (local.enabled !== undefined)  enabledInput.checked = !!local.enabled;
0440             if (local.renotification_window_hours !== undefined)
0441                 renotificationInp.value = local.renotification_window_hours;
0442             if (local.recipients !== undefined) recipientsInput.value = local.recipients;
0443             cmContent.setValue(local.content || '');
0444             cmData.setValue(local.data || '{}');
0445             setIndicator('saved', 'Local backup restored — save to commit.');
0446             updateSaveState();
0447         } catch (e) { setIndicator('error', 'Restore failed: ' + e.message); }
0448     };
0449 })();
0450 </script>
0451 {% endblock %}