Back to home page

EIC code displayed by LXR

 
 

    


Warning, file /swf-monitor/src/templates/base.html was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

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:trials_list' %}" class="{% if active_nav.pcs_trials %}nav-active{% endif %}">Prod trials</a>
0682             </div>
0683         </div>
0684         <div class="dropdown">
0685             <button class="dropbtn{% if active_nav.campaigns %} nav-active{% endif %}">Campaigns</button>
0686             <div class="dropdown-content">
0687                 <a href="{% url 'pcs:pcs_catalog' %}" class="{% if active_nav.campaigns %}nav-active{% endif %}">Campaign Catalog</a>
0688                 <a href="{% url 'pcs:pcs_catalog' %}?lifecycle=current&amp;view=progress">Campaign Progress</a>
0689                 <a href="{% url 'pcs:pcs_physics_configs' %}">Physics configs by campaign</a>
0690                 <a href="{% url 'pcs:pcs_campaign_plan' %}">Campaign Plan</a>
0691                 <a href="{% url 'ai:ai_narratives' %}">Campaign Narratives</a>
0692                 <a href="{% url 'monitor_app:ai_content_list' %}?subject_type=campaign_task,campaign">Campaign Assessments</a>
0693             </div>
0694         </div>
0695         <div class="dropdown">
0696             <button class="dropbtn{% if active_nav.data %} nav-active{% endif %}">Data</button>
0697             <div class="dropdown-content">
0698                 <a href="{% url 'pcs:find_data' %}" class="{% if active_nav.find_data %}nav-active{% endif %}">Find data</a>
0699                 <a href="{% url 'pcs:evgen_inputs' %}" class="{% if active_nav.pcs_evgen_inputs %}nav-active{% endif %}">EVGEN Inputs</a>
0700                 <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>
0701                 <a href="{% url 'pcs:storage_listings' listing='ghosts' %}" class="{% if active_nav.pcs_storage %}nav-active{% endif %}">Storage exceptions</a>
0702                 <a href="{% url 'pcs:request_size_plot' %}" class="{% if active_nav.pcs_request_sizes %}nav-active{% endif %}">Request size plot</a>
0703                 <a href="{% url 'pcs:stash_page' %}">Failover stash</a>
0704                 <a href="{% url 'pcs:validation_v1_index' %}">Validation API</a>
0705             </div>
0706         </div>
0707         <div class="dropdown">
0708             <button class="dropbtn{% if active_nav.ai_content %} nav-active{% endif %}">AI</button>
0709             <div class="dropdown-content">
0710                 <a href="{% url 'monitor_app:ai_content_list' %}" class="{% if active_nav.ai_content %}nav-active{% endif %}">Campaign Assessments</a>
0711                 <a href="{% url 'ai:ai_proposals' %}">AI Proposals</a>
0712                 <a href="{% url 'ai:ai_narratives' %}">Campaign Narratives</a>
0713                 <a href="{{ request.META.SCRIPT_NAME }}/mcp/" title="Connect an AI assistant to this monitor's MCP tools">MCP</a>
0714             </div>
0715         </div>
0716         <strong class="nav-label nav-panda-label">PanDA</strong>
0717         <div class="dropdown">
0718             <button class="dropbtn{% if active_nav.panda_activity or active_nav.panda_tasks or active_nav.panda_jobs %} nav-active{% endif %}">Activity</button>
0719             <div class="dropdown-content">
0720                 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity summary</a>
0721                 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0722                 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0723                 <a href="{% url 'monitor_app:panda_front' %}">Pressure front</a>
0724             </div>
0725         </div>
0726         <div class="dropdown">
0727             <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>
0728             <div class="dropdown-content">
0729                 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Error summary</a>
0730                 <a href="{% url 'monitor_app:panda_segfaults' %}" class="{% if active_nav.panda_segfaults %}nav-active{% endif %}">Segfault catalog</a>
0731                 <a href="{% url 'monitor_app:panda_segfault_findings' %}">Segfault findings</a>
0732                 <a href="{% url 'snapper_ai:snapper_focus' scope='epicprod' focus_slug='errors' %}">Snapper error view</a>
0733                 <a href="{% url 'monitor_app:alarms_dashboard' %}" class="{% if active_nav.alarms %}nav-active{% endif %}">Alarms</a>
0734                 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Job diagnostics</a>
0735             </div>
0736         </div>
0737         <div class="dropdown">
0738             <button class="dropbtn{% if active_nav.sites %} nav-active{% endif %}">Sites</button>
0739             <div class="dropdown-content">
0740                 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">PanDA queues</a>
0741                 <a href="{% url 'monitor_app:compute_usage' %}" class="{% if active_nav.compute_usage %}nav-active{% endif %}">Site compute usage</a>
0742                 <a href="{% url 'snapper_ai:snapper_focus' scope='epicprod' focus_slug='site' %}">Snapper site view</a>
0743                 <a href="{% url 'canary:probes_page' %}" class="{% if active_nav.canary %}nav-active{% endif %}">Canary probes</a>
0744                 <a href="{% url 'monitor_app:panda_node_guard' %}" class="{% if active_nav.panda_node_guard %}nav-active{% endif %}">Node guard</a>
0745                 <a href="{% url 'monitor_app:panda_storage_doors' %}" class="{% if active_nav.panda_storage_doors %}nav-active{% endif %}">Storage doors</a>
0746             </div>
0747         </div>
0748         <a href="{% url 'monitor_app:analysis' %}" class="nav-analysis{% if active_nav.analysis %} nav-active{% endif %}">Analysis</a>
0749         <a href="{% url 'monitor_app:log_list' %}?app_name=epicprod" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0750         </span>
0751         <span class="nav-spacer"></span>
0752         <div class="dropdown" id="nav-system">
0753             <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 %}"
0754                     id="system-status-nav-link"
0755                     data-system-status="{{ system_status_overall|default:'unknown' }}"
0756                     data-system-status-url="{% url 'monitor_app:system_status_json' %}"
0757                     {% if system_status_latest_checked_at %}data-system-checked-at="{{ system_status_latest_checked_at|date:'c' }}"{% endif %}
0758                     title="{{ system_status_reason|default:'' }}">System</button>
0759             <div class="dropdown-content">
0760                 <a href="{% url 'monitor_app:system_status' %}"
0761                    id="system-status-dropdown-link"
0762                    class="{% if system_status_overall == 'error' %}nav-system-error{% endif %}{% if active_nav.system %} nav-active{% endif %}">System Status</a>
0763                 <a href="{% url 'snapper_ai:snapper_report' scope='epicprod' %}"
0764                    class="{% if active_nav.snapper %}nav-active{% endif %}">Snapper report</a>
0765                 <a href="{% url 'snapper_ai:snapper_focus' scope='epicprod' focus_slug='platform' %}">Platform status</a>
0766                 {% if teamcomms_url %}
0767                     <a href="{{ teamcomms_url }}">TeamComms</a>
0768                 {% endif %}
0769                 {% if may_administer_users %}
0770                     <a href="{% url 'monitor_app:user_admin' %}"
0771                        class="{% if active_nav.user_admin %}nav-active{% endif %}">User admin</a>
0772                 {% endif %}
0773                 {% if user.is_authenticated and user.is_staff %}
0774                     <a href="{% url 'admin:index' %}">Admin</a>
0775                 {% endif %}
0776                 <a href="{{ request.META.SCRIPT_NAME }}/mcp/" title="Connect an AI assistant to this monitor's MCP tools">MCP</a>
0777                 <a href="{% url 'monitor_app:about' %}" class="{% if active_nav.about %}nav-active{% endif %}">About</a>
0778             </div>
0779         </div>
0780         <div class="nav-auth">
0781             {% if user.is_authenticated %}
0782                 <a href="{% url 'monitor_app:account' %}{% if user_view %}?user_view=1{% endif %}" class="{% if active_nav.account %}nav-active{% endif %}">{{ user.username }}</a>
0783                 <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>
0784             {% else %}
0785                 <a href="{% url 'login' %}?next={{ request.get_full_path|urlencode }}">Login</a>
0786             {% endif %}
0787         </div>
0788     </nav>
0789     </div>
0790     <main>
0791         {% timezone "America/New_York" %}
0792         <div class="main-time-info">
0793             {% if is_tunnel %}
0794             <a href="https://pandaserver02.sdcc.bnl.gov/swf-monitor{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0795                target="_blank" rel="noopener">Go to page at pandaserver02</a> &middot;
0796             {% else %}
0797             <a href="{{ external_face_base_url }}/prod{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0798                target="_blank" rel="noopener">Go to page at {{ external_face_base_url|cut:"https://" }}</a> &middot;
0799             {% endif %}
0800             Built at {% now "Ymd H:i:s" %} {% now "T" %}
0801         </div>
0802         {% endtimezone %}
0803         {% if messages %}
0804             {% for message in messages %}
0805                 <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show w-100 mb-0 rounded-0" role="alert"
0806                      style="background-color: #e9ecef; border: none; border-bottom: 1px solid #dee2e6; padding: 20px;">
0807                     {% if 'safe' in message.tags %}
0808                         {{ message|safe }}
0809                     {% else %}
0810                         {{ message }}
0811                     {% endif %}
0812                     <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
0813                 </div>
0814             {% endfor %}
0815         {% endif %}
0816         {% block content %}{% endblock %}
0817     </main>
0818 <script>
0819 function setNavMode(mode) {
0820     localStorage.setItem('navMode', mode);
0821     applyNavMode(mode);
0822 }
0823 function applyNavMode(mode) {
0824     if (mode !== 'testbed') mode = 'production';
0825     document.documentElement.setAttribute('data-nav-mode', mode);
0826     const ft = document.getElementById('flip-testbed');
0827     const fp = document.getElementById('flip-production');
0828     if (ft) ft.classList.toggle('nav-active', mode === 'testbed');
0829     if (fp) fp.classList.toggle('nav-active', mode === 'production');
0830 }
0831 (function() {
0832     var mode = document.documentElement.getAttribute('data-nav-mode')
0833             || 'production';
0834     applyNavMode(mode);
0835 })();
0836 (function() {
0837     const staleAfterSeconds = 15 * 60;
0838     const dropdown = document.getElementById('system-status-dropdown');
0839     const link = document.getElementById('system-status-nav-link');
0840     if (!link) return;
0841     const dropdownLink = document.getElementById('system-status-dropdown-link');
0842     const statusUrl = link.getAttribute('data-system-status-url');
0843     function refreshSystemNav() {
0844         const status = link.getAttribute('data-system-status') || 'unknown';
0845         const checkedAt = link.getAttribute('data-system-checked-at');
0846         let stale = false;
0847         if (checkedAt) {
0848             const ts = Date.parse(checkedAt);
0849             if (!Number.isNaN(ts)) {
0850                 stale = (Date.now() - ts) / 1000 > staleAfterSeconds;
0851             }
0852         }
0853         const showError = status === 'error' || stale;
0854         link.classList.toggle('nav-system-error', showError);
0855         if (dropdownLink) dropdownLink.classList.toggle('nav-system-error', showError);
0856         if (stale) {
0857             link.title = 'System status is stale by more than 15 minutes.';
0858         }
0859     }
0860     function pollSystemStatus() {
0861         if (!statusUrl || !window.fetch) return;
0862         if (!window.swfPollGate.attended()) return;
0863         fetch(statusUrl, {
0864             cache: 'no-store',
0865             credentials: 'same-origin',
0866             headers: {'Accept': 'application/json'}
0867         }).then(function(resp) {
0868             if (!resp.ok) throw new Error('HTTP ' + resp.status);
0869             return resp.json();
0870         }).then(function(data) {
0871             link.setAttribute('data-system-status', data.overall_status || 'unknown');
0872             if (data.latest_checked_at) {
0873                 link.setAttribute('data-system-checked-at', data.latest_checked_at);
0874             }
0875             link.title = data.overall_reason || '';
0876             refreshSystemNav();
0877         }).catch(function() {
0878             refreshSystemNav();
0879         });
0880     }
0881     refreshSystemNav();
0882     pollSystemStatus();
0883     if (dropdown) {
0884         dropdown.addEventListener('mouseenter', pollSystemStatus);
0885         dropdown.addEventListener('focusin', pollSystemStatus);
0886     }
0887     window.swfPollGate.onReturn(pollSystemStatus);
0888     setInterval(refreshSystemNav, 10000);
0889     setInterval(pollSystemStatus, 60000);
0890 })();
0891 </script>
0892     <script>
0893       /* Fit an in-page document editor to the visible window: if the
0894          document overflows its panel, grow the panel to use nearly the
0895          full viewport height. The app-wide utility for editing large
0896          documents — call swfFitEditor(el) when opening an editor, or give
0897          the textarea the swf-fit-editor class to fit on focus. */
0898       window.swfFitEditor = function (el) {
0899         if (!el) return;
0900         const margin = 24;
0901         const avail = window.innerHeight - el.getBoundingClientRect().top - margin;
0902         if (el.scrollHeight > el.clientHeight && avail > el.clientHeight) {
0903           el.style.height = Math.max(avail, 240) + 'px';
0904         }
0905       };
0906       document.addEventListener('focusin', function (e) {
0907         if (e.target.matches && e.target.matches('textarea.swf-fit-editor')) {
0908           window.swfFitEditor(e.target);
0909         }
0910       });
0911 
0912       /* The app-wide relative-time ticker: give any element
0913          data-relative-time="<ISO timestamp>" and its text renders and
0914          ticks as 'just now' / 'Ns ago' / 'Nm ago' / 'Nh ago' / 'Nd ago'.
0915          Optional data-relative-time-interval="<seconds>" slows that
0916          element's redraw to every N seconds (default 1). */
0917       window.swfFormatElapsed = function (seconds) {
0918         if (seconds < 5) return 'just now';
0919         if (seconds < 60) return seconds + 's ago';
0920         const minutes = Math.floor(seconds / 60);
0921         if (minutes < 60) return minutes + 'm ago';
0922         const hours = Math.floor(minutes / 60);
0923         if (hours < 48) return hours + 'h ago';
0924         return Math.floor(hours / 24) + 'd ago';
0925       };
0926       (function () {
0927         let n = 0;
0928         function tick() {
0929           const now = Date.now();
0930           document.querySelectorAll('[data-relative-time]').forEach(function (el) {
0931             const every = parseInt(
0932               el.getAttribute('data-relative-time-interval'), 10) || 1;
0933             if (n % every !== 0) return;
0934             const ts = Date.parse(el.getAttribute('data-relative-time'));
0935             if (Number.isNaN(ts)) return;
0936             el.textContent = window.swfFormatElapsed(
0937               Math.max(0, Math.floor((now - ts) / 1000)));
0938           });
0939           n += 1;
0940         }
0941         tick();
0942         setInterval(tick, 1000);
0943       })();
0944       /* Update-button click feedback: an element with class
0945          swf-update-btn shows 'Updating...' while its navigation is in
0946          flight. The label restores on pageshow (a back/forward-cache
0947          return) and by a 20 s timeout (an aborted navigation), so it
0948          can never stick. */
0949       (function () {
0950         function restore() {
0951           document.querySelectorAll('.swf-update-btn[data-updating]').forEach(function (el) {
0952             el.textContent = el.getAttribute('data-updating');
0953             el.removeAttribute('data-updating');
0954           });
0955         }
0956         document.addEventListener('click', function (ev) {
0957           const el = ev.target.closest ? ev.target.closest('.swf-update-btn') : null;
0958           if (!el || el.hasAttribute('data-updating')) return;
0959           el.setAttribute('data-updating', el.textContent);
0960           el.textContent = 'Updating...';
0961           setTimeout(restore, 20000);
0962         });
0963         window.addEventListener('pageshow', restore);
0964       })();
0965       // Latching table headers latch beneath the sticky nav bar; expose
0966       // its measured height to the CSS above.
0967       (function () {
0968         function setNavHeightVar() {
0969           const nav = document.querySelector('.sticky-header');
0970           document.documentElement.style.setProperty(
0971             '--swf-nav-height', (nav ? nav.offsetHeight : 0) + 'px');
0972         }
0973         setNavHeightVar();
0974         window.addEventListener('resize', setNavHeightVar);
0975       })();
0976       /* Adaptive house tables. Narrow tables retain window scrolling and
0977          the nav-offset header latch. Only a table whose rendered width
0978          exceeds its available width gets a fitted two-axis scroll frame,
0979          a frame-top header latch, and two pinned leading columns. */
0980       (function () {
0981         const tableSelector =
0982           'table.dataTable, table.swf-sortable, table.swf-latch-head';
0983         let tableFrameUpdatePending = false;
0984         let tableFrameFitPending = false;
0985 
0986         function ensureTableFrame(table) {
0987           if (table.parentElement &&
0988               table.parentElement.classList.contains('swf-table-frame')) {
0989             return table.parentElement;
0990           }
0991           const frame = document.createElement('div');
0992           frame.className = 'swf-table-frame';
0993           table.parentNode.insertBefore(frame, table);
0994           frame.appendChild(table);
0995           return frame;
0996         }
0997 
0998         function requestedPinnedColumns(table) {
0999           const value = table.getAttribute('data-swf-pin-columns');
1000           if (value === null) return 2;
1001           const parsed = Number.parseInt(value, 10);
1002           return Number.isFinite(parsed) ? Math.max(0, Math.min(2, parsed)) : 2;
1003         }
1004 
1005         function fitTableFrame(frame) {
1006           if (!frame.classList.contains('swf-wide-table-scroll')) return;
1007           const nav = document.querySelector('.sticky-header');
1008           const navHeight = nav ? nav.offsetHeight : 0;
1009           // The region is sized to the whole height under the nav, wherever
1010           // the frame sits, so the page keeps exactly (frame top - nav) to
1011           // scroll: the matter above scrolls away until the header seats
1012           // beneath the nav, where it latches. Until then the frame lets
1013           // the wheel through (no vertical scrolling of its own), so
1014           // scrolling anywhere moves the page first and the table after.
1015           const fullAvailable = Math.max(120, window.innerHeight - navHeight - 16);
1016           frame.style.setProperty('--swf-table-max-height', fullAvailable + 'px');
1017           const seated = frame.getBoundingClientRect().top <= navHeight + 1;
1018           frame.style.overflowY = seated ? 'auto' : 'hidden';
1019         }
1020 
1021         function updateTableFrame(table) {
1022           if (!table.matches(tableSelector) || !table.parentNode) return;
1023           const frame = ensureTableFrame(table);
1024           const availableWidth = frame.getBoundingClientRect().width;
1025           const isWide = table.scrollWidth > availableWidth + 1;
1026 
1027           frame.classList.toggle('swf-wide-table-scroll', isWide);
1028           table.classList.toggle('swf-wide-table', isWide);
1029           if (!isWide) {
1030             table.removeAttribute('data-swf-active-pin-columns');
1031             table.style.removeProperty('--swf-first-column-width');
1032             frame.style.removeProperty('--swf-table-max-height');
1033             return;
1034           }
1035 
1036           const columnHeaderRow =
1037             table.querySelector('thead tr.swf-latch-column-row')
1038             || table.querySelector('thead tr:first-child');
1039           const headerCells = columnHeaderRow
1040             ? columnHeaderRow.querySelectorAll(':scope > th') : [];
1041           const pinned = Math.min(requestedPinnedColumns(table), headerCells.length);
1042           table.setAttribute('data-swf-active-pin-columns', String(pinned));
1043           if (pinned > 1) {
1044             table.style.setProperty(
1045               '--swf-first-column-width',
1046               headerCells[0].getBoundingClientRect().width + 'px');
1047           } else {
1048             table.style.removeProperty('--swf-first-column-width');
1049           }
1050           fitTableFrame(frame);
1051         }
1052 
1053         function updateAllTableFrames() {
1054           tableFrameUpdatePending = false;
1055           document.querySelectorAll(tableSelector).forEach(updateTableFrame);
1056         }
1057 
1058         function scheduleTableFrameUpdate() {
1059           if (tableFrameUpdatePending) return;
1060           tableFrameUpdatePending = true;
1061           window.requestAnimationFrame(updateAllTableFrames);
1062         }
1063 
1064         function scheduleTableFrameFit() {
1065           if (tableFrameFitPending) return;
1066           tableFrameFitPending = true;
1067           window.requestAnimationFrame(function () {
1068             tableFrameFitPending = false;
1069             document.querySelectorAll(
1070               '.swf-table-frame.swf-wide-table-scroll'
1071             ).forEach(fitTableFrame);
1072           });
1073         }
1074 
1075         window.swfUpdateTableFrames = scheduleTableFrameUpdate;
1076         $(function () {
1077           // House sortable tables get chrome-free DataTables sorting before
1078           // the adaptive frame measures their rendered column widths.
1079           // data-swf-order="N,desc" opts a table into a default sort.
1080           $('table.swf-sortable').each(function () {
1081             const spec = this.getAttribute('data-swf-order');
1082             let order = [];
1083             if (spec) {
1084               const parts = spec.split(',');
1085               order = [[parseInt(parts[0], 10) || 0,
1086                         parts[1] === 'desc' ? 'desc' : 'asc']];
1087             }
1088             $(this).DataTable({
1089               paging: false, searching: false, info: false, order: order
1090             });
1091           });
1092           scheduleTableFrameUpdate();
1093         });
1094         $(document).on('draw.dt column-sizing.dt', scheduleTableFrameUpdate);
1095         window.addEventListener('resize', scheduleTableFrameUpdate);
1096         window.addEventListener('scroll', scheduleTableFrameFit, {passive: true});
1097       })();
1098     </script>
1099 </body>
1100 </html>