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