File indexing completed on 2026-04-28 07:24:56
0001 {% extends 'base.html' %}
0002 {% load static %}
0003 {% load swf_fmt %}
0004 {% load humanize %}
0005
0006 {% block title %}Alarms — ePIC Production Monitor{% endblock %}
0007
0008 {% block extra_head %}
0009 <script>
0010 // In-place refresh: fetch the same URL, swap the <main> contents.
0011 // The browser keeps scrollY untouched during innerHTML replacement,
0012 // so there's no reload, no scroll save/restore dance, no ghosting.
0013 (function() {
0014 var INTERVAL_MS = {{ auto_refresh_seconds }} * 1000;
0015
0016 function reExecuteScripts(root) {
0017 // innerHTML inserts <script> nodes without executing them.
0018 // Re-create each so inline scripts (countdown, scroll handlers)
0019 // run in the new content.
0020 var old = root.querySelectorAll('script');
0021 for (var i = 0; i < old.length; i++) {
0022 var s = document.createElement('script');
0023 if (old[i].src) s.src = old[i].src;
0024 else s.textContent = old[i].textContent;
0025 old[i].parentNode.replaceChild(s, old[i]);
0026 }
0027 }
0028
0029 async function refresh() {
0030 try {
0031 var resp = await fetch(window.location.href, {
0032 cache: 'no-store',
0033 credentials: 'same-origin',
0034 });
0035 if (!resp.ok) return;
0036 var text = await resp.text();
0037 var doc = new DOMParser().parseFromString(text, 'text/html');
0038 var newMain = doc.querySelector('main');
0039 var curMain = document.querySelector('main');
0040 if (!newMain || !curMain) return;
0041 // Skip if the fetched doc doesn't look like our dashboard
0042 // (e.g. session expired → login page).
0043 if (!newMain.querySelector('.alarms-scope')) return;
0044 curMain.innerHTML = newMain.innerHTML;
0045 reExecuteScripts(curMain);
0046 } catch (e) { /* network blip — try again next tick */ }
0047 }
0048 setInterval(refresh, INTERVAL_MS);
0049 })();
0050 </script>
0051 {% endblock %}
0052
0053 {% block content %}
0054 <link rel="stylesheet" href="{% static 'css/state-colors.css' %}">
0055 <style>
0056 html { scroll-behavior: auto !important; }
0057 .alarm-section-header {
0058 background: #d8d8d8;
0059 color: #222;
0060 padding: 6px 12px;
0061 border-radius: 3px;
0062 font-size: 1.1em;
0063 font-weight: 600;
0064 margin-top: 1.8em;
0065 margin-bottom: 0.8em;
0066 }
0067 .alarm-section-header .num {
0068 color: #555;
0069 font-weight: 700;
0070 margin-right: 8px;
0071 }
0072 /* Kill Bootstrap's `code` pink-on-white within the alarms dashboard —
0073 identifiers (team names, alarm slugs, entry_ids) should
0074 not read as errors. */
0075 .alarms-scope code,
0076 .alarm-label {
0077 font-family: "SF Mono", Monaco, Menlo, monospace;
0078 font-size: 0.92em;
0079 color: #444;
0080 background: #ececec;
0081 padding: 1px 6px;
0082 border-radius: 3px;
0083 }
0084 .quiet-badge {
0085 background: #a58700; color: #fff; padding: 1px 8px;
0086 border-radius: 3px; font-size: 0.75em; font-weight: 500;
0087 margin-left: 6px;
0088 }
0089 /* Description / body panel — soft grey instead of white card. */
0090 .alarm-desc {
0091 background: #f3f3f3;
0092 color: #333;
0093 padding: 10px 14px;
0094 border-radius: 4px;
0095 max-width: 1000px;
0096 margin-bottom: 0.8em;
0097 }
0098 .alarm-desc .label {
0099 color: #777;
0100 font-size: 0.82em;
0101 text-transform: uppercase;
0102 letter-spacing: 0.05em;
0103 margin-bottom: 4px;
0104 }
0105 .alarm-desc pre {
0106 white-space: pre-wrap;
0107 margin: 0;
0108 color: #333;
0109 font-family: inherit;
0110 font-size: 0.95em;
0111 }
0112 </style>
0113 <div class="container-fluid mt-3 alarms-scope" style="max-width: 1400px;">
0114 <div class="d-flex justify-content-between align-items-baseline mb-2">
0115 <h2 class="mb-0">Alarms</h2>
0116 <div class="text-muted" style="font-size: 0.9em;">
0117 Built at {{ built_at_dt|date:"Y-m-d H:i:s" }} ·
0118 all times {{ built_at_dt|date:"T" }} ·
0119 auto-refresh {{ auto_refresh_seconds }}s
0120 </div>
0121 </div>
0122 <div class="mb-3" style="font-size: 0.92em;">
0123 <a href="#summary">alarm summary</a>
0124 {% for r in summary_rows %}
0125 <span class="text-muted">·</span> <a href="#section-{{ r.entry_id }}">{{ r.title|default:r.name }}</a>
0126 {% endfor %}
0127 <span class="text-muted">·</span> <a href="#recent-runs">recent crons</a>
0128 </div>
0129
0130 {% if health.status == 'ok' %}
0131 <div class="ok_fill p-3 mb-3" style="border-radius:4px;">
0132 <strong>Cron engine: OK.</strong>
0133 {% for r in health.reasons %}{{ r }} {% endfor %}
0134 Cycle 5 min. Next check in <span id="next-tick-countdown" data-secs="{{ next_check_seconds }}">{{ next_check_seconds }}s</span>.
0135 </div>
0136 {% elif health.status == 'warn' %}
0137 <div class="warning_fill p-3 mb-3" style="border-radius:4px;">
0138 <strong>Cron engine: Warning.</strong>
0139 <ul class="mb-0">{% for r in health.reasons %}<li>{{ r }}</li>{% endfor %}</ul>
0140 <div class="mt-1">Cycle 5 min. Next check in <span id="next-tick-countdown" data-secs="{{ next_check_seconds }}">{{ next_check_seconds }}s</span>.</div>
0141 </div>
0142 {% elif health.status == 'bad' %}
0143 <div class="failed_fill p-3 mb-3" style="border-radius:4px;">
0144 <strong>Cron engine: BAD.</strong>
0145 <ul class="mb-0">{% for r in health.reasons %}<li>{{ r }}</li>{% endfor %}</ul>
0146 <div class="mt-1">Cycle 5 min. Next check in <span id="next-tick-countdown" data-secs="{{ next_check_seconds }}">{{ next_check_seconds }}s</span>.</div>
0147 </div>
0148 {% else %}
0149 <div class="unknown_fill p-3 mb-3" style="border-radius:4px;">
0150 <strong>Cron engine: unknown.</strong>
0151 {% for r in health.reasons %}{{ r }} {% endfor %}
0152 Cycle 5 min. Next check in <span id="next-tick-countdown" data-secs="{{ next_check_seconds }}">{{ next_check_seconds }}s</span>.
0153 </div>
0154 {% endif %}
0155 <script>
0156 (function(){
0157 var el = document.getElementById('next-tick-countdown');
0158 if (!el) return;
0159 var secs = parseInt(el.getAttribute('data-secs'), 10);
0160 function fmt(s) {
0161 if (s <= 0) return 'due any moment';
0162 var m = Math.floor(s / 60), r = s % 60;
0163 return m > 0 ? (m + 'm ' + r + 's') : (r + 's');
0164 }
0165 el.textContent = fmt(secs);
0166 setInterval(function(){
0167 secs -= 1;
0168 el.textContent = fmt(secs);
0169 }, 1000);
0170 })();
0171 </script>
0172
0173 <section class="mb-4">
0174 <div class="alarm-section-header">
0175 Teams
0176 <span class="text-muted" style="font-size:0.85em; font-weight:400;">
0177 — reusable recipient lists, referenced as <code>@teamname</code>
0178 </span>
0179 </div>
0180 {% if teams %}
0181 <table class="table table-sm table-hover" style="max-width:1100px;">
0182 <thead>
0183 <tr>
0184 <th>Name</th>
0185 <th>Title</th>
0186 <th>Members</th>
0187 <th>Modified</th>
0188 <th></th>
0189 </tr>
0190 </thead>
0191 <tbody>
0192 {% for t in teams %}
0193 <tr>
0194 <td><code>{{ t.name }}</code></td>
0195 <td>{{ t.title|default:'' }}</td>
0196 <td><small>{{ t.content }}</small></td>
0197 <td><small>{{ t.modified|fmt_dt }}</small></td>
0198 <td>
0199 <a class="btn btn-dark-green btn-sm"
0200 href="{% url 'monitor_app:team_edit' at_name=t.name %}">Edit</a>
0201 </td>
0202 </tr>
0203 {% endfor %}
0204 </tbody>
0205 </table>
0206 {% else %}
0207 <p class="text-muted">No teams defined yet.</p>
0208 {% endif %}
0209
0210 <a class="btn btn-sm btn-primary"
0211 href="{% url 'monitor_app:team_new' %}">Create team</a>
0212 </section>
0213
0214 <div id="summary" class="alarm-section-header">
0215 Alarm summary
0216 <span class="text-muted" style="font-size:0.85em; font-weight:400;">
0217 — one row per configured alarm
0218 </span>
0219 </div>
0220 <form method="get" class="d-flex align-items-center gap-2 mb-2">
0221 <label class="text-muted">Since:</label>
0222 <input type="number" name="hours" value="{{ hours }}" min="1" max="720"
0223 class="form-control form-control-sm" style="width:90px;"
0224 title="How far back to look when counting/listing past alarms. View filter only; does not change alarm behavior.">
0225 <span class="text-muted">hours ago</span>
0226 <button class="btn btn-primary btn-sm">Apply</button>
0227 </form>
0228 <table class="table table-sm table-hover">
0229 <thead>
0230 <tr>
0231 <th>Alarm</th>
0232 <th title="Per-alarm email switch. When OFF, the alarm's detection still runs and events still appear here — only email notifications are suppressed. Flip in the editor.">Emails</th>
0233 <th>Alarm tasks in last {{ hours }}h</th>
0234 <th>Current alarm tasks</th>
0235 <th>Last alarm</th>
0236 </tr>
0237 </thead>
0238 <tbody>
0239 {% for r in summary_rows %}
0240 <tr>
0241 <td>
0242 <a href="#section-{{ r.entry_id }}"><span class="text-muted">Alarm {{ forloop.counter }}:</span> {{ r.title|default:r.name }}</a>
0243 {% if r.quiet %}<span class="quiet-badge" title="No detections in the last few runs despite prior history — may indicate a silently broken alarm.">quiet</span>{% endif %}
0244 </td>
0245 <td class="{% if r.enabled %}ok_fill{% else %}paused_fill{% endif %}"
0246 title="{% if r.enabled %}Emails are ON — notifications are sent on new detections.{% else %}Emails are OFF — detection still runs and events appear on this dashboard; mail is suppressed.{% endif %}">
0247 {% if r.enabled %}on{% else %}off{% endif %}
0248 </td>
0249 <td class="{% if r.count %}failed_fill{% else %}ok_fill{% endif %}">{{ r.count }}</td>
0250 <td class="{% if r.active %}failed_fill{% else %}ok_fill{% endif %}">{{ r.active }}</td>
0251 <td>{% if r.last_fired_dt %}{{ r.last_fired_dt|naturaltime }}{% else %}—{% endif %}</td>
0252 </tr>
0253 {% empty %}
0254 <tr><td colspan="5" class="text-muted">No alarm configs defined.</td></tr>
0255 {% endfor %}
0256 </tbody>
0257 </table>
0258
0259 {% for sec in sections %}
0260 <section id="section-{{ sec.config.entry_id }}">
0261 <div class="alarm-section-header d-flex justify-content-between align-items-center">
0262 <div>
0263 <span class="num">Alarm {{ forloop.counter }}:</span>
0264 {{ sec.config.title|default:sec.config.name }}
0265 {% if not sec.config.enabled %}
0266 <span class="paused_fill ms-1" style="font-size:0.75em; padding: 2px 10px; border-radius:3px; font-weight:500;"
0267 title="Emails OFF — detection still runs; only email notifications are suppressed.">emails off</span>
0268 {% endif %}
0269 <span class="text-muted ms-2" style="font-size:0.8em; font-weight:400;">
0270 <code>{{ sec.config.entry_id }}</code>
0271 </span>
0272 </div>
0273 <a class="btn btn-dark-green btn-sm"
0274 href="{% url 'monitor_app:alarm_config_edit' entry_id=sec.config.entry_id %}">Edit</a>
0275 </div>
0276
0277 <table class="table table-sm mb-2" style="max-width:1000px;">
0278 <tbody>
0279 <tr><th style="width:180px;">Created / modified</th>
0280 <td>{{ sec.config.created|fmt_dt }} / {{ sec.config.modified|fmt_dt }}</td></tr>
0281 <tr><th>Recipients</th>
0282 <td>{{ sec.config.recipients_text|default:'—' }}</td></tr>
0283 <tr><th>Source</th>
0284 <td>
0285 <a href="https://github.com/BNLNPPS/swf-remote/blob/main/alarms/swf_alarms/alarms/{{ sec.config.name }}.py"
0286 target="_blank" rel="noopener">alarms/swf_alarms/alarms/{{ sec.config.name }}.py</a>
0287 </td></tr>
0288 </tbody>
0289 </table>
0290
0291 <h4 style="font-size: 1em;" class="mt-3 mb-2">
0292 Current alarm tasks —
0293 {% if sec.active %}<span class="text-danger">{{ sec.active }}</span>{% else %}0{% endif %}
0294 </h4>
0295 {% if sec.active_rows %}
0296 <table class="table table-sm table-hover">
0297 <thead>
0298 <tr>
0299 <th>Description</th>
0300 <th>Metric</th>
0301 <th>First fired</th>
0302 </tr>
0303 </thead>
0304 <tbody>
0305 {% for ar in sec.active_rows %}
0306 <tr>
0307 <td>
0308 <a href="{% url 'monitor_app:alarm_task_history' entry_id=sec.config.entry_id %}?key={{ ar.dedupe_key|urlencode }}&hours={{ hours }}">
0309 {{ ar.subject }}
0310 </a>
0311 </td>
0312 <td>{{ ar.metric|default:'—' }}</td>
0313 <td>{{ ar.fire_time|fmt_dt }}{% if ar.fire_time_dt %} <span class="text-muted">({{ ar.fire_time_dt|naturaltime }})</span>{% endif %}</td>
0314 </tr>
0315 {% endfor %}
0316 </tbody>
0317 </table>
0318 {% else %}
0319 <p class="text-muted">No active alarms right now.</p>
0320 {% endif %}
0321
0322 {% if sec.config.content %}
0323 <div class="alarm-desc">
0324 <div class="label">Description / email body</div>
0325 <pre>{{ sec.config.content }}</pre>
0326 </div>
0327 {% endif %}
0328 {% if sec.params_table %}
0329 <div style="margin-bottom: 0.8em;">
0330 <div class="text-muted" style="font-size:0.82em; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:4px;">Params</div>
0331 <table class="table table-sm" style="max-width:1100px; font-size:0.9em;">
0332 <thead>
0333 <tr>
0334 <th>Param</th>
0335 <th>Value</th>
0336 <th>Type</th>
0337 <th>Required</th>
0338 <th>Default</th>
0339 <th>Description</th>
0340 </tr>
0341 </thead>
0342 <tbody>
0343 {% for p in sec.params_table %}
0344 <tr{% if p.required %} style="font-weight:600;"{% endif %}>
0345 <td><code>{{ p.name }}</code></td>
0346 <td>{% if p.has_value %}<code>{{ p.value }}</code>{% else %}<span class="text-muted">—</span>{% endif %}</td>
0347 <td><small>{{ p.type }}</small></td>
0348 <td>{% if p.required %}<span class="text-danger">yes</span>{% else %}no{% endif %}</td>
0349 <td><small>{% if p.has_default %}{{ p.default|default_if_none:"—" }}{% else %}—{% endif %}</small></td>
0350 <td><small>{{ p.description }}</small></td>
0351 </tr>
0352 {% endfor %}
0353 </tbody>
0354 </table>
0355 </div>
0356 {% endif %}
0357 {% if sec.preview_body %}
0358 <div style="margin-bottom: 0.8em;">
0359 <div class="text-muted" style="font-size:0.82em; text-transform:uppercase; letter-spacing:0.05em; margin-bottom:4px;">Current email preview (what would ship now if emails were on)</div>
0360 <pre style="background:#f3f3f3; color:#333; padding:10px 14px; border:1px solid #e0e0e0; border-radius:4px; font-size:12.5px; margin:0; white-space:pre-wrap; max-width:1100px;"><span class="text-muted">Subject:</span> <strong>{{ sec.preview_subject }}</strong>
0361 {{ sec.preview_body }}</pre>
0362 </div>
0363 {% endif %}
0364 </section>
0365 {% endfor %}
0366
0367 <div id="recent-runs" class="alarm-section-header">
0368 Recent cron engine runs
0369 <span class="text-muted" style="font-size:0.85em; font-weight:400;">
0370 — each run, with per-alarm breakdown
0371 </span>
0372 </div>
0373 <table class="table table-sm">
0374 <thead>
0375 <tr>
0376 <th>Started</th>
0377 <th>Per-alarm results</th>
0378 </tr>
0379 </thead>
0380 <tbody>
0381 {% for r in recent_runs %}
0382 <tr>
0383 <td style="white-space:nowrap;">{{ r.data.started_at|fmt_dt }}</td>
0384 <td>
0385 {% if r.data.per_alarm %}
0386 {% for alarm_id, pc in r.data.per_alarm.items %}
0387 <div style="font-size:0.9em;">
0388 <a href="{% url 'monitor_app:alarm_run_report' run_uuid=r.id entry_id=alarm_id %}"><code>{{ alarm_id }}</code></a>:
0389 seen {{ pc.alarms_seen|default:0 }}{% if pc.bundle_sent %}, emailed{% endif %}{% if pc.errors %}, <span class="text-danger">error{% if pc.error_message %} — {{ pc.error_message }}{% endif %}</span>{% endif %}
0390 </div>
0391 {% endfor %}
0392 {% else %}
0393 <span class="text-muted">—</span>
0394 {% endif %}
0395 </td>
0396 </tr>
0397 {% endfor %}
0398 </tbody>
0399 </table>
0400 </div>
0401 {% endblock %}