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';">×</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&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> ·
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> ·
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>