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 team {{ team_name }} — 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
0013 <style>
0014 .CodeMirror { height: auto !important; border: 1px solid #333; font-family: "SF Mono", Monaco, Menlo, monospace; font-size: 13px; }
0015 .CodeMirror-scroll { min-height: 260px; max-height: 60vh; }
0016 .autosave-indicator { font-size: 12px; color: #888; margin-left: 1em; }
0017 .autosave-indicator.saving { color: #daa520; }
0018 .autosave-indicator.saved { color: #4CAF50; }
0019 .autosave-indicator.error { color: #dc3545; }
0020 .version-row td { padding: 4px 8px; font-size: 12.5px; border-top: 1px solid #2a2a2a; }
0021 .version-row .vnum { color: #daa520; white-space: nowrap; }
0022 .version-row .vts { color: #8a8a8a; white-space: nowrap; }
0023 .version-row .vwho { color: #8a8a8a; white-space: nowrap; }
0024 .local-backup-row td { background: #2a2200 !important; color: #ff9800; }
0025 </style>
0026
0027 <div class="container mt-3" style="max-width: 1000px;">
0028 <p><a href="{% url 'monitor_app:alarms_dashboard' %}">← All alarms</a></p>
0029 {% if create_mode %}
0030 <h2 class="mb-3">New team</h2>
0031 {% else %}
0032 <h2 class="mb-3">Team <code>{{ team_name }}</code></h2>
0033 {% endif %}
0034
0035 <div class="d-flex align-items-center mb-2">
0036 <button id="save-btn" class="btn btn-sm btn-secondary" disabled>Save</button>
0037 <span id="autosave-indicator" class="autosave-indicator"></span>
0038 </div>
0039
0040 {% if create_mode %}
0041 <h3 style="font-size:1em;" class="mt-3">Name</h3>
0042 <p class="text-muted" style="font-size:0.85em;">
0043 Short slug used as <code>@<name></code> in alarm recipient
0044 lists. No leading <code>@</code> — it's added automatically.
0045 </p>
0046 <input type="text" id="name-input" class="form-control"
0047 maxlength="255" placeholder="e.g. eic-ops">
0048 {% endif %}
0049
0050 <h3 style="font-size:1em;" class="mt-3">Title</h3>
0051 <input type="text" id="title-input" class="form-control" value="{{ title }}"
0052 maxlength="255" placeholder="Short human-readable title">
0053
0054 <h3 style="font-size:1em;" class="mt-3">Members (emails, space- or comma-separated)</h3>
0055 <p class="text-muted" style="font-size:0.85em;">
0056 One per line or several per line. Blanks and commas are fine.
0057 </p>
0058 <div id="content-editor">
0059 <textarea id="editor">{{ content }}</textarea>
0060 </div>
0061
0062 <div id="version-history" class="mt-4"></div>
0063 </div>
0064
0065 {% if create_mode %}
0066 <script>
0067 (function() {
0068 var CREATE_URL = "{% url 'monitor_app:team_create' %}";
0069 var nameInput = document.getElementById('name-input');
0070 var titleInput = document.getElementById('title-input');
0071 var cmContent = CodeMirror.fromTextArea(document.getElementById('editor'), {
0072 mode: null, theme: 'material-darker', lineNumbers: true,
0073 lineWrapping: true, tabSize: 2,
0074 });
0075 var indicator = document.getElementById('autosave-indicator');
0076 var saveBtn = document.getElementById('save-btn');
0077 indicator.textContent = 'Enter a name and save to create.';
0078
0079 function updateSaveState() {
0080 var dirty = nameInput.value.trim().length > 0;
0081 saveBtn.disabled = !dirty;
0082 saveBtn.className = 'btn btn-sm ' + (dirty ? 'btn-primary' : 'btn-secondary');
0083 }
0084 nameInput.addEventListener('input', updateSaveState);
0085 updateSaveState();
0086
0087 saveBtn.addEventListener('click', function() {
0088 var nm = nameInput.value.trim();
0089 if (!nm) { indicator.textContent = 'name required'; return; }
0090 indicator.className = 'autosave-indicator saving';
0091 indicator.textContent = 'Creating…';
0092 fetch(CREATE_URL, {
0093 method: 'POST',
0094 headers: {'Content-Type': 'application/json'},
0095 body: JSON.stringify({
0096 name: nm,
0097 title: titleInput.value,
0098 content: cmContent.getValue(),
0099 }),
0100 }).then(function(r) { return r.json().then(function(j){ return {ok: r.ok, j: j}; }); })
0101 .then(function(o) {
0102 if (!o.ok) {
0103 indicator.className = 'autosave-indicator error';
0104 indicator.textContent = 'Error: ' + (o.j.error || 'create failed');
0105 return;
0106 }
0107 window.location = o.j.edit_url;
0108 })
0109 .catch(function(e) {
0110 indicator.className = 'autosave-indicator error';
0111 indicator.textContent = 'Error: ' + e.message;
0112 });
0113 });
0114 })();
0115 </script>
0116 {% else %}
0117 <script>
0118 (function() {
0119 var AT_NAME = "{{ team_name|escapejs }}";
0120 var SAVE_URL = "{% url 'monitor_app:team_save' at_name=team_name %}";
0121 var VERSION_URL = "{% url 'monitor_app:team_version' at_name=team_name version_num=0 %}".slice(0, -2);
0122 var LOCAL_KEY = 'swf-teams-autosave-' + AT_NAME;
0123 var AUTOSAVE_MS = 10000;
0124
0125 var cmContent = CodeMirror.fromTextArea(document.getElementById('editor'), {
0126 mode: null, theme: 'material-darker', lineNumbers: true,
0127 lineWrapping: true, tabSize: 2,
0128 extraKeys: {
0129 'Cmd-S': function() { save(false); },
0130 'Ctrl-S': function() { save(false); },
0131 },
0132 });
0133 var titleInput = document.getElementById('title-input');
0134 var lastTitle = titleInput.value;
0135 var lastContent = cmContent.getValue();
0136
0137 var indicator = document.getElementById('autosave-indicator');
0138 function setIndicator(cls, text) {
0139 indicator.className = 'autosave-indicator ' + cls;
0140 indicator.textContent = text;
0141 }
0142
0143 var saveBtn = document.getElementById('save-btn');
0144 function isDirty() {
0145 return titleInput.value !== lastTitle || cmContent.getValue() !== lastContent;
0146 }
0147 function updateSaveState() {
0148 if (isDirty()) {
0149 saveBtn.classList.remove('btn-secondary');
0150 saveBtn.classList.add('btn-primary');
0151 saveBtn.disabled = false;
0152 } else {
0153 saveBtn.classList.remove('btn-primary');
0154 saveBtn.classList.add('btn-secondary');
0155 saveBtn.disabled = true;
0156 }
0157 }
0158
0159 function save(isAutosave) {
0160 var titleVal = titleInput.value;
0161 var contentVal = cmContent.getValue();
0162 if (isAutosave && titleVal === lastTitle && contentVal === lastContent) {
0163 return Promise.resolve();
0164 }
0165 setIndicator('saving', isAutosave ? 'Autosaving…' : 'Saving…');
0166 return fetch(SAVE_URL, {
0167 method: 'POST',
0168 headers: { 'Content-Type': 'application/json' },
0169 body: JSON.stringify({
0170 title: titleVal, content: contentVal, autosave: !!isAutosave,
0171 }),
0172 }).then(function(r) {
0173 if (!r.ok) return r.json().then(function(j) { throw new Error(j.error || ('HTTP ' + r.status)); });
0174 return r.json();
0175 }).then(function(j) {
0176 lastTitle = titleVal;
0177 lastContent = contentVal;
0178 try { localStorage.removeItem(LOCAL_KEY); } catch (e) {}
0179 setIndicator('saved', (isAutosave ? 'Autosaved' : 'Saved') +
0180 ' — v' + (j.version_num || 0) +
0181 ' at ' + new Date((j.modified || 0) * 1000).toLocaleTimeString());
0182 updateSaveState();
0183 }).catch(function(e) {
0184 setIndicator('error', 'Save failed: ' + e.message);
0185 });
0186 }
0187
0188 document.getElementById('save-btn').addEventListener('click', function() { save(false); });
0189 setInterval(function() { save(true); }, AUTOSAVE_MS);
0190
0191 function backup() {
0192 try {
0193 localStorage.setItem(LOCAL_KEY, JSON.stringify({
0194 title: titleInput.value,
0195 content: cmContent.getValue(),
0196 ts: Date.now(),
0197 }));
0198 } catch (e) {}
0199 }
0200 function onChange() { backup(); updateSaveState(); }
0201 cmContent.on('change', onChange);
0202 titleInput.addEventListener('input', onChange);
0203 updateSaveState();
0204
0205 window.addEventListener('beforeunload', function() {
0206 if (titleInput.value === lastTitle && cmContent.getValue() === lastContent) return;
0207 try {
0208 var blob = new Blob([JSON.stringify({
0209 title: titleInput.value, content: cmContent.getValue(), autosave: true,
0210 })], { type: 'application/json' });
0211 navigator.sendBeacon(SAVE_URL, blob);
0212 } catch (e) {}
0213 });
0214
0215 // Version history (static from page render).
0216 var versions = JSON.parse('{{ versions_json|escapejs }}');
0217 function fmtTs(t) { return t ? new Date(t * 1000).toISOString().replace('T', ' ').slice(0, 19) : ''; }
0218 function esc(s) { return String(s||'').replace(/[&<>"']/g,function(c){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];}); }
0219
0220 var root = document.getElementById('version-history');
0221 var hasLocal = false;
0222 try { hasLocal = !!localStorage.getItem(LOCAL_KEY); } catch (e) {}
0223
0224 var html = '<h3 style="font-size:1em;">Version history <span class="text-muted">(' + versions.length + ')</span></h3>';
0225 html += '<table class="table table-sm"><tbody>';
0226 if (hasLocal) {
0227 try {
0228 var lo = JSON.parse(localStorage.getItem(LOCAL_KEY));
0229 html += '<tr class="local-backup-row"><td class="vnum">local</td>' +
0230 '<td class="vts">' + new Date(lo.ts).toISOString().replace('T',' ').slice(0,19) + '</td>' +
0231 '<td class="vwho">autosave (browser)</td>' +
0232 '<td>' + esc((lo.content || '').slice(0,120)) + '</td>' +
0233 '<td><button class="btn btn-sm btn-outline-warning" onclick="swfTeamRestoreLocal()">Restore</button></td></tr>';
0234 } catch (e) {}
0235 }
0236 for (var i = 0; i < versions.length; i++) {
0237 var v = versions[i];
0238 html += '<tr class="version-row"><td class="vnum">v' + v.version_num + '</td>' +
0239 '<td class="vts">' + fmtTs(v.timestamp) + '</td>' +
0240 '<td class="vwho">' + esc(v.changed_by || '') + '</td>' +
0241 '<td>' + esc((v.preview || '').slice(0,120)) + '</td>' +
0242 '<td><button class="btn btn-sm btn-outline-secondary" onclick="swfTeamRestoreVersion(' + v.version_num + ')">Load</button></td></tr>';
0243 }
0244 html += '</tbody></table>';
0245 root.innerHTML = html;
0246
0247 window.swfTeamRestoreVersion = function(n) {
0248 if (!confirm('Load v' + n + ' into the editor? Unsaved changes will be lost.')) return;
0249 fetch(VERSION_URL + n + '/')
0250 .then(function(r) { return r.json(); })
0251 .then(function(j) {
0252 titleInput.value = j.title || '';
0253 cmContent.setValue(j.content || '');
0254 setIndicator('saved', 'v' + n + ' loaded — edit and save to commit.');
0255 updateSaveState();
0256 })
0257 .catch(function(e) { setIndicator('error', 'Restore failed: ' + e.message); });
0258 };
0259 window.swfTeamRestoreLocal = function() {
0260 try {
0261 var lo = JSON.parse(localStorage.getItem(LOCAL_KEY));
0262 if (!lo) return;
0263 if (!confirm('Restore from local browser backup? Unsaved server state will be overwritten on save.')) return;
0264 titleInput.value = lo.title || '';
0265 cmContent.setValue(lo.content || '');
0266 setIndicator('saved', 'Local backup restored — save to commit.');
0267 updateSaveState();
0268 } catch (e) { setIndicator('error', 'Restore failed: ' + e.message); }
0269 };
0270 })();
0271 </script>
0272 {% endif %}
0273 {% endblock %}