Back to home page

EIC code displayed by LXR

 
 

    


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';">&times;</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&amp;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> &middot;
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> &middot;
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>