Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 10:47:38

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