Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-07-21 08:46:28

0001 {% load static tz %}
0002 <!DOCTYPE html>
0003 <html lang="en">
0004 <head>
0005     <meta charset="UTF-8">
0006     <meta name="viewport" content="width=device-width, initial-scale=1.0">
0007     <title>{% block title %}SWF Monitor{% endblock %}</title>
0008     <script>
0009     (function() {
0010         var mode = 'production';
0011         try {
0012             mode = localStorage.getItem('navMode') || 'production';
0013         } catch (e) {}
0014         if (mode !== 'testbed') mode = 'production';
0015         document.documentElement.setAttribute('data-nav-mode', mode);
0016     })();
0017     </script>
0018     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
0019     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
0020     <link rel="stylesheet" href="{% static 'css/style.css' %}">
0021     <link rel="stylesheet" href="{% static 'css/state-colors.css' %}">
0022     <link rel="stylesheet" href="https://cdn.datatables.net/1.13.8/css/dataTables.bootstrap5.min.css">
0023     <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
0024     <script src="https://cdn.datatables.net/1.13.8/js/jquery.dataTables.min.js"></script>
0025     <script src="https://cdn.datatables.net/1.13.8/js/dataTables.bootstrap5.min.js"></script>
0026     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css">
0027     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
0028     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
0029     <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
0030     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
0031     <style>
0032       body {
0033         margin: 0;
0034         padding: 0;
0035       }
0036       main {
0037         padding: 0.65rem 1rem 1rem;
0038         position: relative;
0039       }
0040       main > .container,
0041       main > .container-fluid {
0042         margin-top: 0 !important;
0043       }
0044       main > h1,
0045       main > h2,
0046       main > .container > h1,
0047       main > .container > h2,
0048       main > .container-fluid > h1,
0049       main > .container-fluid > h2,
0050       main > .container > div.mb-3 > h1,
0051       main > .container > div.mb-3 > h2,
0052       main > .container > div.mb-4 > div > h1,
0053       main > .container > div.mb-4 > div > h2,
0054       main > .container > .d-flex h1,
0055       main > .container > .d-flex h2,
0056       main > .container-fluid > div.mb-3 > h1,
0057       main > .container-fluid > div.mb-3 > h2,
0058       main > .container-fluid > div.mb-4 > div > h1,
0059       main > .container-fluid > div.mb-4 > div > h2,
0060       main > .container-fluid > .d-flex h1,
0061       main > .container-fluid > .d-flex h2 {
0062         font-size: 1.7rem;
0063         line-height: 1.2;
0064         margin-top: 0 !important;
0065       }
0066       main > h1,
0067       main > h2,
0068       main > .container > h1,
0069       main > .container > h2,
0070       main > .container-fluid > h1,
0071       main > .container-fluid > h2,
0072       main > .container > div.mb-3 > h1,
0073       main > .container > div.mb-3 > h2,
0074       main > .container > div.mb-4 > div > h1,
0075       main > .container > div.mb-4 > div > h2,
0076       main > .container-fluid > div.mb-3 > h1,
0077       main > .container-fluid > div.mb-3 > h2,
0078       main > .container-fluid > div.mb-4 > div > h1,
0079       main > .container-fluid > div.mb-4 > div > h2 {
0080         margin-bottom: 0.2rem !important;
0081       }
0082       main > .container > .d-flex h1,
0083       main > .container > .d-flex h2,
0084       main > .container-fluid > .d-flex h1,
0085       main > .container-fluid > .d-flex h2 {
0086         margin-bottom: 0 !important;
0087       }
0088       /* Button roles (see CLAUDE.md "Button roles" — the AI-facing table):
0089          constructive create/save/new/import = btn-primary; object verbs
0090          edit/copy on existing items = btn-dark-green; consequential commits
0091          submit/lock/publish/approve = btn-success; suggestion-bar Apply =
0092          btn-warning btn-apply; destructive delete = btn-danger; neutral
0093          cancel/utility = btn-secondary; btn-outline-* reserved for stateful
0094          toggles where fill signals on/off. All btn-sm. */
0095       .btn.btn-dark-green {
0096         background-color: #166534; border-color: #166534; color: #fff;
0097       }
0098       .btn.btn-dark-green:hover, .btn.btn-dark-green:focus {
0099         background-color: #14532d; border-color: #14532d; color: #fff;
0100       }
0101       .btn.btn-dark-green:disabled, .btn.btn-dark-green.disabled {
0102         background-color: #6c757d; border-color: #6c757d; color: #e9ecef;
0103       }
0104       #js-error-overlay {
0105         position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
0106         z-index: 100000; max-width: 720px; width: calc(100vw - 2rem);
0107         background: #c9302c; color: #fff; padding: 1.25rem 1.5rem;
0108         border: 3px solid #7a1a18; border-radius: .5rem;
0109         box-shadow: 0 10px 30px rgba(0,0,0,.4);
0110         font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
0111         display: none;
0112       }
0113       #js-error-overlay h3 { margin: 0 0 .5rem; font-size: 1.1rem; }
0114       #js-error-overlay pre {
0115         background: rgba(0,0,0,.25); color: #fff; padding: .5rem .75rem;
0116         border-radius: .25rem; margin: .5rem 0 0; font-size: .8rem;
0117         white-space: pre-wrap; word-break: break-word; max-height: 40vh; overflow: auto;
0118       }
0119       #js-error-overlay .close-btn {
0120         position: absolute; top: .5rem; right: .75rem;
0121         background: transparent; border: none; color: #fff; font-size: 1.5rem;
0122         cursor: pointer; line-height: 1;
0123       }
0124       nav {
0125         display: flex;
0126         align-items: center;
0127         gap: .8em;
0128       }
0129       .sticky-header {
0130         position: sticky;
0131         top: 0;
0132         z-index: 1030;
0133       }
0134       .nav-spacer {
0135         flex: 1 1 auto;
0136       }
0137       .nav-auth {
0138         display: flex;
0139         gap: .8em;
0140       }
0141       .main-time-info {
0142         color: #667085;
0143         float: right;
0144         font-size: .75rem;
0145         margin: -.25rem 0 .25rem 1rem;
0146         white-space: nowrap;
0147       }
0148       nav a {
0149         color: #ADD8E6;
0150       }
0151       nav a.nav-active {
0152         text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0153       }
0154       /* Dropdown Button */
0155       .dropbtn {
0156         background-color: inherit;
0157         color: #8fc7e8;
0158         text-decoration: none;
0159         padding: 0;
0160         font-size: inherit;
0161         border: none;
0162         cursor: pointer;
0163       }
0164       .dropbtn.nav-active {
0165         text-shadow: 0 0 .65px currentColor, 0 0 .65px currentColor;
0166       }
0167 
0168       /* The container <div> - needed to position the dropdown content */
0169       .dropdown {
0170         position: relative;
0171         display: inline-block;
0172         margin-right: .35em;
0173       }
0174 
0175       /* Dropdown Content (Hidden by Default) */
0176       .dropdown-content {
0177         display: none;
0178         position: absolute;
0179         background-color: #f9f9f9;
0180         min-width: 160px;
0181         box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
0182         z-index: 1;
0183       }
0184 
0185       /* Links inside the dropdown */
0186       .dropdown-content a {
0187         color: black;
0188         padding: 12px 16px;
0189         text-decoration: none;
0190         display: block;
0191       }
0192       .dropdown-content a.nav-active {
0193         background-color: #e9ecef;
0194       }
0195 
0196       /* Change color of dropdown links on hover */
0197       .dropdown-content a:hover {background-color: #f1f1f1}
0198 
0199       /* Show the dropdown menu on hover */
0200       .dropdown:hover .dropdown-content {
0201         display: block;
0202       }
0203       /* Nav mode flipper */
0204       .nav-mode { display: none; }
0205       .nav-mode.active { display: contents; }
0206       html[data-nav-mode="production"] .nav-production,
0207       html[data-nav-mode="testbed"] .nav-testbed { display: contents; }
0208       #mode-label {
0209         border-right: 1px solid rgba(255,255,255,.2);
0210         color: #fff;
0211         font-weight: 700;
0212         padding-right: 12px;
0213         margin-right: 4px;
0214       }
0215       #mode-label .mode-title { display: none; }
0216       html[data-nav-mode="production"] #mode-label .mode-title-production,
0217       html[data-nav-mode="testbed"] #mode-label .mode-title-testbed { display: inline; }
0218       .nav-label {
0219         color: #fff;
0220         font-weight: 700;
0221       }
0222       .nav-panda-label {
0223         margin-left: 1.5em;
0224       }
0225       .nav-analysis {
0226         margin-left: 1.5em;
0227       }
0228       .nav-system-error { color: #ff8174 !important; font-weight: 700; }
0229       /* One-click copy icon for IDs — see swf_fmt.copy_btn template tag */
0230       .copy-btn {
0231         background: none; border: none; color: #6c757d; cursor: pointer;
0232         padding: 0 .25rem; font-size: .85em; line-height: 1; vertical-align: baseline;
0233       }
0234       .copy-btn:hover { color: #0d6efd; }
0235       .copy-btn.copied { color: #198754; }
0236       /* AI presence — the universal convention for material attributable to
0237          the AI (proposals, recommendation filters, AI-authored blocks):
0238          dark purple ink on light purple ground. Tune the system-wide AI
0239          look here and nowhere else. */
0240       :root {
0241         --ai-fg: #5b21b6;
0242         --ai-bg: #f8f4fe;
0243         /* Lavender ground always wears a 1px border in the ink color. */
0244         --ai-border: var(--ai-fg);
0245       }
0246       .ai-attr {
0247         color: var(--ai-fg);
0248         background-color: var(--ai-bg);
0249         border: 1px solid var(--ai-border);
0250         border-radius: .5em;
0251         padding: 0 .45em;
0252         font-weight: 600;
0253       }
0254       /* Purple ink always sits on lavender ground — the universal pairing. */
0255       .ai-attr-text {
0256         color: var(--ai-fg); background-color: var(--ai-bg);
0257         border: 1px solid var(--ai-border);
0258         border-radius: .35em; padding: 0 .3em; font-weight: 600;
0259       }
0260       /* Lavender fill for AI-owned containers (home-page boxes, panels). */
0261       .ai-fill {
0262         background-color: var(--ai-bg);
0263         border: 1px solid var(--ai-border) !important;
0264       }
0265       button.ai-attr:hover, a.ai-attr:hover {
0266         background-color: var(--ai-fg); color: #fff;
0267         border-color: var(--ai-fg);
0268       }
0269     </style>
0270 </head>
0271 <body>
0272     <div id="js-error-overlay" role="alert">
0273         <button class="close-btn" onclick="document.getElementById('js-error-overlay').style.display='none';">&times;</button>
0274         <h3>JavaScript error</h3>
0275         <div id="js-error-msg"></div>
0276         <pre id="js-error-detail"></pre>
0277     </div>
0278     <script>
0279     (function() {
0280         function show(title, detail) {
0281             var box = document.getElementById('js-error-overlay');
0282             var msg = document.getElementById('js-error-msg');
0283             var det = document.getElementById('js-error-detail');
0284             if (!box || !msg || !det) return;
0285             msg.textContent = title || 'Error';
0286             det.textContent = detail || '';
0287             box.style.display = 'block';
0288         }
0289         window.addEventListener('error', function(ev) {
0290             var err = ev.error;
0291             var loc = (ev.filename || '') + (ev.lineno ? ':' + ev.lineno + ':' + (ev.colno || 0) : '');
0292             var stack = err && err.stack ? err.stack : '';
0293             show(ev.message || 'Script error', (loc ? loc + '\n\n' : '') + stack);
0294         });
0295         window.addEventListener('unhandledrejection', function(ev) {
0296             var r = ev.reason;
0297             var msg = (r && r.message) ? r.message : String(r);
0298             var stack = (r && r.stack) ? r.stack : '';
0299             show('Unhandled promise rejection: ' + msg, stack);
0300         });
0301     })();
0302     // One-click copy-to-clipboard handler for .copy-btn buttons rendered by
0303     // the copy_btn template tag (swf_fmt). Swaps the icon to a checkmark for
0304     // 1s on success so the user sees the click landed.
0305     document.addEventListener('click', function(ev) {
0306         var btn = ev.target.closest('.copy-btn');
0307         if (!btn) return;
0308         ev.preventDefault();
0309         var text = btn.getAttribute('data-copy') || '';
0310         if (!text || !navigator.clipboard) return;
0311         navigator.clipboard.writeText(text).then(function() {
0312             var icon = btn.querySelector('i');
0313             if (!icon) return;
0314             var prev = icon.className;
0315             icon.className = 'bi bi-clipboard-check';
0316             btn.classList.add('copied');
0317             setTimeout(function() {
0318                 icon.className = prev;
0319                 btn.classList.remove('copied');
0320             }, 1000);
0321         });
0322     });
0323     // Near-zero-latency tooltips via Bootstrap 5 for elements with title=.
0324     // Scoped to state-filled cells and the copy button so we don't spin up
0325     // a tooltip for every cell on every page. show delay 50ms, hide 0ms.
0326     document.addEventListener('DOMContentLoaded', function() {
0327         if (!window.bootstrap || !bootstrap.Tooltip) return;
0328         function init(root) {
0329             root.querySelectorAll(
0330                 'td[title]:not([data-bs-tooltip-init]), .copy-btn[title]:not([data-bs-tooltip-init])'
0331             ).forEach(function(el) {
0332                 if (!el.getAttribute('title')) return;
0333                 el.setAttribute('data-bs-tooltip-init', '1');
0334                 new bootstrap.Tooltip(el, {delay: {show: 50, hide: 0}});
0335             });
0336         }
0337         init(document);
0338         // DataTables redraws replace rows; re-init after each draw.
0339         if (window.jQuery) {
0340             jQuery(document).on('draw.dt', function(_, settings) {
0341                 init(settings.nTable || document);
0342             });
0343         }
0344     });
0345     </script>
0346     <div class="sticky-header">
0347     <nav>
0348         <div class="dropdown" id="mode-flipper">
0349             <button class="dropbtn" id="mode-label">
0350                 <span class="mode-title mode-title-production">epicprod</span>
0351                 <span class="mode-title mode-title-testbed">ePIC Testbed</span>
0352             </button>
0353             <div class="dropdown-content">
0354                 <a href="{% url 'monitor_app:prod_hub' %}" id="flip-production" onclick="setNavMode('production');">epicprod - ePIC Production</a>
0355                 <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>
0356             </div>
0357         </div>
0358 
0359         {# ── Testbed mode ── #}
0360         <span class="nav-mode nav-testbed">
0361         <div class="dropdown" id="system-status-dropdown">
0362             <button class="dropbtn{% if active_nav.workflows %} nav-active{% endif %}">Workflows</button>
0363             <div class="dropdown-content">
0364                 <a href="{% url 'monitor_app:workflows_home' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Views</a>
0365                 <a href="{% url 'monitor_app:workflow_definitions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow definitions</a>
0366                 <a href="{% url 'monitor_app:workflow_executions_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Workflow executions</a>
0367                 <a href="{% url 'monitor_app:namespaces_list' %}" class="{% if active_nav.workflows %}nav-active{% endif %}">Namespaces</a>
0368             </div>
0369         </div>
0370         <div class="dropdown">
0371             <button class="dropbtn{% if active_nav.files %} nav-active{% endif %}">Files</button>
0372             <div class="dropdown-content">
0373                 <a href="{% url 'monitor_app:stf_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Files</a>
0374                 <a href="{% url 'monitor_app:fastmon_files_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">STF Samples</a>
0375                 <a href="{% url 'monitor_app:tf_slices_list' %}" class="{% if active_nav.files %}nav-active{% endif %}">TF Slices</a>
0376             </div>
0377         </div>
0378         <a href="{% url 'monitor_app:workflow_agents_list' %}" class="{% if active_nav.agents %}nav-active{% endif %}">Agents</a>
0379         <a href="{% url 'monitor_app:subscribers_list' %}" class="{% if active_nav.subscribers %}nav-active{% endif %}">Subscribers</a>
0380         <a href="{% url 'monitor_app:workflow_messages' %}" class="{% if active_nav.messages %}nav-active{% endif %}">Messages</a>
0381         <a href="{% url 'monitor_app:log_summary' %}" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0382         <a href="{% url 'monitor_app:database_tables_list' %}" class="{% if active_nav.database %}nav-active{% endif %}">Database</a>
0383         <a href="{% url 'monitor_app:persistent_state' %}" class="{% if active_nav.state %}nav-active{% endif %}">State</a>
0384         <div class="dropdown">
0385             <button class="dropbtn{% if active_nav.panda_rucio %} nav-active{% endif %}">PanDA/Rucio</button>
0386             <div class="dropdown-content">
0387                 <a href="{% url 'monitor_app:panda_hub' %}" class="{% if active_nav.panda_hub %}nav-active{% endif %}">PanDA/Rucio Hub</a>
0388                 <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0389                 <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0390                 <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0391                 <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0392                 <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0393                 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">EIC PanDA Queues</a>
0394                 <a href="{% url 'monitor_app:panda_database_tables_list' %}" class="{% if active_nav.panda_database %}nav-active{% endif %}">PanDA Database</a>
0395                 <a href="{% url 'monitor_app:idds_database_tables_list' %}" class="{% if active_nav.idds_database %}nav-active{% endif %}">iDDS Database</a>
0396                 <a href="{% url 'monitor_app:rucio_endpoints_list' %}" class="{% if active_nav.rucio_endpoints %}nav-active{% endif %}">Rucio Endpoints</a>
0397             </div>
0398         </div>
0399         </span>
0400 
0401         {# ── Production mode ── #}
0402         {# SYNC OBLIGATION: swf-remote renders its own copy of this production nav (swf-remote src/templates/base.html) for its natively served pages. #}
0403         {# Any change to this nav block must be mirrored there — notify/hand off to the devcloud (ec2dev) side with every nav change. #}
0404         <span class="nav-mode nav-production">
0405         <div class="dropdown">
0406             <button class="dropbtn{% if active_nav.requests %} nav-active{% endif %}">Requests</button>
0407             <div class="dropdown-content">
0408                 <a href="https://docs.google.com/forms/d/e/1FAIpQLScDqiEaHayAcwBDGAWa4W6k-6yUFzS-XiWuhLolpy64mLk5FA/viewform" target="_blank" rel="noopener">Google request form</a>
0409                 <a href="{% url 'pcs:pcs_request_composer' %}">Request Production</a>
0410                 <a href="{% url 'pcs:questionnaires_list' %}">Request list</a>
0411             </div>
0412         </div>
0413         <div class="dropdown">
0414             <button class="dropbtn{% if active_nav.pcs %} nav-active{% endif %}">PCS</button>
0415             <div class="dropdown-content">
0416                 <a href="{% url 'pcs:pcs_hub' %}" class="{% if active_nav.pcs_hub %}nav-active{% endif %}">PCS Hub</a>
0417                 <a href="{% url 'pcs:physics_categories_list' %}" class="{% if active_nav.pcs_categories %}nav-active{% endif %}">Physics Categories</a>
0418                 <a href="{% url 'pcs:tag_compose' tag_type='p' %}" class="{% if active_nav.pcs_physics_tags %}nav-active{% endif %}">Physics Tags</a>
0419                 <a href="{% url 'pcs:tag_compose' tag_type='e' %}" class="{% if active_nav.pcs_evgen_tags %}nav-active{% endif %}">EvGen Tags</a>
0420                 <a href="{% url 'pcs:tag_compose' tag_type='s' %}" class="{% if active_nav.pcs_simu_tags %}nav-active{% endif %}">Simu Tags</a>
0421                 <a href="{% url 'pcs:tag_compose' tag_type='r' %}" class="{% if active_nav.pcs_reco_tags %}nav-active{% endif %}">Reco Tags</a>
0422                 <a href="{% url 'pcs:tag_compose' tag_type='k' %}" class="{% if active_nav.pcs_background_tags %}nav-active{% endif %}">Background Tags</a>
0423                 <a href="{% url 'pcs:datasets_compose' %}" class="{% if active_nav.pcs_datasets %}nav-active{% endif %}">Datasets</a>
0424                 <a href="{% url 'pcs:prod_configs_compose' %}" class="{% if active_nav.pcs_configs %}nav-active{% endif %}">Prod Configs</a>
0425                 <a href="{% url 'pcs:prod_task_compose' %}?tab=tasks" class="{% if active_nav.pcs_tasks %}nav-active{% endif %}">Prod Tasks</a>
0426             </div>
0427         </div>
0428         <div class="dropdown">
0429             <button class="dropbtn{% if active_nav.campaigns %} nav-active{% endif %}">Campaigns</button>
0430             <div class="dropdown-content">
0431                 <a href="{% url 'pcs:pcs_catalog' %}" class="{% if active_nav.campaigns %}nav-active{% endif %}">Campaign Catalog</a>
0432                 <a href="{% url 'pcs:pcs_catalog' %}?lifecycle=current&amp;view=progress">Campaign Progress</a>
0433                 <a href="{% url 'pcs:pcs_physics_configs' %}">Physics configs by campaign</a>
0434                 <a href="{% url 'ai:ai_narratives' %}">Campaign Narratives</a>
0435                 <a href="{% url 'monitor_app:ai_content_list' %}?subject_type=campaign_task,campaign">Campaign Assessments</a>
0436             </div>
0437         </div>
0438         <div class="dropdown">
0439             <button class="dropbtn{% if active_nav.ai_content %} nav-active{% endif %}">AI</button>
0440             <div class="dropdown-content">
0441                 <a href="{% url 'monitor_app:ai_content_list' %}" class="{% if active_nav.ai_content %}nav-active{% endif %}">Assessments</a>
0442                 <a href="{% url 'ai:ai_proposals' %}">Proposals</a>
0443             </div>
0444         </div>
0445         <strong class="nav-label nav-panda-label">PanDA</strong>
0446         <a href="{% url 'monitor_app:panda_activity' %}" class="{% if active_nav.panda_activity %}nav-active{% endif %}">Activity</a>
0447         <a href="{% url 'monitor_app:panda_tasks_list' %}" class="{% if active_nav.panda_tasks %}nav-active{% endif %}">Tasks</a>
0448         <a href="{% url 'monitor_app:panda_jobs_list' %}" class="{% if active_nav.panda_jobs %}nav-active{% endif %}">Jobs</a>
0449         <a href="{% url 'monitor_app:panda_errors_list' %}" class="{% if active_nav.panda_errors %}nav-active{% endif %}">Errors</a>
0450         <a href="{% url 'monitor_app:log_list' %}?app_name=epicprod" class="{% if active_nav.logs %}nav-active{% endif %}">Logs</a>
0451         <a href="{% url 'monitor_app:alarms_dashboard' %}" class="{% if active_nav.alarms %}nav-active{% endif %}">Alarms</a>
0452         <a href="{% url 'monitor_app:panda_diagnostics_list' %}" class="{% if active_nav.panda_diagnostics %}nav-active{% endif %}">Diagnostics</a>
0453         <div class="dropdown">
0454             <button class="dropbtn{% if active_nav.sites %} nav-active{% endif %}">Sites</button>
0455             <div class="dropdown-content">
0456                 <a href="{% url 'monitor_app:compute_usage' %}" class="{% if active_nav.compute_usage %}nav-active{% endif %}">Site usage</a>
0457                 <a href="{% url 'monitor_app:epic_queues_list' %}" class="{% if active_nav.panda_queues %}nav-active{% endif %}">PanDA queues</a>
0458             </div>
0459         </div>
0460         <a href="{% url 'monitor_app:analysis' %}" class="nav-analysis{% if active_nav.analysis %} nav-active{% endif %}">Analysis</a>
0461         </span>
0462         <span class="nav-spacer"></span>
0463         <div class="dropdown">
0464             <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 %}"
0465                     id="system-status-nav-link"
0466                     data-system-status="{{ system_status_overall|default:'unknown' }}"
0467                     data-system-status-url="{% url 'monitor_app:system_status_json' %}"
0468                     {% if system_status_latest_checked_at %}data-system-checked-at="{{ system_status_latest_checked_at|date:'c' }}"{% endif %}
0469                     title="{{ system_status_reason|default:'' }}">System</button>
0470             <div class="dropdown-content">
0471                 <a href="{% url 'monitor_app:system_status' %}"
0472                    id="system-status-dropdown-link"
0473                    class="{% if system_status_overall == 'error' %}nav-system-error{% endif %}{% if active_nav.system %} nav-active{% endif %}">System Status</a>
0474                 <a href="{% url 'monitor_app:snapper_report' scope='epicprod' %}"
0475                    class="{% if active_nav.snapper %}nav-active{% endif %}">Snapper</a>
0476                 {% if user.is_authenticated and user.is_staff %}
0477                     <a href="{% url 'admin:index' %}">Admin</a>
0478                 {% endif %}
0479                 <a href="{% url 'monitor_app:about' %}" class="{% if active_nav.about %}nav-active{% endif %}">About</a>
0480             </div>
0481         </div>
0482         <div class="nav-auth">
0483             {% if user.is_authenticated %}
0484                 <a href="{% url 'monitor_app:account' %}" class="{% if active_nav.account %}nav-active{% endif %}">{{ user.username }}</a>
0485                 <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>
0486             {% else %}
0487                 <a href="{% url 'login' %}?next={{ request.get_full_path|urlencode }}">Login</a>
0488             {% endif %}
0489         </div>
0490     </nav>
0491     </div>
0492     <main>
0493         {% timezone "America/New_York" %}
0494         <div class="main-time-info">
0495             {% if is_tunnel %}
0496             <a href="https://pandaserver02.sdcc.bnl.gov/swf-monitor{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0497                target="_blank" rel="noopener">Go to page at pandaserver02</a> &middot;
0498             {% else %}
0499             <a href="{{ external_face_base_url }}/prod{{ request.path_info }}{% if request.META.QUERY_STRING %}?{{ request.META.QUERY_STRING }}{% endif %}"
0500                target="_blank" rel="noopener">Go to page at {{ external_face_base_url|cut:"https://" }}</a> &middot;
0501             {% endif %}
0502             Built at {% now "Ymd H:i:s" %} {% now "T" %}
0503         </div>
0504         {% endtimezone %}
0505         {% if messages %}
0506             {% for message in messages %}
0507                 <div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show w-100 mb-0 rounded-0" role="alert"
0508                      style="background-color: #e9ecef; border: none; border-bottom: 1px solid #dee2e6; padding: 20px;">
0509                     {% if 'safe' in message.tags %}
0510                         {{ message|safe }}
0511                     {% else %}
0512                         {{ message }}
0513                     {% endif %}
0514                     <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
0515                 </div>
0516             {% endfor %}
0517         {% endif %}
0518         {% block content %}{% endblock %}
0519     </main>
0520 <script>
0521 function setNavMode(mode) {
0522     localStorage.setItem('navMode', mode);
0523     applyNavMode(mode);
0524 }
0525 function applyNavMode(mode) {
0526     if (mode !== 'testbed') mode = 'production';
0527     document.documentElement.setAttribute('data-nav-mode', mode);
0528     const ft = document.getElementById('flip-testbed');
0529     const fp = document.getElementById('flip-production');
0530     if (ft) ft.classList.toggle('nav-active', mode === 'testbed');
0531     if (fp) fp.classList.toggle('nav-active', mode === 'production');
0532 }
0533 (function() {
0534     var mode = localStorage.getItem('navMode') || 'production';
0535     applyNavMode(mode);
0536 })();
0537 (function() {
0538     const staleAfterSeconds = 15 * 60;
0539     const dropdown = document.getElementById('system-status-dropdown');
0540     const link = document.getElementById('system-status-nav-link');
0541     if (!link) return;
0542     const dropdownLink = document.getElementById('system-status-dropdown-link');
0543     const statusUrl = link.getAttribute('data-system-status-url');
0544     function refreshSystemNav() {
0545         const status = link.getAttribute('data-system-status') || 'unknown';
0546         const checkedAt = link.getAttribute('data-system-checked-at');
0547         let stale = false;
0548         if (checkedAt) {
0549             const ts = Date.parse(checkedAt);
0550             if (!Number.isNaN(ts)) {
0551                 stale = (Date.now() - ts) / 1000 > staleAfterSeconds;
0552             }
0553         }
0554         const showError = status === 'error' || stale;
0555         link.classList.toggle('nav-system-error', showError);
0556         if (dropdownLink) dropdownLink.classList.toggle('nav-system-error', showError);
0557         if (stale) {
0558             link.title = 'System status is stale by more than 15 minutes.';
0559         }
0560     }
0561     function pollSystemStatus() {
0562         if (!statusUrl || !window.fetch) return;
0563         fetch(statusUrl, {
0564             cache: 'no-store',
0565             credentials: 'same-origin',
0566             headers: {'Accept': 'application/json'}
0567         }).then(function(resp) {
0568             if (!resp.ok) throw new Error('HTTP ' + resp.status);
0569             return resp.json();
0570         }).then(function(data) {
0571             link.setAttribute('data-system-status', data.overall_status || 'unknown');
0572             if (data.latest_checked_at) {
0573                 link.setAttribute('data-system-checked-at', data.latest_checked_at);
0574             }
0575             link.title = data.overall_reason || '';
0576             refreshSystemNav();
0577         }).catch(function() {
0578             refreshSystemNav();
0579         });
0580     }
0581     refreshSystemNav();
0582     pollSystemStatus();
0583     if (dropdown) {
0584         dropdown.addEventListener('mouseenter', pollSystemStatus);
0585         dropdown.addEventListener('focusin', pollSystemStatus);
0586     }
0587     setInterval(refreshSystemNav, 10000);
0588     setInterval(pollSystemStatus, 60000);
0589 })();
0590 </script>
0591     <script>
0592       /* Fit an in-page document editor to the visible window: if the
0593          document overflows its panel, grow the panel to use nearly the
0594          full viewport height. The app-wide utility for editing large
0595          documents — call swfFitEditor(el) when opening an editor, or give
0596          the textarea the swf-fit-editor class to fit on focus. */
0597       window.swfFitEditor = function (el) {
0598         if (!el) return;
0599         const margin = 24;
0600         const avail = window.innerHeight - el.getBoundingClientRect().top - margin;
0601         if (el.scrollHeight > el.clientHeight && avail > el.clientHeight) {
0602           el.style.height = Math.max(avail, 240) + 'px';
0603         }
0604       };
0605       document.addEventListener('focusin', function (e) {
0606         if (e.target.matches && e.target.matches('textarea.swf-fit-editor')) {
0607           window.swfFitEditor(e.target);
0608         }
0609       });
0610 
0611       /* The app-wide relative-time ticker: give any element
0612          data-relative-time="<ISO timestamp>" and its text renders and
0613          ticks as 'just now' / 'Ns ago' / 'Nm ago' / 'Nh ago' / 'Nd ago'. */
0614       window.swfFormatElapsed = function (seconds) {
0615         if (seconds < 5) return 'just now';
0616         if (seconds < 60) return seconds + 's ago';
0617         const minutes = Math.floor(seconds / 60);
0618         if (minutes < 60) return minutes + 'm ago';
0619         const hours = Math.floor(minutes / 60);
0620         if (hours < 48) return hours + 'h ago';
0621         return Math.floor(hours / 24) + 'd ago';
0622       };
0623       (function () {
0624         function tick() {
0625           const now = Date.now();
0626           document.querySelectorAll('[data-relative-time]').forEach(function (el) {
0627             const ts = Date.parse(el.getAttribute('data-relative-time'));
0628             if (Number.isNaN(ts)) return;
0629             el.textContent = window.swfFormatElapsed(
0630               Math.max(0, Math.floor((now - ts) / 1000)));
0631           });
0632         }
0633         tick();
0634         setInterval(tick, 1000);
0635       })();
0636       // House sortable tables: every <table class="swf-sortable"> gets
0637       // DataTables click-to-sort headers (the globally loaded house table
0638       // widget), without paging/search chrome. Columns are sortable by
0639       // default across the app; opting out is the special case. See
0640       // CLAUDE.md, House UI helpers.
0641       $(function () {
0642         $('table.swf-sortable').DataTable({
0643           paging: false, searching: false, info: false, order: []
0644         });
0645       });
0646     </script>
0647 </body>
0648 </html>