File indexing completed on 2026-08-12 09:36:19
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 routeMode = '{{ nav_mode|default:""|escapejs }}';
0011 var mode = 'production';
0012 try {
0013 if (routeMode === 'testbed' || routeMode === 'production') {
0014 mode = routeMode;
0015 localStorage.setItem('navMode', mode);
0016 } else {
0017 mode = localStorage.getItem('navMode') || 'production';
0018 }
0019 } catch (e) {}
0020 if (mode !== 'testbed') mode = 'production';
0021 document.documentElement.setAttribute('data-nav-mode', mode);
0022 })();
0023 </script>
0024 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
0025 <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
0026 <link rel="stylesheet" href="{% static 'css/style.css' %}">
0027 <link rel="stylesheet" href="{% static 'css/state-colors.css' %}">
0028 <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
0029 <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
0030 <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
0031 <script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
0032 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
0033 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
0034 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
0035 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
0036 <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
0037 <style>
0038 body {
0039 margin: 0;
0040 padding: 0;
0041 }
0042 main {
0043 padding: 0.65rem 1rem 1rem;
0044 position: relative;
0045 }
0046 main > .container,
0047 main > .container-fluid {
0048 margin-top: 0 !important;
0049 }
0050 main > h1,
0051 main > h2,
0052 main > .container > h1,
0053 main > .container > h2,
0054 main > .container-fluid > h1,
0055 main > .container-fluid > h2,
0056 main > .container > div.mb-3 > h1,
0057 main > .container > div.mb-3 > h2,
0058 main > .container > div.mb-4 > div > h1,
0059 main > .container > div.mb-4 > div > h2,
0060 main > .container > .d-flex h1,
0061 main > .container > .d-flex h2,
0062 main > .container-fluid > div.mb-3 > h1,
0063 main > .container-fluid > div.mb-3 > h2,
0064 main > .container-fluid > div.mb-4 > div > h1,
0065 main > .container-fluid > div.mb-4 > div > h2,
0066 main > .container-fluid > .d-flex h1,
0067 main > .container-fluid > .d-flex h2 {
0068 font-size: 1.7rem;
0069 line-height: 1.2;
0070 margin-top: 0 !important;
0071 }
0072 main > h1,
0073 main > h2,
0074 main > .container > h1,
0075 main > .container > h2,
0076 main > .container-fluid > h1,
0077 main > .container-fluid > h2,
0078 main > .container > div.mb-3 > h1,
0079 main > .container > div.mb-3 > h2,
0080 main > .container > div.mb-4 > div > h1,
0081 main > .container > div.mb-4 > div > h2,
0082 main > .container-fluid > div.mb-3 > h1,
0083 main > .container-fluid > div.mb-3 > h2,
0084 main > .container-fluid > div.mb-4 > div > h1,
0085 main > .container-fluid > div.mb-4 > div > h2 {
0086 margin-bottom: 0.2rem !important;
0087 }
0088 main > .container > .d-flex h1,
0089 main > .container > .d-flex h2,
0090 main > .container-fluid > .d-flex h1,
0091 main > .container-fluid > .d-flex h2 {
0092 margin-bottom: 0 !important;
0093 }
0094 /* Button roles (see CLAUDE.md "Button roles" — the AI-facing table):
0095 constructive create/save/new/import = btn-primary; object verbs
0096 edit/copy on existing items = btn-dark-green; consequential commits
0097 submit/lock/publish/approve = btn-success; suggestion-bar Apply =
0098 btn-warning btn-apply; destructive delete = btn-danger; neutral
0099 cancel/utility = btn-secondary; btn-outline-* reserved for stateful
0100 toggles where fill signals on/off. All btn-sm. */
0101 .btn.btn-dark-green {
0102 background-color: #166534; border-color: #166534; color: #fff;
0103 }
0104 .btn.btn-dark-green:hover, .btn.btn-dark-green:focus {
0105 background-color: #14532d; border-color: #14532d; color: #fff;
0106 }
0107 .btn.btn-dark-green:disabled, .btn.btn-dark-green.disabled {
0108 background-color: #6c757d; border-color: #6c757d; color: #e9ecef;
0109 }
0110 #js-error-overlay {
0111 position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
0112 z-index: 100000; max-width: 720px; width: calc(100vw - 2rem);
0113 background: #c9302c; color: #fff; padding: 1.25rem 1.5rem;
0114 border: 3px solid #7a1a18; border-radius: .5rem;
0115 box-shadow: 0 10px 30px rgba(0,0,0,.4);
0116 font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
0117 display: none;
0118 }
0119 #js-error-overlay h3 { margin: 0 0 .5rem; font-size: 1.1rem; }
0120 #js-error-overlay pre {
0121 background: rgba(0,0,0,.25); color: #fff; padding: .5rem .75rem;
0122 border-radius: .25rem; margin: .5rem 0 0; font-size: .8rem;
0123 white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow: auto;
0124 }
0125 #js-error-overlay .close-btn {
0126 position: absolute; top: .5rem; right: .75rem;
0127 background: transparent; border: none; color: #fff; font-size: 1.5rem;
0128 cursor: pointer; line-height: 1;
0129 }
0130 nav {
0131 display: flex;
0132 align-items: center;
0133 gap: .8em;
0134 }
0135 .sticky-header {
0136 position: sticky;
0137 top: 0;
0138 z-index: 1030;
0139 }
0140 /* Latching table headers: tables that fit the page latch beneath the
0141 nav while the page scrolls. A table wider than its available width
0142 is moved into an adaptive two-axis scroll frame; there the header
0143 latches at the frame top and the first two columns latch at the
0144 left. swf-latch-head opts any other table into the house behavior.
0145 border-collapse borders do not travel with a stuck header, so the
0146 header's frame is drawn as an inset shadow. */
0147 table.dataTable thead th,
0148 table.swf-sortable thead th,
0149 table.swf-latch-head thead th {
0150 /* !important: DataTables' stylesheet pins sortable th to
0151 position:relative with a more specific selector. */
0152 position: sticky !important;
0153 top: var(--swf-nav-height, 0px);
0154 z-index: 100;
0155 background-color: var(--bs-body-bg, #fff);
0156 box-shadow: inset 1px 0 0 #dee2e6, inset 0 1px 0 #dee2e6,
0157 inset 0 -1px 0 #dee2e6;
0158 }
0159 .swf-table-frame {
0160 max-width: 100%;
0161 width: 100%;
0162 }
0163 .swf-table-frame.swf-wide-table-scroll {
0164 max-height: var(--swf-table-max-height, 70vh);
0165 overflow: auto;
0166 overscroll-behavior: contain;
0167 scrollbar-gutter: stable;
0168 }
0169 .swf-wide-table-scroll > table {
0170 margin-bottom: 0 !important;
0171 }
0172 .swf-wide-table-scroll > table thead th {
0173 top: 0;
0174 }
0175 /* Wide tables pin two leading columns by default so a compact ID can
0176 retain the useful name beside it. data-swf-pin-columns can lower
0177 that number for an exceptional table. Solid cell backgrounds keep
0178 horizontally scrolling columns from showing through. */
0179 .swf-wide-table[data-swf-active-pin-columns="1"] > tbody > tr > :first-child,
0180 .swf-wide-table[data-swf-active-pin-columns="2"] > tbody > tr > :first-child {
0181 position: sticky;
0182 left: 0;
0183 z-index: 50;
0184 background-color: var(--bs-body-bg, #fff);
0185 }
0186 .swf-wide-table[data-swf-active-pin-columns="2"] > tbody > tr > :nth-child(2) {
0187 position: sticky;
0188 left: var(--swf-first-column-width, 0px);
0189 z-index: 50;
0190 background-color: var(--bs-body-bg, #fff);
0191 box-shadow: inset -1px 0 0 #dee2e6;
0192 }
0193 .swf-wide-table.table-striped[data-swf-active-pin-columns="1"] > tbody > tr:nth-of-type(odd) > :first-child,
0194 .swf-wide-table.table-striped[data-swf-active-pin-columns="2"] > tbody > tr:nth-of-type(odd) > :first-child,
0195 .swf-wide-table.table-striped[data-swf-active-pin-columns="2"] > tbody > tr:nth-of-type(odd) > :nth-child(2) {
0196 background-color: var(--bs-tertiary-bg, #f2f2f2);
0197 }
0198 /* Header cells at the two-axis intersection sit above both the body
0199 columns and the rest of the latching header. */
0200 .swf-wide-table[data-swf-active-pin-columns="1"] thead th:first-child,
0201 .swf-wide-table[data-swf-active-pin-columns="2"] thead th:first-child {
0202 left: 0;
0203 z-index: 101;
0204 }
0205 .swf-wide-table[data-swf-active-pin-columns="2"] thead th:nth-child(2) {
0206 left: var(--swf-first-column-width, 0px);
0207 z-index: 101;
0208 }
0209 .nav-spacer {
0210 flex: 1 1 auto;
0211 }
0212 .nav-auth {
0213 display: flex;
0214 gap: .8em;
0215 }
0216 .main-time-info {
0217 color: #667085;
0218 float: right;
0219 font-size: .75rem;
0220 margin: -.25rem 0 .25rem 1rem;
0221 white-space: nowrap;
0222 }
0223 nav a {
0224 color: #ADD8E6;
0225 }
0226 nav a.nav-active {
0227 text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0228 }
0229 /* Dropdown Button */
0230 .dropbtn {
0231 background-color: inherit;
0232 color: #8fc7e8;
0233 text-decoration: none;
0234 padding: 0;
0235 font-size: inherit;
0236 border: none;
0237 cursor: pointer;
0238 }
0239 .dropbtn.nav-active {
0240 text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0241 }
0242
0243 /* The container <div> - needed to position the dropdown content */
0244 .dropdown {
0245 position: relative;
0246 display: inline-block;
0247 margin-right: .35em;
0248 }
0249
0250 /* Dropdown Content (Hidden by Default) */
0251 .dropdown-content {
0252 display: none;
0253 position: absolute;
0254 background-color: #f9f9f9;
0255 min-width: 160px;
0256 box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
0257 z-index: 1;
0258 }
0259
0260 /* Links inside the dropdown */
0261 .dropdown-content a {
0262 color: black;
0263 padding: 12px 16px;
0264 text-decoration: none;
0265 display: block;
0266 }
0267 .dropdown-content a.nav-active {
0268 background-color: #e9ecef;
0269 }
0270
0271 /* Change color of dropdown links on hover */
0272 .dropdown-content a:hover {background-color: #f1f1f1}
0273
0274 /* Show the dropdown menu on hover */
0275 .dropdown:hover .dropdown-content {
0276 display: block;
0277 }
0278 /* Nav mode flipper */
0279 .nav-mode { display: none; }
0280 .nav-mode.active { display: contents; }
0281 html[data-nav-mode="production"] .nav-production,
0282 html[data-nav-mode="testbed"] .nav-testbed { display: contents; }
0283 #mode-label {
0284 border-right: 1px solid rgba(255,255,255,.2);
0285 color: #fff;
0286 font-weight: 700;
0287 padding-right: 12px;
0288 margin-right: 4px;
0289 }
0290 #mode-label .mode-title { display: none; }
0291 html[data-nav-mode="production"] #mode-label .mode-title-production,
0292 html[data-nav-mode="testbed"] #mode-label .mode-title-testbed { display: inline; }
0293 .nav-label {
0294 color: #fff;
0295 font-weight: 700;
0296 }
0297 .nav-panda-label {
0298 margin-left: 1.5em;
0299 }
0300 .nav-analysis {
0301 margin-left: 1.5em;
0302 }
0303 .nav-system-error { color: #ff8174 !important; font-weight: 700; }
0304 /* One-click copy icon for IDs — see swf_fmt.copy_btn template tag */
0305 .copy-btn {
0306 background: none; border: none; color: #6c757d; cursor: pointer;
0307 padding: 0 .25rem; font-size: .85em; line-height: 1; vertical-align: baseline;
0308 }
0309 .copy-btn:hover { color: #0d6efd; }
0310 .copy-btn.copied { color: #198754; }
0311 /* AI presence — the universal convention for material attributable to
0312 the AI (proposals, recommendation filters, AI-authored blocks):
0313 dark purple ink on light purple ground. Tune the system-wide AI
0314 look here and nowhere else. */
0315 :root {
0316 --ai-fg: #5b21b6;
0317 --ai-bg: #f8f4fe;
0318 /* Lavender ground always wears a 1px border in the ink color. */
0319 --ai-border: var(--ai-fg);
0320 }
0321 .ai-attr {
0322 color: var(--ai-fg);
0323 background-color: var(--ai-bg);
0324 border: 1px solid var(--ai-border);
0325 border-radius: .5em;
0326 padding: 0 .45em;
0327 font-weight: 600;
0328 }
0329 /* Purple ink always sits on lavender ground — the universal pairing. */
0330 .ai-attr-text {
0331 color: var(--ai-fg); background-color: var(--ai-bg);
0332 border: 1px solid var(--ai-border);
0333 border-radius: .35em; padding: 0 .3em; font-weight: 600;
0334 }
0335 /* Lavender fill for AI-owned containers (home-page boxes, panels). */
0336 .ai-fill {
0337 background-color: var(--ai-bg);
0338 border: 1px solid var(--ai-border) !important;
0339 }
0340 button.ai-attr:hover, a.ai-attr:hover {
0341 background-color: var(--ai-fg); color: #fff;
0342 border-color: var(--ai-fg);
0343 }
0344 /* Checkboxes app-wide: Bootstrap's default border is too light
0345 to see; an unticked box must read as present. */
0346 .form-check-input, input[type="checkbox"] {
0347 border: 1.5px solid #495057;
0348 }
0349 </style>
0350 </head>
0351 <body>
0352 <div id="js-error-overlay" role="alert">
0353 <button class="close-btn" onclick="document.getElementById('js-error-overlay').style.display='none';">×</button>
0354 <h3>JavaScript error</h3>
0355 <div id="js-error-msg"></div>
0356 <pre id="js-error-detail"></pre>
0357 </div>
0358 <script>
0359 (function() {
0360 function show(title, detail) {
0361 var box = document.getElementById('js-error-overlay');
0362 var msg = document.getElementById('js-error-msg');
0363 var det = document.getElementById('js-error-detail');
0364 if (!box || !msg || !det) return;
0365 msg.textContent = title || 'Error';
0366 det.textContent = detail || '';
0367 box.style.display = 'block';
0368 }
0369 window.addEventListener('error', function(ev) {
0370 var err = ev.error;
0371 var loc = (ev.filename || '') + (ev.lineno ? ':' + ev.lineno + ':' + (ev.colno || 0) : '');
0372 var stack = err && err.stack ? err.stack : '';
0373 show(ev.message || 'Script error', (loc ? loc + '\n\n' : '') + stack);
0374 });
0375 window.addEventListener('unhandledrejection', function(ev) {
0376 var r = ev.reason;
0377 var msg = (r && r.message) ? r.message : String(r);
0378 var stack = (r && r.stack) ? r.stack : '';
0379 show('Unhandled promise rejection: ' + msg, stack);
0380 });
0381 })();
0382 // One-click copy-to-clipboard handler for .copy-btn buttons rendered by
0383 // the copy_btn template tag (swf_fmt). Swaps the icon to a checkmark for
0384 // 1s on success so the user sees the click landed.
0385 document.addEventListener('click', function(ev) {
0386 var btn = ev.target.closest('.copy-btn');
0387 if (!btn) return;
0388 ev.preventDefault();
0389 var text = btn.getAttribute('data-copy') || '';
0390 if (!text || !navigator.clipboard) return;
0391 navigator.clipboard.writeText(text).then(function() {
0392 var icon = btn.querySelector('i');
0393 if (!icon) return;
0394 var prev = icon.className;
0395 icon.className = 'bi bi-clipboard-check';
0396 btn.classList.add('copied');
0397 setTimeout(function() {
0398 icon.className = prev;
0399 btn.classList.remove('copied');
0400 }, 1000);
0401 });
0402 });
0403 // Near-zero-latency tooltips via Bootstrap 5 for elements with title=.
0404 // Scoped to state-filled cells and the copy button so we don't spin up
0405 // a tooltip for every cell on every page. show delay 50ms, hide 0ms.
0406 document.addEventListener('DOMContentLoaded', function() {
0407 if (!window.bootstrap || !bootstrap.Tooltip) return;
0408 function init(root) {
0409 root.querySelectorAll(
0410 'td[title]:not([data-bs-tooltip-init]), .copy-btn[title]:not([data-bs-tooltip-init])'
0411 ).forEach(function(el) {
0412 if (!el.getAttribute('title')) return;
0413 el.setAttribute('data-bs-tooltip-init', '1');
0414 new bootstrap.Tooltip(el, {delay: {show: 50, hide: 0}});
0415 });
0416 }
0417 init(document);
0418 // DataTables redraws replace rows; re-init after each draw.
0419 if (window.jQuery) {
0420 jQuery(document).on('draw.dt', function(_, settings) {
0421 init(settings.nTable || document);
0422 });
0423 }
0424 });
0425 </script>
0426 <div class="sticky-header">
0427 <nav>
0428 <div class="dropdown" id="mode-flipper">
0429 <button class="dropbtn" id="mode-label">
0430 <span class="mode-title mode-title-production">epicprod</span>
0431 <span class="mode-title mode-title-testbed">ePIC Testbed</span>
0432 </button>
0433 <div class="dropdown-content">
0434 <a href="{% url 'monitor_app:prod_hub' %}" id="flip-production" onclick="setNavMode('production');">epicprod - ePIC Production</a>
0435 <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>
0436 </div>
0437 </div>
0438
0439 {# ── Testbed mode ── #}
0440 <span class="nav-mode nav-testbed">
0441 <div class="dropdown" id="system-status-dropdown">
0442 <button class="dropbtn{% if active_nav.workflows %} nav-active{% endif %}">Workflows</button>
0443 <div class="dropdown-content">
0444 <a href="{% url 'monitor_app:workflows_home' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Views</a>
0445 <a href="{% url 'monitor_app:workflow_definitions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow definitions</a>
0446 <a href="{% url 'monitor_app:workflow_executions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow executions</a>
0447 <a href="{% url 'monitor_app:namespaces_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Namespaces</a>
0448 </div>
0449 </div>
0450 <div class="dropdown">
0451 <button class="dropbtn{% if active_nav.files %} nav-active{% endif %}">Files</button>
0452 <div class="dropdown-content">
0453 <a href="{% url 'monitor_app:stf_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Files</a>
0454 <a href="{% url 'monitor_app:fastmon_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Samples</a>
0455 <a href="{% url 'monitor_app:tf_slices_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">TF Slices</a>
0456 </div>
0457 </div>
0458 <a href="{% url 'monitor_app:workflow_agents_list' %}" class="{% if active_nav.agents %}nav-active{% endif %}">Agents</a>
0459 <a href="{% url 'monitor_app:subscribers_list' %}" class="{% if active_nav.subscribers %}nav-active{% endif %}">Subscribers</a>
0460 <a href="{% url 'monitor_app:workflow_messages' %}" class="{% if active_nav.messages %}nav-active{% endif %}">Messages</a>
0461 <a href="{% url 'monitor_app:log_summary' %}" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0462 <a href="{% url 'monitor_app:database_tables_list' %}" class="{% if active_nav.database %}nav-active{% endif %}">Database</a>
0463 <a href="{% url 'monitor_app:persistent_state' %}" class="{% if active_nav.state %}nav-active{% endif %}">State</a>
0464 <div class="dropdown">
0465 <button class="dropbtn{% if active_nav.panda_rucio %} nav-active{% endif %}">PanDA/Rucio</button>
0466 <div class="dropdown-content">
0467 <a href="{% url 'monitor_app:panda_hub' %}" class="{% if active_nav.panda_hub %}nav-active{% endif %}">PanDA/Rucio Hub</a>
0468 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0469 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0470 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0471 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0472 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0473 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">EIC PanDA Queues</a>
0474 <a href="{% url 'monitor_app:panda_database_tables_list' %}" class="{% if active_nav.panda_database %}nav-active{% endif %}">PanDA Database</a>
0475 <a href="{% url 'monitor_app:idds_database_tables_list' %}" class="{% if active_nav.idds_database %}nav-active{% endif %}">iDDS Database</a>
0476 <a href="{% url 'monitor_app:rucio_endpoints_list' %}" class="{% if active_nav.rucio_endpoints %}nav-active{% endif %}">Rucio Endpoints</a>
0477 </div>
0478 </div>
0479 </span>
0480
0481 {# ── Production mode ── #}
0482 {# SYNC OBLIGATION: swf-remote renders its own copy of this production nav (swf-remote src/templates/base.html) for its natively served pages. #}
0483 {# Any change to this nav block must be mirrored there — notify/hand off to the devcloud (ec2dev) side with every nav change. #}
0484 <span class="nav-mode nav-production">
0485 <div class="dropdown">
0486 <button class="dropbtn{% if active_nav.requests %} nav-active{% endif %}">Requests</button>
0487 <div class="dropdown-content">
0488 <a href="https://docs.google.com/forms/d/e/1FAIpQLScDqiEaHayAcwBDGAWa4W6k-6yUFzS-XiWuhLolpy64mLk5FA/viewform" target="_blank" rel="noopener">Google request form</a>
0489 <a href="{% url 'pcs:pcs_request_composer' %}">Request Production</a>
0490 <a href="{% url 'pcs:questionnaires_list' %}">Request list</a>
0491 </div>
0492 </div>
0493 <div class="dropdown">
0494 <button class="dropbtn{% if active_nav.pcs %} nav-active{% endif %}">PCS</button>
0495 <div class="dropdown-content">
0496 <a href="{% url 'pcs:pcs_hub' %}" class="{% if active_nav.pcs_hub %}nav-active{% endif %}">PCS Hub</a>
0497 <a href="{% url 'pcs:physics_categories_list' %}" class="{% if active_nav.pcs_categories %}nav-active{% endif %}">Physics Categories</a>
0498 <a href="{% url 'pcs:tag_compose' tag_type='p' %}" class="{% if active_nav.pcs_physics_tags %}nav-active{% endif %}">Physics Tags</a>
0499 <a href="{% url 'pcs:tag_compose' tag_type='e' %}" class="{% if active_nav.pcs_evgen_tags %}nav-active{% endif %}">EvGen Tags</a>
0500 <a href="{% url 'pcs:tag_compose' tag_type='s' %}" class="{% if active_nav.pcs_simu_tags %}nav-active{% endif %}">Simu Tags</a>
0501 <a href="{% url 'pcs:tag_compose' tag_type='r' %}" class="{% if active_nav.pcs_reco_tags %}nav-active{% endif %}">Reco Tags</a>
0502 <a href="{% url 'pcs:tag_compose' tag_type='k' %}" class="{% if active_nav.pcs_background_tags %}nav-active{% endif %}">Background Tags</a>
0503 <a href="{% url 'pcs:datasets_compose' %}" class="{% if active_nav.pcs_datasets %}nav-active{% endif %}">Datasets</a>
0504 <a href="{% url 'pcs:prod_configs_compose' %}" class="{% if active_nav.pcs_configs %}nav-active{% endif %}">Prod Configs</a>
0505 <a href="{% url 'pcs:prod_task_compose' %}?tab=tasks" class="{% if active_nav.pcs_tasks %}nav-active{% endif %}">Prod Tasks</a>
0506 </div>
0507 </div>
0508 <div class="dropdown">
0509 <button class="dropbtn{% if active_nav.campaigns %} nav-active{% endif %}">Campaigns</button>
0510 <div class="dropdown-content">
0511 <a href="{% url 'pcs:pcs_catalog' %}" class="{% if active_nav.campaigns %}nav-active{% endif %}">Campaign Catalog</a>
0512 <a href="{% url 'pcs:pcs_catalog' %}?lifecycle=current&view=progress">Campaign Progress</a>
0513 <a href="{% url 'pcs:pcs_physics_configs' %}">Physics configs by campaign</a>
0514 <a href="{% url 'pcs:pcs_campaign_plan' %}">Campaign Plan</a>
0515 <a href="{% url 'ai:ai_narratives' %}">Campaign Narratives</a>
0516 <a href="{% url 'monitor_app:ai_content_list' %}?subject_type=campaign_task,campaign">Campaign Assessments</a>
0517 </div>
0518 </div>
0519 <div class="dropdown">
0520 <button class="dropbtn{% if active_nav.ai_content %} nav-active{% endif %}">AI</button>
0521 <div class="dropdown-content">
0522 <a href="{% url 'monitor_app:ai_content_list' %}" class="{% if active_nav.ai_content %}nav-active{% endif %}">Assessments</a>
0523 <a href="{% url 'ai:ai_proposals' %}">Proposals</a>
0524 </div>
0525 </div>
0526 <strong class="nav-label nav-panda-label">PanDA</strong>
0527 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0528 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0529 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0530 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0531 <a href="{% url 'monitor_app:log_list' %}?app_name=epicprod" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0532 <a href="{% url 'monitor_app:alarms_dashboard' %}" class="{% if active_nav.alarms %}nav-active{% endif %}">Alarms</a>
0533 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0534 <div class="dropdown">
0535 <button class="dropbtn{% if active_nav.sites %} nav-active{% endif %}">Sites</button>
0536 <div class="dropdown-content">
0537 <a href="{% url 'monitor_app:compute_usage' %}" class="{% if active_nav.compute_usage %}nav-active{% endif %}">Site usage</a>
0538 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">PanDA queues</a>
0539 </div>
0540 </div>
0541 <a href="{% url 'monitor_app:analysis' %}" class="nav-analysis{% if active_nav.analysis %} nav-active{% endif %}">Analysis</a>
0542 </span>
0543 <span class="nav-spacer"></span>
0544 <div class="dropdown">
0545 <button class="dropbtn{% if system_status_overall == 'error' %} nav-system-error{% endif %}{% if active_nav.system or active_nav.snapper or active_nav.canary or active_nav.about %} nav-active{% endif %}"
0546 id="system-status-nav-link"
0547 data-system-status="{{ system_status_overall|default:'unknown' }}"
0548 data-system-status-url="{% url 'monitor_app:system_status_json' %}"
0549 {% if system_status_latest_checked_at %}data-system-checked-at="{{ system_status_latest_checked_at|date:'c' }}"{% endif %}
0550 title="{{ system_status_reason|default:'' }}">System</button>
0551 <div class="dropdown-content">
0552 <a href="{% url 'monitor_app:system_status' %}"
0553 id="system-status-dropdown-link"
0554 class="{% if system_status_overall == 'error' %}nav-system-error{% endif %}{% if active_nav.system %} nav-active{% endif %}">System Status</a>
0555 <a href="{% url 'snapper_ai:snapper_report' scope='epicprod' %}"
0556 class="{% if active_nav.snapper %}nav-active{% endif %}">Snapper</a>
0557 <a href="{% url 'canary:canary_page' %}"
0558 class="{% if active_nav.canary %}nav-active{% endif %}">Canary</a>
0559 {% if user.is_authenticated and user.is_staff %}
0560 <a href="{% url 'admin:index' %}">Admin</a>
0561 {% endif %}
0562 <a href="{% url 'monitor_app:about' %}" class="{% if active_nav.about %}nav-active{% endif %}">About</a>
0563 </div>
0564 </div>
0565 <div class="nav-auth">
0566 {% if user.is_authenticated %}
0567 <a href="{% url 'monitor_app:account' %}" class="{% if active_nav.account %}nav-active{% endif %}">{{ user.username }}</a>
0568 <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>
0569 {% else %}
0570 <a href="{% url 'login' %}?next={{ request.get_full_path|urlencode }}">Login</a>
0571 {% endif %}
0572 </div>
0573 </nav>
0574 </div>
0575 <main>
0576 {% timezone "America/New_York" %}
0577 <div class="main-time-info">
0578 {% if is_tunnel %}
0579 <a href="https://pandaserver02.sdcc.bnl.gov/swf-monitor{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0580 target="_blank" rel="noopener">Go to page at pandaserver02</a> ·
0581 {% else %}
0582 <a href="{{ external_face_base_url }}/prod{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0583 target="_blank" rel="noopener">Go to page at {{ external_face_base_url|cut:"https://" }}</a> ·
0584 {% endif %}
0585 Built at {% now "Ymd H:i:s" %} {% now "T" %}
0586 </div>
0587 {% endtimezone %}
0588 {% if messages %}
0589 {% for message in messages %}
0590 <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show w-100 mb-0 rounded-0" role="alert"
0591 style="background-color: #e9ecef; border: none; border-bottom: 1px solid #dee2e6; padding: 20px;">
0592 {% if 'safe' in message.tags %}
0593 {{ message|safe }}
0594 {% else %}
0595 {{ message }}
0596 {% endif %}
0597 <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
0598 </div>
0599 {% endfor %}
0600 {% endif %}
0601 {% block content %}{% endblock %}
0602 </main>
0603 <script>
0604 function setNavMode(mode) {
0605 localStorage.setItem('navMode', mode);
0606 applyNavMode(mode);
0607 }
0608 function applyNavMode(mode) {
0609 if (mode !== 'testbed') mode = 'production';
0610 document.documentElement.setAttribute('data-nav-mode', mode);
0611 const ft = document.getElementById('flip-testbed');
0612 const fp = document.getElementById('flip-production');
0613 if (ft) ft.classList.toggle('nav-active', mode === 'testbed');
0614 if (fp) fp.classList.toggle('nav-active', mode === 'production');
0615 }
0616 (function() {
0617 var mode = document.documentElement.getAttribute('data-nav-mode')
0618 || 'production';
0619 applyNavMode(mode);
0620 })();
0621 (function() {
0622 const staleAfterSeconds = 15 * 60;
0623 const dropdown = document.getElementById('system-status-dropdown');
0624 const link = document.getElementById('system-status-nav-link');
0625 if (!link) return;
0626 const dropdownLink = document.getElementById('system-status-dropdown-link');
0627 const statusUrl = link.getAttribute('data-system-status-url');
0628 function refreshSystemNav() {
0629 const status = link.getAttribute('data-system-status') || 'unknown';
0630 const checkedAt = link.getAttribute('data-system-checked-at');
0631 let stale = false;
0632 if (checkedAt) {
0633 const ts = Date.parse(checkedAt);
0634 if (!Number.isNaN(ts)) {
0635 stale = (Date.now() - ts) / 1000 > staleAfterSeconds;
0636 }
0637 }
0638 const showError = status === 'error' || stale;
0639 link.classList.toggle('nav-system-error', showError);
0640 if (dropdownLink) dropdownLink.classList.toggle('nav-system-error', showError);
0641 if (stale) {
0642 link.title = 'System status is stale by more than 15 minutes.';
0643 }
0644 }
0645 function pollSystemStatus() {
0646 if (!statusUrl || !window.fetch) return;
0647 fetch(statusUrl, {
0648 cache: 'no-store',
0649 credentials: 'same-origin',
0650 headers: {'Accept': 'application/json'}
0651 }).then(function(resp) {
0652 if (!resp.ok) throw new Error('HTTP ' + resp.status);
0653 return resp.json();
0654 }).then(function(data) {
0655 link.setAttribute('data-system-status', data.overall_status || 'unknown');
0656 if (data.latest_checked_at) {
0657 link.setAttribute('data-system-checked-at', data.latest_checked_at);
0658 }
0659 link.title = data.overall_reason || '';
0660 refreshSystemNav();
0661 }).catch(function() {
0662 refreshSystemNav();
0663 });
0664 }
0665 refreshSystemNav();
0666 pollSystemStatus();
0667 if (dropdown) {
0668 dropdown.addEventListener('mouseenter', pollSystemStatus);
0669 dropdown.addEventListener('focusin', pollSystemStatus);
0670 }
0671 setInterval(refreshSystemNav, 10000);
0672 setInterval(pollSystemStatus, 60000);
0673 })();
0674 </script>
0675 <script>
0676 /* Fit an in-page document editor to the visible window: if the
0677 document overflows its panel, grow the panel to use nearly the
0678 full viewport height. The app-wide utility for editing large
0679 documents — call swfFitEditor(el) when opening an editor, or give
0680 the textarea the swf-fit-editor class to fit on focus. */
0681 window.swfFitEditor = function (el) {
0682 if (!el) return;
0683 const margin = 24;
0684 const avail = window.innerHeight - el.getBoundingClientRect().top - margin;
0685 if (el.scrollHeight > el.clientHeight && avail > el.clientHeight) {
0686 el.style.height = Math.max(avail, 240) + 'px';
0687 }
0688 };
0689 document.addEventListener('focusin', function (e) {
0690 if (e.target.matches && e.target.matches('textarea.swf-fit-editor')) {
0691 window.swfFitEditor(e.target);
0692 }
0693 });
0694
0695 /* The app-wide relative-time ticker: give any element
0696 data-relative-time="<ISO timestamp>" and its text renders and
0697 ticks as 'just now' / 'Ns ago' / 'Nm ago' / 'Nh ago' / 'Nd ago'. */
0698 window.swfFormatElapsed = function (seconds) {
0699 if (seconds < 5) return 'just now';
0700 if (seconds < 60) return seconds + 's ago';
0701 const minutes = Math.floor(seconds / 60);
0702 if (minutes < 60) return minutes + 'm ago';
0703 const hours = Math.floor(minutes / 60);
0704 if (hours < 48) return hours + 'h ago';
0705 return Math.floor(hours / 24) + 'd ago';
0706 };
0707 (function () {
0708 function tick() {
0709 const now = Date.now();
0710 document.querySelectorAll('[data-relative-time]').forEach(function (el) {
0711 const ts = Date.parse(el.getAttribute('data-relative-time'));
0712 if (Number.isNaN(ts)) return;
0713 el.textContent = window.swfFormatElapsed(
0714 Math.max(0, Math.floor((now - ts) / 1000)));
0715 });
0716 }
0717 tick();
0718 setInterval(tick, 1000);
0719 })();
0720 // Latching table headers latch beneath the sticky nav bar; expose
0721 // its measured height to the CSS above.
0722 (function () {
0723 function setNavHeightVar() {
0724 const nav = document.querySelector('.sticky-header');
0725 document.documentElement.style.setProperty(
0726 '--swf-nav-height', (nav ? nav.offsetHeight : 0) + 'px');
0727 }
0728 setNavHeightVar();
0729 window.addEventListener('resize', setNavHeightVar);
0730 })();
0731 /* Adaptive house tables. Narrow tables retain window scrolling and
0732 the nav-offset header latch. Only a table whose rendered width
0733 exceeds its available width gets a fitted two-axis scroll frame,
0734 a frame-top header latch, and two pinned leading columns. */
0735 (function () {
0736 const tableSelector =
0737 'table.dataTable, table.swf-sortable, table.swf-latch-head';
0738 let tableFrameUpdatePending = false;
0739 let tableFrameFitPending = false;
0740
0741 function ensureTableFrame(table) {
0742 if (table.parentElement &&
0743 table.parentElement.classList.contains('swf-table-frame')) {
0744 return table.parentElement;
0745 }
0746 const frame = document.createElement('div');
0747 frame.className = 'swf-table-frame';
0748 table.parentNode.insertBefore(frame, table);
0749 frame.appendChild(table);
0750 return frame;
0751 }
0752
0753 function requestedPinnedColumns(table) {
0754 const value = table.getAttribute('data-swf-pin-columns');
0755 if (value === null) return 2;
0756 const parsed = Number.parseInt(value, 10);
0757 return Number.isFinite(parsed) ? Math.max(0, Math.min(2, parsed)) : 2;
0758 }
0759
0760 function fitTableFrame(frame) {
0761 if (!frame.classList.contains('swf-wide-table-scroll')) return;
0762 const nav = document.querySelector('.sticky-header');
0763 const navHeight = nav ? nav.offsetHeight : 0;
0764 const top = Math.max(frame.getBoundingClientRect().top, navHeight);
0765 const fullAvailable = Math.max(120, window.innerHeight - navHeight - 16);
0766 const available = Math.max(
0767 Math.min(240, fullAvailable), window.innerHeight - top - 16);
0768 frame.style.setProperty('--swf-table-max-height', available + 'px');
0769 }
0770
0771 function updateTableFrame(table) {
0772 if (!table.matches(tableSelector) || !table.parentNode) return;
0773 const frame = ensureTableFrame(table);
0774 const availableWidth = frame.getBoundingClientRect().width;
0775 const isWide = table.scrollWidth > availableWidth + 1;
0776
0777 frame.classList.toggle('swf-wide-table-scroll', isWide);
0778 table.classList.toggle('swf-wide-table', isWide);
0779 if (!isWide) {
0780 table.removeAttribute('data-swf-active-pin-columns');
0781 table.style.removeProperty('--swf-first-column-width');
0782 frame.style.removeProperty('--swf-table-max-height');
0783 return;
0784 }
0785
0786 const headerCells = table.querySelectorAll('thead tr:first-child > th');
0787 const pinned = Math.min(requestedPinnedColumns(table), headerCells.length);
0788 table.setAttribute('data-swf-active-pin-columns', String(pinned));
0789 if (pinned > 1) {
0790 table.style.setProperty(
0791 '--swf-first-column-width',
0792 headerCells[0].getBoundingClientRect().width + 'px');
0793 } else {
0794 table.style.removeProperty('--swf-first-column-width');
0795 }
0796 fitTableFrame(frame);
0797 }
0798
0799 function updateAllTableFrames() {
0800 tableFrameUpdatePending = false;
0801 document.querySelectorAll(tableSelector).forEach(updateTableFrame);
0802 }
0803
0804 function scheduleTableFrameUpdate() {
0805 if (tableFrameUpdatePending) return;
0806 tableFrameUpdatePending = true;
0807 window.requestAnimationFrame(updateAllTableFrames);
0808 }
0809
0810 function scheduleTableFrameFit() {
0811 if (tableFrameFitPending) return;
0812 tableFrameFitPending = true;
0813 window.requestAnimationFrame(function () {
0814 tableFrameFitPending = false;
0815 document.querySelectorAll(
0816 '.swf-table-frame.swf-wide-table-scroll'
0817 ).forEach(fitTableFrame);
0818 });
0819 }
0820
0821 window.swfUpdateTableFrames = scheduleTableFrameUpdate;
0822 $(function () {
0823 // House sortable tables get chrome-free DataTables sorting before
0824 // the adaptive frame measures their rendered column widths.
0825 $('table.swf-sortable').DataTable({
0826 paging: false, searching: false, info: false, order: []
0827 });
0828 scheduleTableFrameUpdate();
0829 });
0830 $(document).on('draw.dt column-sizing.dt', scheduleTableFrameUpdate);
0831 window.addEventListener('resize', scheduleTableFrameUpdate);
0832 window.addEventListener('scroll', scheduleTableFrameFit, {passive: true});
0833 })();
0834 </script>
0835 </body>
0836 </html>