File indexing completed on 2026-07-21 08:46:28
0001 {% load static tz %}
0002 <!DOCTYPE html>
0003 <html lang="en">
0004 <head>
0005 <meta charset="UTF-8">
0006 <meta name="viewport" content="width=device-width, initial-scale=1.0">
0007 <title>{% block title %}SWF Monitor{% endblock %}</title>
0008 <script>
0009 (function() {
0010 var mode = 'production';
0011 try {
0012 mode = localStorage.getItem('navMode') || 'production';
0013 } catch (e) {}
0014 if (mode !== 'testbed') mode = 'production';
0015 document.documentElement.setAttribute('data-nav-mode', mode);
0016 })();
0017 </script>
0018 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
0019 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
0020 <link rel="stylesheet" href="{% static 'css/style.css' %}">
0021 <link rel="stylesheet" href="{% static 'css/state-colors.css' %}">
0022 <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
0023 <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
0024 <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
0025 <script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
0026 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
0027 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
0028 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
0029 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
0030 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
0031 <style>
0032 body {
0033 margin: 0;
0034 padding: 0;
0035 }
0036 main {
0037 padding: 0.65rem 1rem 1rem;
0038 position: relative;
0039 }
0040 main > .container,
0041 main > .container-fluid {
0042 margin-top: 0 !important;
0043 }
0044 main > h1,
0045 main > h2,
0046 main > .container > h1,
0047 main > .container > h2,
0048 main > .container-fluid > h1,
0049 main > .container-fluid > h2,
0050 main > .container > div.mb-3 > h1,
0051 main > .container > div.mb-3 > h2,
0052 main > .container > div.mb-4 > div > h1,
0053 main > .container > div.mb-4 > div > h2,
0054 main > .container > .d-flex h1,
0055 main > .container > .d-flex h2,
0056 main > .container-fluid > div.mb-3 > h1,
0057 main > .container-fluid > div.mb-3 > h2,
0058 main > .container-fluid > div.mb-4 > div > h1,
0059 main > .container-fluid > div.mb-4 > div > h2,
0060 main > .container-fluid > .d-flex h1,
0061 main > .container-fluid > .d-flex h2 {
0062 font-size: 1.7rem;
0063 line-height: 1.2;
0064 margin-top: 0 !important;
0065 }
0066 main > h1,
0067 main > h2,
0068 main > .container > h1,
0069 main > .container > h2,
0070 main > .container-fluid > h1,
0071 main > .container-fluid > h2,
0072 main > .container > div.mb-3 > h1,
0073 main > .container > div.mb-3 > h2,
0074 main > .container > div.mb-4 > div > h1,
0075 main > .container > div.mb-4 > div > h2,
0076 main > .container-fluid > div.mb-3 > h1,
0077 main > .container-fluid > div.mb-3 > h2,
0078 main > .container-fluid > div.mb-4 > div > h1,
0079 main > .container-fluid > div.mb-4 > div > h2 {
0080 margin-bottom: 0.2rem !important;
0081 }
0082 main > .container > .d-flex h1,
0083 main > .container > .d-flex h2,
0084 main > .container-fluid > .d-flex h1,
0085 main > .container-fluid > .d-flex h2 {
0086 margin-bottom: 0 !important;
0087 }
0088 /* Button roles (see CLAUDE.md "Button roles" — the AI-facing table):
0089 constructive create/save/new/import = btn-primary; object verbs
0090 edit/copy on existing items = btn-dark-green; consequential commits
0091 submit/lock/publish/approve = btn-success; suggestion-bar Apply =
0092 btn-warning btn-apply; destructive delete = btn-danger; neutral
0093 cancel/utility = btn-secondary; btn-outline-* reserved for stateful
0094 toggles where fill signals on/off. All btn-sm. */
0095 .btn.btn-dark-green {
0096 background-color: #166534; border-color: #166534; color: #fff;
0097 }
0098 .btn.btn-dark-green:hover, .btn.btn-dark-green:focus {
0099 background-color: #14532d; border-color: #14532d; color: #fff;
0100 }
0101 .btn.btn-dark-green:disabled, .btn.btn-dark-green.disabled {
0102 background-color: #6c757d; border-color: #6c757d; color: #e9ecef;
0103 }
0104 #js-error-overlay {
0105 position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
0106 z-index: 100000; max-width: 720px; width: calc(100vw - 2rem);
0107 background: #c9302c; color: #fff; padding: 1.25rem 1.5rem;
0108 border: 3px solid #7a1a18; border-radius: .5rem;
0109 box-shadow: 0 10px 30px rgba(0,0,0,.4);
0110 font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
0111 display: none;
0112 }
0113 #js-error-overlay h3 { margin: 0 0 .5rem; font-size: 1.1rem; }
0114 #js-error-overlay pre {
0115 background: rgba(0,0,0,.25); color: #fff; padding: .5rem .75rem;
0116 border-radius: .25rem; margin: .5rem 0 0; font-size: .8rem;
0117 white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow: auto;
0118 }
0119 #js-error-overlay .close-btn {
0120 position: absolute; top: .5rem; right: .75rem;
0121 background: transparent; border: none; color: #fff; font-size: 1.5rem;
0122 cursor: pointer; line-height: 1;
0123 }
0124 nav {
0125 display: flex;
0126 align-items: center;
0127 gap: .8em;
0128 }
0129 .sticky-header {
0130 position: sticky;
0131 top: 0;
0132 z-index: 1030;
0133 }
0134 .nav-spacer {
0135 flex: 1 1 auto;
0136 }
0137 .nav-auth {
0138 display: flex;
0139 gap: .8em;
0140 }
0141 .main-time-info {
0142 color: #667085;
0143 float: right;
0144 font-size: .75rem;
0145 margin: -.25rem 0 .25rem 1rem;
0146 white-space: nowrap;
0147 }
0148 nav a {
0149 color: #ADD8E6;
0150 }
0151 nav a.nav-active {
0152 text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0153 }
0154 /* Dropdown Button */
0155 .dropbtn {
0156 background-color: inherit;
0157 color: #8fc7e8;
0158 text-decoration: none;
0159 padding: 0;
0160 font-size: inherit;
0161 border: none;
0162 cursor: pointer;
0163 }
0164 .dropbtn.nav-active {
0165 text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0166 }
0167
0168 /* The container <div> - needed to position the dropdown content */
0169 .dropdown {
0170 position: relative;
0171 display: inline-block;
0172 margin-right: .35em;
0173 }
0174
0175 /* Dropdown Content (Hidden by Default) */
0176 .dropdown-content {
0177 display: none;
0178 position: absolute;
0179 background-color: #f9f9f9;
0180 min-width: 160px;
0181 box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
0182 z-index: 1;
0183 }
0184
0185 /* Links inside the dropdown */
0186 .dropdown-content a {
0187 color: black;
0188 padding: 12px 16px;
0189 text-decoration: none;
0190 display: block;
0191 }
0192 .dropdown-content a.nav-active {
0193 background-color: #e9ecef;
0194 }
0195
0196 /* Change color of dropdown links on hover */
0197 .dropdown-content a:hover {background-color: #f1f1f1}
0198
0199 /* Show the dropdown menu on hover */
0200 .dropdown:hover .dropdown-content {
0201 display: block;
0202 }
0203 /* Nav mode flipper */
0204 .nav-mode { display: none; }
0205 .nav-mode.active { display: contents; }
0206 html[data-nav-mode="production"] .nav-production,
0207 html[data-nav-mode="testbed"] .nav-testbed { display: contents; }
0208 #mode-label {
0209 border-right: 1px solid rgba(255,255,255,.2);
0210 color: #fff;
0211 font-weight: 700;
0212 padding-right: 12px;
0213 margin-right: 4px;
0214 }
0215 #mode-label .mode-title { display: none; }
0216 html[data-nav-mode="production"] #mode-label .mode-title-production,
0217 html[data-nav-mode="testbed"] #mode-label .mode-title-testbed { display: inline; }
0218 .nav-label {
0219 color: #fff;
0220 font-weight: 700;
0221 }
0222 .nav-panda-label {
0223 margin-left: 1.5em;
0224 }
0225 .nav-analysis {
0226 margin-left: 1.5em;
0227 }
0228 .nav-system-error { color: #ff8174 !important; font-weight: 700; }
0229 /* One-click copy icon for IDs — see swf_fmt.copy_btn template tag */
0230 .copy-btn {
0231 background: none; border: none; color: #6c757d; cursor: pointer;
0232 padding: 0 .25rem; font-size: .85em; line-height: 1; vertical-align: baseline;
0233 }
0234 .copy-btn:hover { color: #0d6efd; }
0235 .copy-btn.copied { color: #198754; }
0236 /* AI presence — the universal convention for material attributable to
0237 the AI (proposals, recommendation filters, AI-authored blocks):
0238 dark purple ink on light purple ground. Tune the system-wide AI
0239 look here and nowhere else. */
0240 :root {
0241 --ai-fg: #5b21b6;
0242 --ai-bg: #f8f4fe;
0243 /* Lavender ground always wears a 1px border in the ink color. */
0244 --ai-border: var(--ai-fg);
0245 }
0246 .ai-attr {
0247 color: var(--ai-fg);
0248 background-color: var(--ai-bg);
0249 border: 1px solid var(--ai-border);
0250 border-radius: .5em;
0251 padding: 0 .45em;
0252 font-weight: 600;
0253 }
0254 /* Purple ink always sits on lavender ground — the universal pairing. */
0255 .ai-attr-text {
0256 color: var(--ai-fg); background-color: var(--ai-bg);
0257 border: 1px solid var(--ai-border);
0258 border-radius: .35em; padding: 0 .3em; font-weight: 600;
0259 }
0260 /* Lavender fill for AI-owned containers (home-page boxes, panels). */
0261 .ai-fill {
0262 background-color: var(--ai-bg);
0263 border: 1px solid var(--ai-border) !important;
0264 }
0265 button.ai-attr:hover, a.ai-attr:hover {
0266 background-color: var(--ai-fg); color: #fff;
0267 border-color: var(--ai-fg);
0268 }
0269 </style>
0270 </head>
0271 <body>
0272 <div id="js-error-overlay" role="alert">
0273 <button class="close-btn" onclick="document.getElementById('js-error-overlay').style.display='none';">×</button>
0274 <h3>JavaScript error</h3>
0275 <div id="js-error-msg"></div>
0276 <pre id="js-error-detail"></pre>
0277 </div>
0278 <script>
0279 (function() {
0280 function show(title, detail) {
0281 var box = document.getElementById('js-error-overlay');
0282 var msg = document.getElementById('js-error-msg');
0283 var det = document.getElementById('js-error-detail');
0284 if (!box || !msg || !det) return;
0285 msg.textContent = title || 'Error';
0286 det.textContent = detail || '';
0287 box.style.display = 'block';
0288 }
0289 window.addEventListener('error', function(ev) {
0290 var err = ev.error;
0291 var loc = (ev.filename || '') + (ev.lineno ? ':' + ev.lineno + ':' + (ev.colno || 0) : '');
0292 var stack = err && err.stack ? err.stack : '';
0293 show(ev.message || 'Script error', (loc ? loc + '\n\n' : '') + stack);
0294 });
0295 window.addEventListener('unhandledrejection', function(ev) {
0296 var r = ev.reason;
0297 var msg = (r && r.message) ? r.message : String(r);
0298 var stack = (r && r.stack) ? r.stack : '';
0299 show('Unhandled promise rejection: ' + msg, stack);
0300 });
0301 })();
0302 // One-click copy-to-clipboard handler for .copy-btn buttons rendered by
0303 // the copy_btn template tag (swf_fmt). Swaps the icon to a checkmark for
0304 // 1s on success so the user sees the click landed.
0305 document.addEventListener('click', function(ev) {
0306 var btn = ev.target.closest('.copy-btn');
0307 if (!btn) return;
0308 ev.preventDefault();
0309 var text = btn.getAttribute('data-copy') || '';
0310 if (!text || !navigator.clipboard) return;
0311 navigator.clipboard.writeText(text).then(function() {
0312 var icon = btn.querySelector('i');
0313 if (!icon) return;
0314 var prev = icon.className;
0315 icon.className = 'bi bi-clipboard-check';
0316 btn.classList.add('copied');
0317 setTimeout(function() {
0318 icon.className = prev;
0319 btn.classList.remove('copied');
0320 }, 1000);
0321 });
0322 });
0323 // Near-zero-latency tooltips via Bootstrap 5 for elements with title=.
0324 // Scoped to state-filled cells and the copy button so we don't spin up
0325 // a tooltip for every cell on every page. show delay 50ms, hide 0ms.
0326 document.addEventListener('DOMContentLoaded', function() {
0327 if (!window.bootstrap || !bootstrap.Tooltip) return;
0328 function init(root) {
0329 root.querySelectorAll(
0330 'td[title]:not([data-bs-tooltip-init]), .copy-btn[title]:not([data-bs-tooltip-init])'
0331 ).forEach(function(el) {
0332 if (!el.getAttribute('title')) return;
0333 el.setAttribute('data-bs-tooltip-init', '1');
0334 new bootstrap.Tooltip(el, {delay: {show: 50, hide: 0}});
0335 });
0336 }
0337 init(document);
0338 // DataTables redraws replace rows; re-init after each draw.
0339 if (window.jQuery) {
0340 jQuery(document).on('draw.dt', function(_, settings) {
0341 init(settings.nTable || document);
0342 });
0343 }
0344 });
0345 </script>
0346 <div class="sticky-header">
0347 <nav>
0348 <div class="dropdown" id="mode-flipper">
0349 <button class="dropbtn" id="mode-label">
0350 <span class="mode-title mode-title-production">epicprod</span>
0351 <span class="mode-title mode-title-testbed">ePIC Testbed</span>
0352 </button>
0353 <div class="dropdown-content">
0354 <a href="{% url 'monitor_app:prod_hub' %}" id="flip-production" onclick="setNavMode('production');">epicprod - ePIC Production</a>
0355 <a href="{% if is_tunnel %}https://pandaserver02.sdcc.bnl.gov/swf-monitor/testbed/{% else %}{% url 'monitor_app:testbed_hub' %}{% endif %}" id="flip-testbed" onclick="setNavMode('testbed');">ePIC Testbed</a>
0356 </div>
0357 </div>
0358
0359 {# ── Testbed mode ── #}
0360 <span class="nav-mode nav-testbed">
0361 <div class="dropdown" id="system-status-dropdown">
0362 <button class="dropbtn{% if active_nav.workflows %} nav-active{% endif %}">Workflows</button>
0363 <div class="dropdown-content">
0364 <a href="{% url 'monitor_app:workflows_home' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Views</a>
0365 <a href="{% url 'monitor_app:workflow_definitions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow definitions</a>
0366 <a href="{% url 'monitor_app:workflow_executions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow executions</a>
0367 <a href="{% url 'monitor_app:namespaces_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Namespaces</a>
0368 </div>
0369 </div>
0370 <div class="dropdown">
0371 <button class="dropbtn{% if active_nav.files %} nav-active{% endif %}">Files</button>
0372 <div class="dropdown-content">
0373 <a href="{% url 'monitor_app:stf_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Files</a>
0374 <a href="{% url 'monitor_app:fastmon_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Samples</a>
0375 <a href="{% url 'monitor_app:tf_slices_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">TF Slices</a>
0376 </div>
0377 </div>
0378 <a href="{% url 'monitor_app:workflow_agents_list' %}" class="{% if active_nav.agents %}nav-active{% endif %}">Agents</a>
0379 <a href="{% url 'monitor_app:subscribers_list' %}" class="{% if active_nav.subscribers %}nav-active{% endif %}">Subscribers</a>
0380 <a href="{% url 'monitor_app:workflow_messages' %}" class="{% if active_nav.messages %}nav-active{% endif %}">Messages</a>
0381 <a href="{% url 'monitor_app:log_summary' %}" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0382 <a href="{% url 'monitor_app:database_tables_list' %}" class="{% if active_nav.database %}nav-active{% endif %}">Database</a>
0383 <a href="{% url 'monitor_app:persistent_state' %}" class="{% if active_nav.state %}nav-active{% endif %}">State</a>
0384 <div class="dropdown">
0385 <button class="dropbtn{% if active_nav.panda_rucio %} nav-active{% endif %}">PanDA/Rucio</button>
0386 <div class="dropdown-content">
0387 <a href="{% url 'monitor_app:panda_hub' %}" class="{% if active_nav.panda_hub %}nav-active{% endif %}">PanDA/Rucio Hub</a>
0388 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0389 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0390 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0391 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0392 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0393 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">EIC PanDA Queues</a>
0394 <a href="{% url 'monitor_app:panda_database_tables_list' %}" class="{% if active_nav.panda_database %}nav-active{% endif %}">PanDA Database</a>
0395 <a href="{% url 'monitor_app:idds_database_tables_list' %}" class="{% if active_nav.idds_database %}nav-active{% endif %}">iDDS Database</a>
0396 <a href="{% url 'monitor_app:rucio_endpoints_list' %}" class="{% if active_nav.rucio_endpoints %}nav-active{% endif %}">Rucio Endpoints</a>
0397 </div>
0398 </div>
0399 </span>
0400
0401 {# ── Production mode ── #}
0402 {# SYNC OBLIGATION: swf-remote renders its own copy of this production nav (swf-remote src/templates/base.html) for its natively served pages. #}
0403 {# Any change to this nav block must be mirrored there — notify/hand off to the devcloud (ec2dev) side with every nav change. #}
0404 <span class="nav-mode nav-production">
0405 <div class="dropdown">
0406 <button class="dropbtn{% if active_nav.requests %} nav-active{% endif %}">Requests</button>
0407 <div class="dropdown-content">
0408 <a href="https://docs.google.com/forms/d/e/1FAIpQLScDqiEaHayAcwBDGAWa4W6k-6yUFzS-XiWuhLolpy64mLk5FA/viewform" target="_blank" rel="noopener">Google request form</a>
0409 <a href="{% url 'pcs:pcs_request_composer' %}">Request Production</a>
0410 <a href="{% url 'pcs:questionnaires_list' %}">Request list</a>
0411 </div>
0412 </div>
0413 <div class="dropdown">
0414 <button class="dropbtn{% if active_nav.pcs %} nav-active{% endif %}">PCS</button>
0415 <div class="dropdown-content">
0416 <a href="{% url 'pcs:pcs_hub' %}" class="{% if active_nav.pcs_hub %}nav-active{% endif %}">PCS Hub</a>
0417 <a href="{% url 'pcs:physics_categories_list' %}" class="{% if active_nav.pcs_categories %}nav-active{% endif %}">Physics Categories</a>
0418 <a href="{% url 'pcs:tag_compose' tag_type='p' %}" class="{% if active_nav.pcs_physics_tags %}nav-active{% endif %}">Physics Tags</a>
0419 <a href="{% url 'pcs:tag_compose' tag_type='e' %}" class="{% if active_nav.pcs_evgen_tags %}nav-active{% endif %}">EvGen Tags</a>
0420 <a href="{% url 'pcs:tag_compose' tag_type='s' %}" class="{% if active_nav.pcs_simu_tags %}nav-active{% endif %}">Simu Tags</a>
0421 <a href="{% url 'pcs:tag_compose' tag_type='r' %}" class="{% if active_nav.pcs_reco_tags %}nav-active{% endif %}">Reco Tags</a>
0422 <a href="{% url 'pcs:tag_compose' tag_type='k' %}" class="{% if active_nav.pcs_background_tags %}nav-active{% endif %}">Background Tags</a>
0423 <a href="{% url 'pcs:datasets_compose' %}" class="{% if active_nav.pcs_datasets %}nav-active{% endif %}">Datasets</a>
0424 <a href="{% url 'pcs:prod_configs_compose' %}" class="{% if active_nav.pcs_configs %}nav-active{% endif %}">Prod Configs</a>
0425 <a href="{% url 'pcs:prod_task_compose' %}?tab=tasks" class="{% if active_nav.pcs_tasks %}nav-active{% endif %}">Prod Tasks</a>
0426 </div>
0427 </div>
0428 <div class="dropdown">
0429 <button class="dropbtn{% if active_nav.campaigns %} nav-active{% endif %}">Campaigns</button>
0430 <div class="dropdown-content">
0431 <a href="{% url 'pcs:pcs_catalog' %}" class="{% if active_nav.campaigns %}nav-active{% endif %}">Campaign Catalog</a>
0432 <a href="{% url 'pcs:pcs_catalog' %}?lifecycle=current&view=progress">Campaign Progress</a>
0433 <a href="{% url 'pcs:pcs_physics_configs' %}">Physics configs by campaign</a>
0434 <a href="{% url 'ai:ai_narratives' %}">Campaign Narratives</a>
0435 <a href="{% url 'monitor_app:ai_content_list' %}?subject_type=campaign_task,campaign">Campaign Assessments</a>
0436 </div>
0437 </div>
0438 <div class="dropdown">
0439 <button class="dropbtn{% if active_nav.ai_content %} nav-active{% endif %}">AI</button>
0440 <div class="dropdown-content">
0441 <a href="{% url 'monitor_app:ai_content_list' %}" class="{% if active_nav.ai_content %}nav-active{% endif %}">Assessments</a>
0442 <a href="{% url 'ai:ai_proposals' %}">Proposals</a>
0443 </div>
0444 </div>
0445 <strong class="nav-label nav-panda-label">PanDA</strong>
0446 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0447 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0448 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0449 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0450 <a href="{% url 'monitor_app:log_list' %}?app_name=epicprod" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0451 <a href="{% url 'monitor_app:alarms_dashboard' %}" class="{% if active_nav.alarms %}nav-active{% endif %}">Alarms</a>
0452 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0453 <div class="dropdown">
0454 <button class="dropbtn{% if active_nav.sites %} nav-active{% endif %}">Sites</button>
0455 <div class="dropdown-content">
0456 <a href="{% url 'monitor_app:compute_usage' %}" class="{% if active_nav.compute_usage %}nav-active{% endif %}">Site usage</a>
0457 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">PanDA queues</a>
0458 </div>
0459 </div>
0460 <a href="{% url 'monitor_app:analysis' %}" class="nav-analysis{% if active_nav.analysis %} nav-active{% endif %}">Analysis</a>
0461 </span>
0462 <span class="nav-spacer"></span>
0463 <div class="dropdown">
0464 <button class="dropbtn{% if system_status_overall == 'error' %} nav-system-error{% endif %}{% if active_nav.system or active_nav.snapper or active_nav.about %} nav-active{% endif %}"
0465 id="system-status-nav-link"
0466 data-system-status="{{ system_status_overall|default:'unknown' }}"
0467 data-system-status-url="{% url 'monitor_app:system_status_json' %}"
0468 {% if system_status_latest_checked_at %}data-system-checked-at="{{ system_status_latest_checked_at|date:'c' }}"{% endif %}
0469 title="{{ system_status_reason|default:'' }}">System</button>
0470 <div class="dropdown-content">
0471 <a href="{% url 'monitor_app:system_status' %}"
0472 id="system-status-dropdown-link"
0473 class="{% if system_status_overall == 'error' %}nav-system-error{% endif %}{% if active_nav.system %} nav-active{% endif %}">System Status</a>
0474 <a href="{% url 'monitor_app:snapper_report' scope='epicprod' %}"
0475 class="{% if active_nav.snapper %}nav-active{% endif %}">Snapper</a>
0476 {% if user.is_authenticated and user.is_staff %}
0477 <a href="{% url 'admin:index' %}">Admin</a>
0478 {% endif %}
0479 <a href="{% url 'monitor_app:about' %}" class="{% if active_nav.about %}nav-active{% endif %}">About</a>
0480 </div>
0481 </div>
0482 <div class="nav-auth">
0483 {% if user.is_authenticated %}
0484 <a href="{% url 'monitor_app:account' %}" class="{% if active_nav.account %}nav-active{% endif %}">{{ user.username }}</a>
0485 <form method="post" action="{% url 'logout' %}" style="display:inline;margin:0;">{% csrf_token %}<button type="submit" style="background:none;border:none;color:inherit;cursor:pointer;padding:0;font:inherit;">Logout</button></form>
0486 {% else %}
0487 <a href="{% url 'login' %}?next={{ request.get_full_path|urlencode }}">Login</a>
0488 {% endif %}
0489 </div>
0490 </nav>
0491 </div>
0492 <main>
0493 {% timezone "America/New_York" %}
0494 <div class="main-time-info">
0495 {% if is_tunnel %}
0496 <a href="https://pandaserver02.sdcc.bnl.gov/swf-monitor{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0497 target="_blank" rel="noopener">Go to page at pandaserver02</a> ·
0498 {% else %}
0499 <a href="{{ external_face_base_url }}/prod{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0500 target="_blank" rel="noopener">Go to page at {{ external_face_base_url|cut:"https://" }}</a> ·
0501 {% endif %}
0502 Built at {% now "Ymd H:i:s" %} {% now "T" %}
0503 </div>
0504 {% endtimezone %}
0505 {% if messages %}
0506 {% for message in messages %}
0507 <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show w-100 mb-0 rounded-0" role="alert"
0508 style="background-color: #e9ecef; border: none; border-bottom: 1px solid #dee2e6; padding: 20px;">
0509 {% if 'safe' in message.tags %}
0510 {{ message|safe }}
0511 {% else %}
0512 {{ message }}
0513 {% endif %}
0514 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
0515 </div>
0516 {% endfor %}
0517 {% endif %}
0518 {% block content %}{% endblock %}
0519 </main>
0520 <script>
0521 function setNavMode(mode) {
0522 localStorage.setItem('navMode', mode);
0523 applyNavMode(mode);
0524 }
0525 function applyNavMode(mode) {
0526 if (mode !== 'testbed') mode = 'production';
0527 document.documentElement.setAttribute('data-nav-mode', mode);
0528 const ft = document.getElementById('flip-testbed');
0529 const fp = document.getElementById('flip-production');
0530 if (ft) ft.classList.toggle('nav-active', mode === 'testbed');
0531 if (fp) fp.classList.toggle('nav-active', mode === 'production');
0532 }
0533 (function() {
0534 var mode = localStorage.getItem('navMode') || 'production';
0535 applyNavMode(mode);
0536 })();
0537 (function() {
0538 const staleAfterSeconds = 15 * 60;
0539 const dropdown = document.getElementById('system-status-dropdown');
0540 const link = document.getElementById('system-status-nav-link');
0541 if (!link) return;
0542 const dropdownLink = document.getElementById('system-status-dropdown-link');
0543 const statusUrl = link.getAttribute('data-system-status-url');
0544 function refreshSystemNav() {
0545 const status = link.getAttribute('data-system-status') || 'unknown';
0546 const checkedAt = link.getAttribute('data-system-checked-at');
0547 let stale = false;
0548 if (checkedAt) {
0549 const ts = Date.parse(checkedAt);
0550 if (!Number.isNaN(ts)) {
0551 stale = (Date.now() - ts) / 1000 > staleAfterSeconds;
0552 }
0553 }
0554 const showError = status === 'error' || stale;
0555 link.classList.toggle('nav-system-error', showError);
0556 if (dropdownLink) dropdownLink.classList.toggle('nav-system-error', showError);
0557 if (stale) {
0558 link.title = 'System status is stale by more than 15 minutes.';
0559 }
0560 }
0561 function pollSystemStatus() {
0562 if (!statusUrl || !window.fetch) return;
0563 fetch(statusUrl, {
0564 cache: 'no-store',
0565 credentials: 'same-origin',
0566 headers: {'Accept': 'application/json'}
0567 }).then(function(resp) {
0568 if (!resp.ok) throw new Error('HTTP ' + resp.status);
0569 return resp.json();
0570 }).then(function(data) {
0571 link.setAttribute('data-system-status', data.overall_status || 'unknown');
0572 if (data.latest_checked_at) {
0573 link.setAttribute('data-system-checked-at', data.latest_checked_at);
0574 }
0575 link.title = data.overall_reason || '';
0576 refreshSystemNav();
0577 }).catch(function() {
0578 refreshSystemNav();
0579 });
0580 }
0581 refreshSystemNav();
0582 pollSystemStatus();
0583 if (dropdown) {
0584 dropdown.addEventListener('mouseenter', pollSystemStatus);
0585 dropdown.addEventListener('focusin', pollSystemStatus);
0586 }
0587 setInterval(refreshSystemNav, 10000);
0588 setInterval(pollSystemStatus, 60000);
0589 })();
0590 </script>
0591 <script>
0592 /* Fit an in-page document editor to the visible window: if the
0593 document overflows its panel, grow the panel to use nearly the
0594 full viewport height. The app-wide utility for editing large
0595 documents — call swfFitEditor(el) when opening an editor, or give
0596 the textarea the swf-fit-editor class to fit on focus. */
0597 window.swfFitEditor = function (el) {
0598 if (!el) return;
0599 const margin = 24;
0600 const avail = window.innerHeight - el.getBoundingClientRect().top - margin;
0601 if (el.scrollHeight > el.clientHeight && avail > el.clientHeight) {
0602 el.style.height = Math.max(avail, 240) + 'px';
0603 }
0604 };
0605 document.addEventListener('focusin', function (e) {
0606 if (e.target.matches && e.target.matches('textarea.swf-fit-editor')) {
0607 window.swfFitEditor(e.target);
0608 }
0609 });
0610
0611 /* The app-wide relative-time ticker: give any element
0612 data-relative-time="<ISO timestamp>" and its text renders and
0613 ticks as 'just now' / 'Ns ago' / 'Nm ago' / 'Nh ago' / 'Nd ago'. */
0614 window.swfFormatElapsed = function (seconds) {
0615 if (seconds < 5) return 'just now';
0616 if (seconds < 60) return seconds + 's ago';
0617 const minutes = Math.floor(seconds / 60);
0618 if (minutes < 60) return minutes + 'm ago';
0619 const hours = Math.floor(minutes / 60);
0620 if (hours < 48) return hours + 'h ago';
0621 return Math.floor(hours / 24) + 'd ago';
0622 };
0623 (function () {
0624 function tick() {
0625 const now = Date.now();
0626 document.querySelectorAll('[data-relative-time]').forEach(function (el) {
0627 const ts = Date.parse(el.getAttribute('data-relative-time'));
0628 if (Number.isNaN(ts)) return;
0629 el.textContent = window.swfFormatElapsed(
0630 Math.max(0, Math.floor((now - ts) / 1000)));
0631 });
0632 }
0633 tick();
0634 setInterval(tick, 1000);
0635 })();
0636 // House sortable tables: every <table class="swf-sortable"> gets
0637 // DataTables click-to-sort headers (the globally loaded house table
0638 // widget), without paging/search chrome. Columns are sortable by
0639 // default across the app; opting out is the special case. See
0640 // CLAUDE.md, House UI helpers.
0641 $(function () {
0642 $('table.swf-sortable').DataTable({
0643 paging: false, searching: false, info: false, order: []
0644 });
0645 });
0646 </script>
0647 </body>
0648 </html>