/**
 * Rusjp MeshDash — Universal Header Controller v5.0
 * ==================================================
 *
 * v5 Overhaul:
 *  - Unified status strip: node info, stats, and connection status
 *    merged into ONE cohesive panel instead of three separate boxes.
 *  - Mobile-first responsive: hamburger menu, touch-friendly dropdowns
 *    that toggle on click instead of hover (no more disappearing menus).
 *  - 24-hour graph cap: history endpoint filtered to last 86400 seconds.
 *  - Single authoritative graph data array with throttled live pushes.
 *  - radioStateTuple() is the ONE canonical status mapper everywhere.
 *  - Dropdown menus use click-toggle on all devices for reliability.
 *  - Performance: requestAnimationFrame for graph draws, passive listeners.
 */
(function () {
    if (window.RusjpMeshHeader) return;
  
    const CONFIG = {
      ssePath:                '/sse',
      statusEndpoint:         '/api/status',
      localNodeFullEndpoint:  '/api/local_node/full',
      historyEndpoint:        '/api/system/connection_history',
      statsEndpoint:          '/api/stats',
      heartbeatInterval:      5 * 60 * 1000,
      pollOnConnect:          true,
      graphMaxPoints:         1000,
      graphHistorySeconds:    86400,
      links: [
        { href: '/',          icon: 'fa-home',             label: 'Home' },
        { href: '/map',       icon: 'fa-map-marked-alt',   label: 'Map' },
        {
          label: 'Comms', icon: 'fa-comments', isDropdown: true,
          items: [
            { href: '/dmes',     icon: 'fa-comment-alt', label: 'Direct Message' },
            { href: '/channels', icon: 'fa-globe',       label: 'Channels' }
          ]
        },
        { href: '/analytics', icon: 'fa-chart-line',       label: 'Analytics' },
        { href: '/sensors',   icon: 'fa-thermometer-half', label: 'IoT' },
        { href: '/tasks',     icon: 'fa-tasks',            label: 'Tasks' },
        { href: '/autoreply', icon: 'fa-reply',            label: 'Auto' },
        {
          label: 'System', icon: 'fa-server', isDropdown: true,
          items: [
            { href: '/sse-debug', icon: 'fa-bug',  label: 'Debug' },
            { href: '/settings',  icon: 'fa-cog',  label: 'Settings' }
          ]
        },
        { href: '/logout', icon: 'fa-sign-out-alt', label: 'Logout', color: '#ff6b6b' }
      ]
    };
  
    // ═══════════════════════════════════════════════════════════════
    //  STYLES
    // ═══════════════════════════════════════════════════════════════
    const STYLES = `
  :root {
    --bg:       #020912;
    --bg1:      #060e1c;
    --bg2:      #0a1628;
    --bg3:      #0d1c34;
    --bg4:      #12243f;
    --bd:       #152030;
    --bd2:      #1c3050;
    --bd3:      #263d5e;
    --txt:      #bdd4f0;
    --txt2:     #5a80a8;
    --txt3:     #2e4a68;
    --mono:     'Share Tech Mono', monospace;
    --sans:     'Exo 2', sans-serif;
    --acc:      #00c8f5;
    --acc2:     #0090c0;
    --acc-glow: rgba(0, 200, 245, 0.13);
    --acc-dim:  rgba(0, 200, 245, 0.05);
    --ok:       #00f090;
    --ok-dim:   rgba(0, 240, 144, 0.1);
    --warn:     #ffa800;
    --warn-dim: rgba(255, 168, 0, 0.1);
    --err:      #ff3050;
    --err-dim:  rgba(255, 48, 80, 0.1);
    --pur:      #b060ff;
    --pur-dim:  rgba(176, 96, 255, 0.1);
    --r:        5px;
    --header-h: 56px;
  }
  *, *::before, *::after { box-sizing: border-box; }
  .fas, .far, .fab { font-family: "Font Awesome 6 Free"; }
  
  /* ── Header Bar ── */
  #rmd-root {
    position: fixed; top: 0; left: 0; z-index: 9000;
    width: 100%; height: var(--header-h);
    background: var(--bg1);
    border-bottom: 1px solid var(--bd2);
    box-shadow: 0 4px 24px rgba(0,0,0,0.7);
    color: var(--txt); font-family: var(--sans);
    font-size: 13px; letter-spacing: 0.2px;
  }
  #rmd-root::after {
    content: ''; position: absolute; bottom: 0; left: 0; right: 0;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--acc), transparent);
    opacity: 0.4; pointer-events: none;
  }
  #rmd-spacer { display: block; width: 100%; height: var(--header-h); }
  
  .rmd-wrap {
    display: flex; align-items: center;
    gap: 12px; height: 100%; max-width: 100px;
    margin: 0 auto; padding: 0 1rem;
  }
  
  /* ── Logo ── */
  .rmd-logo {
    display: flex; align-items: center; gap: 8px; flex-shrink: 0;
    color: var(--txt); font-size: 1rem; font-weight: 700;
    text-decoration: none; white-space: nowrap; transition: opacity .15s;
  }
  .rmd-logo:hover { opacity: 0.9; }
  .rmd-logo i { color: var(--acc); font-size: 1.1rem; filter: drop-shadow(0 0 4px var(--acc-glow)); }
  .rmd-hl { color: var(--acc); }
  
  /* ── Navigation ── */
  .rmd-nav { display: flex; align-items: center; gap: 2px; margin: 0; padding: 0; list-style: none; }
  .rmd-nav-item a, .rmd-nav-item .rmd-drop-trigger {
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    min-width: 44px; padding: 5px 8px; border: 1px solid transparent;
    border-radius: var(--r); color: var(--txt2); font-size: .7rem;
    text-decoration: none; white-space: nowrap; letter-spacing: .3px;
    transition: all .15s ease; cursor: pointer; background: none;
    font-family: var(--sans);
  }
  .rmd-nav-item a i, .rmd-nav-item .rmd-drop-trigger i { font-size: .9rem; }
  .rmd-nav-item a span, .rmd-nav-item .rmd-drop-trigger span {
    font-size: .55rem; font-weight: 500; text-transform: uppercase; opacity: .9;
  }
  .rmd-nav-item a:hover, .rmd-nav-item .rmd-drop-trigger:hover {
    transform: translateY(-1px); background: var(--acc-dim);
    border-color: var(--bd3); color: var(--acc);
  }
  .rmd-nav-item a.rmd-active {
    background: var(--acc-glow); border-color: var(--bd2);
    border-bottom-color: var(--acc); color: var(--acc);
  }
  
  /* ── Dropdown ── */
  .rmd-has-drop { position: relative; }
  .rmd-drop-menu {
    position: absolute; top: calc(100% + 4px); left: 50%;
    transform: translateX(-50%); display: none;
    min-width: 170px; padding: 6px;
    background: var(--bg2); border: 1px solid var(--bd3);
    border-radius: var(--r); box-shadow: 0 12px 36px rgba(0,0,0,.85);
    z-index: 9200;
  }
  .rmd-has-drop.rmd-drop-open .rmd-drop-menu {
    display: block; animation: rmd-drop-in .18s ease forwards;
  }
  .rmd-drop-menu a {
    display: flex !important; flex-direction: row !important;
    align-items: center !important; gap: 10px !important;
    padding: 9px 12px !important; margin-bottom: 2px;
    border-radius: 3px; text-align: left;
    background: none !important; border: 1px solid transparent !important;
  }
  .rmd-drop-menu a:hover {
    background: var(--acc-dim) !important;
    border-color: var(--bd3) !important; color: var(--acc) !important;
  }
  .rmd-drop-menu a i { font-size: .85rem !important; width: 16px; color: var(--acc); }
  .rmd-drop-menu a span {
    font-size: .65rem !important; text-transform: none !important;
    opacity: 1 !important; letter-spacing: .4px;
  }
  .rmd-drop-caret {
    font-size: .45rem; margin-left: 2px; opacity: .4;
    transition: transform .2s;
  }
  .rmd-has-drop.rmd-drop-open .rmd-drop-caret {
    transform: rotate(180deg); opacity: 1; color: var(--acc);
  }
  @keyframes rmd-drop-in {
    from { opacity: 0; transform: translateX(-50%) translateY(8px); }
    to   { opacity: 1; transform: translateX(-50%) translateY(0); }
  }
  
  /* ── Divider ── */
  .rmd-div {
    flex-shrink: 0; width: 1px; height: 28px;
    margin: 0 2px; background: var(--bd2);
  }
  
  /* ── UNIFIED STATUS STRIP ── */
  .rmd-unified {
    display: flex; align-items: center; gap: 0;
    margin-left: auto; flex-shrink: 0;
    background: var(--bg2); border: 1px solid var(--bd2);
    border-radius: 6px; overflow: hidden;
    box-shadow: inset 0 1px 3px rgba(0,0,0,.3);
    cursor: pointer; transition: border-color .2s;
    height: 42px;
  }
  .rmd-unified:hover { border-color: var(--bd3); }
  
  /* Section inside unified strip */
  .rmd-u-section {
    display: flex; align-items: center; gap: 8px;
    padding: 0 10px; height: 100%;
    border-right: 1px solid var(--bd);
    font-family: var(--mono); font-size: .65rem;
  }
  .rmd-u-section:last-child { border-right: none; }
  
  /* Node identity section */
  .rmd-u-node {
    display: flex; flex-direction: column; justify-content: center;
    gap: 1px; min-width: 100px; max-width: 180px;
  }
  .rmd-u-node-id {
    color: var(--acc); font-size: .68rem; font-weight: 600;
    letter-spacing: .4px; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  .rmd-u-node-meta {
    display: flex; align-items: center; gap: 6px;
    color: var(--txt2); font-size: .58rem; white-space: nowrap;
    overflow: hidden; text-overflow: ellipsis;
  }
  .rmd-u-node-meta .rmd-bat {
    display: inline-flex; align-items: center; gap: 2px;
  }
  .rmd-u-node-meta .rmd-bat.hi  { color: var(--ok); }
  .rmd-u-node-meta .rmd-bat.mid { color: var(--warn); }
  .rmd-u-node-meta .rmd-bat.lo  { color: var(--err); }
  
  /* Stats section */
  .rmd-u-stats {
    display: grid; grid-template-columns: repeat(3, auto);
    gap: 2px 8px; padding: 4px 10px;
  }
  .rmd-u-stat {
    display: flex; align-items: center; gap: 3px;
    color: var(--txt2); font-size: .58rem; white-space: nowrap;
  }
  .rmd-u-stat i { color: var(--acc); font-size: .55rem; width: 10px; text-align: center; }
  .rmd-u-stat .n {
    color: var(--txt); font-weight: 700; min-width: 16px;
    text-align: right; font-variant-numeric: tabular-nums;
  }
  
  /* Connection section */
  .rmd-u-conn {
    display: flex; align-items: center; gap: 8px; padding: 0 10px;
  }
  .rmd-u-conn-dots {
    display: flex; flex-direction: column; gap: 4px;
  }
  .rmd-u-conn-row {
    display: flex; align-items: center; gap: 5px;
  }
  .rmd-u-dot {
    flex-shrink: 0; width: 7px; height: 7px;
    border-radius: 50%; transition: all .3s ease;
  }
  .rmd-u-dot.lg { width: 9px; height: 9px; }
  .rmd-u-dot.ok   { background: var(--ok);   box-shadow: 0 0 8px var(--ok); }
  .rmd-u-dot.err  { background: var(--err);  box-shadow: 0 0 8px var(--err); }
  .rmd-u-dot.grey { background: var(--txt3); box-shadow: 0 0 4px rgba(0,0,0,.5); }
  .rmd-u-dot.warn { background: var(--warn); box-shadow: 0 0 8px var(--warn); animation: rmd-pulse-w 1s infinite alternate; }
  .rmd-u-dot.sync { background: var(--acc);  box-shadow: 0 0 8px var(--acc); animation: rmd-pulse-s .8s infinite alternate; }
  @keyframes rmd-pulse-w { 0% { opacity:.5; transform:scale(.9); } 100% { opacity:1; transform:scale(1.1); } }
  @keyframes rmd-pulse-s { 0% { opacity:.6; transform:scale(.9); } 100% { opacity:1; transform:scale(1.1); } }
  .rmd-u-lbl {
    color: var(--txt3); font-size: .55rem; font-weight: 600;
    letter-spacing: .5px; text-transform: uppercase; width: 32px;
  }
  .rmd-u-val {
    font-size: .62rem; font-weight: 500; white-space: nowrap;
    max-width: 80px; overflow: hidden; text-overflow: ellipsis;
  }
  .rmd-u-val.ok   { color: var(--ok); }
  .rmd-u-val.warn { color: var(--warn); }
  .rmd-u-val.err  { color: var(--err); }
  .rmd-u-val.grey { color: var(--txt3); }
  
  /* Mini graph inside unified strip */
  .rmd-u-graph { padding: 0 6px; display: flex; align-items: center; }
  .rmd-mini-canvas {
    display: block; background: var(--bg);
    border: 1px solid var(--bd2); border-radius: 3px;
    box-shadow: inset 0 1px 3px rgba(0,0,0,.5);
  }
  
  /* IO dots */
  .rmd-u-io {
    display: flex; flex-direction: column; gap: 3px;
    padding: 0 8px; border-left: 1px solid var(--bd);
    cursor: pointer;
  }
  .rmd-u-io:hover { opacity: .85; }
  .rmd-u-io-row { display: flex; align-items: center; gap: 4px; }
  .rmd-io-dot {
    width: 6px; height: 6px; background: var(--txt3);
    border-radius: 50%; transition: all .05s ease;
  }
  .rmd-io-dot.tx-active { background: var(--ok);  box-shadow: 0 0 10px var(--ok); }
  .rmd-io-dot.rx-active { background: var(--err); box-shadow: 0 0 10px var(--err); }
  .rmd-u-io-lbl {
    color: var(--txt2); font-size: .5rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: .5px; line-height: 1;
  }
  
  /* ── Sync Banner ── */
  #rmd-sync-banner {
    display: none; position: fixed; top: var(--header-h); left: 0; right: 0;
    z-index: 8999; padding: 4px 0; background: var(--bg2);
    border-bottom: 1px solid var(--bd2); color: var(--acc);
    font-family: var(--mono); font-size: .72rem;
    text-align: center; letter-spacing: .4px;
  }
  #rmd-sync-banner.vis { display: block; }
  
  /* ── Mobile toggle ── */
  .rmd-mobile-btn {
    display: none; padding: 6px; background: none;
    border: 1px solid var(--bd2); border-radius: var(--r);
    color: var(--txt2); font-size: 1.1rem; cursor: pointer;
    transition: all .15s;
  }
  .rmd-mobile-btn:hover { color: var(--acc); border-color: var(--bd3); }
  #rmd-mcb { display: none; }
  .rmd-nav-wrap {
    display: flex; align-items: center; gap: 2px;
  }
  
  /* ── DIAGNOSTICS MODAL ── */
  #rmd-modal {
    display: none; position: fixed; inset: 0; z-index: 99999;
    justify-content: center; align-items: center;
    background: rgba(0,0,0,.9); backdrop-filter: blur(12px);
  }
  #rmd-modal.open { display: flex; }
  .rmd-mc {
    display: flex; flex-direction: column; width: 95%; max-width: 1200px;
    max-height: 90vh; background: var(--bg1); border: 1px solid var(--bd2);
    border-radius: 8px; overflow: hidden; box-shadow: 0 30px 80px rgba(0,0,0,.9);
  }
  .rmd-mh {
    display: flex; justify-content: space-between; align-items: center;
    flex-shrink: 0; padding: 20px 24px 16px; background: var(--bg);
    border-bottom: 1px solid var(--bd);
  }
  .rmd-mt { display: flex; align-items: center; gap: 10px; margin: 0; color: var(--txt); font-size: 1.1rem; font-weight: 700; }
  .rmd-mt i { color: var(--acc); font-size: 1.2rem; }
  .rmd-msub { margin: 4px 0 0; color: var(--txt3); font-size: .75rem; }
  .rmd-mx {
    padding: 0; background: none; border: none; color: var(--txt3);
    font-size: 1.6rem; line-height: 1; cursor: pointer; transition: color .2s;
  }
  .rmd-mx:hover { color: var(--txt); }
  .rmd-mb { flex: 1; padding: 24px; background: var(--bg1); overflow-y: auto; }
  
  /* Modal grids */
  .rmd-grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin-bottom: 20px; }
  .rmd-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
  .rmd-card { padding: 16px; background: var(--bg2); border: 1px solid var(--bd2); border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,.3); }
  .rmd-card-hdr {
    display: flex; align-items: center; gap: 6px; margin-bottom: 14px; padding-bottom: 8px;
    border-bottom: 1px solid var(--bd); color: var(--txt2); font-family: var(--mono);
    font-size: .65rem; text-transform: uppercase; letter-spacing: 1px;
  }
  .rmd-card-hdr i { color: var(--acc); font-size: .75rem; }
  .rmd-kv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
  .rmd-kv { display: flex; flex-direction: column; gap: 3px; }
  .rmd-k { color: var(--txt3); font-family: var(--mono); font-size: .6rem; text-transform: uppercase; letter-spacing: .5px; }
  .rmd-v { color: var(--txt); font-family: var(--mono); font-size: .8rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .rmd-v.grn { color: var(--ok); } .rmd-v.blu { color: var(--acc); } .rmd-v.org { color: var(--warn); }
  .rmd-v.red { color: var(--err); } .rmd-v.pur { color: var(--pur); } .rmd-v.grey { color: var(--txt3); }
  
  /* Battery bar */
  .rmd-bat-bar { height: 5px; margin-top: 5px; background: var(--bd); border-radius: 3px; overflow: hidden; }
  .rmd-bat-fill { height: 100%; border-radius: 3px; transition: width .3s, background .3s; }
  
  /* Big stats */
  .rmd-stat-big { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 8px 0; background: var(--bg3); border-radius: 4px; }
  .rmd-stat-num { color: var(--txt); font-size: 1.7rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
  .rmd-stat-lbl { margin-top: 4px; color: var(--txt2); font-family: var(--mono); font-size: .62rem; text-transform: uppercase; letter-spacing: .7px; }
  .rmd-stat-num.grn { color: var(--ok); } .rmd-stat-num.blu { color: var(--acc); } .rmd-stat-num.org { color: var(--warn); }
  
  /* Uptime cards */
  .rmd-uptime-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
  .rmd-uptime-card {
    display: flex; flex-direction: column; gap: 8px; position: relative;
    padding: 16px 16px 14px; background: var(--bg2); border: 1px solid var(--bd2);
    border-radius: 6px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,.3);
  }
  .rmd-uptime-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px; }
  .rmd-uptime-card.uc-session::before { background: var(--ok); }
  .rmd-uptime-card.uc-script::before  { background: var(--acc); }
  .rmd-uptime-card.uc-node::before    { background: var(--pur); }
  .rmd-uc-badge {
    position: absolute; top: 12px; right: 12px; padding: 2px 8px; border-radius: 3px;
    font-family: var(--mono); font-size: .55rem; font-weight: 600; text-transform: uppercase; letter-spacing: .8px;
  }
  .uc-session .rmd-uc-badge { background: var(--ok-dim); color: var(--ok); }
  .uc-script  .rmd-uc-badge { background: var(--acc-dim); color: var(--acc); }
  .uc-node    .rmd-uc-badge { background: var(--pur-dim); color: var(--pur); }
  .rmd-uc-val { padding-left: 10px; font-family: var(--mono); font-size: 1.35rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
  .uc-session .rmd-uc-val { color: var(--ok); }
  .uc-script  .rmd-uc-val { color: var(--acc); }
  .uc-node    .rmd-uc-val { color: var(--pur); }
  .rmd-uc-lbl { padding-left: 10px; color: var(--txt2); font-family: var(--mono); font-size: .62rem; text-transform: uppercase; letter-spacing: .7px; }
  .rmd-uc-sub { padding-left: 10px; color: var(--txt3); font-family: var(--mono); font-size: .65rem; }
  
  /* Connection rails */
  .rmd-conn-rail {
    display: flex; align-items: center; gap: 12px; margin-bottom: 10px;
    padding: 12px 16px; background: var(--bg2); border: 1px solid var(--bd2); border-radius: 6px;
  }
  .rmd-conn-rail:last-child { margin-bottom: 0; }
  .rmd-rail-dot { flex-shrink: 0; width: 11px; height: 11px; border-radius: 50%; transition: all .3s; }
  .rmd-rail-dot.ok   { background: var(--ok);  box-shadow: 0 0 10px var(--ok); }
  .rmd-rail-dot.err  { background: var(--err); box-shadow: 0 0 8px var(--err); }
  .rmd-rail-dot.grey { background: var(--txt3); }
  .rmd-rail-dot.sync { background: var(--acc); animation: rmd-pulse-s .8s infinite alternate; }
  .rmd-rail-dot.warn { background: var(--warn); box-shadow: 0 0 8px var(--warn); animation: rmd-pulse-w 1s infinite alternate; }
  .rmd-rail-layer { min-width: 60px; color: var(--txt2); font-family: var(--mono); font-size: .65rem; font-weight: 600; text-transform: uppercase; letter-spacing: .8px; }
  .rmd-rail-val { flex: 1; font-size: .85rem; font-weight: 600; }
  .rmd-rail-val.ok { color: var(--ok); } .rmd-rail-val.warn { color: var(--warn); } .rmd-rail-val.err { color: var(--err); } .rmd-rail-val.grey { color: var(--txt3); }
  .rmd-rail-sub { color: var(--txt3); font-family: var(--mono); font-size: .68rem; }
  
  /* Full graph */
  .rmd-graph-lbl {
    display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;
    color: var(--txt2); font-family: var(--mono); font-size: .65rem; text-transform: uppercase; letter-spacing: .8px;
  }
  .rmd-graph-lbl span { color: var(--txt3); font-size: .62rem; }
  .rmd-graph-box { width: 100%; height: 90px; background: var(--bg); border: 1px solid var(--bd2); border-radius: 4px; overflow: hidden; }
  .rmd-full-canvas { display: block; width: 100%; height: 100%; }
  .rmd-legend { display: flex; gap: 20px; margin-top: 12px; }
  .rmd-legend span { display: flex; align-items: center; gap: 6px; color: var(--txt2); font-size: .68rem; }
  .rmd-swatch { display: inline-block; width: 12px; height: 4px; border-radius: 2px; }
  
  /* Log modal */
  #rmd-log-modal {
    display: none; position: fixed; inset: 0; z-index: 100000;
    align-items: center; justify-content: center;
    background: rgba(0,0,0,.9); backdrop-filter: blur(8px);
  }
  #rmd-log-modal.open { display: flex; }
  .rmd-log-box { display: flex; flex-direction: column; width: 650px; max-width: 95vw; height: 450px; background: var(--bg1); border: 1px solid var(--bd2); border-radius: 8px; box-shadow: 0 20px 60px #000; }
  .rmd-log-head { display: flex; justify-content: space-between; align-items: center; padding: 14px 18px; background: var(--bg); border-bottom: 1px solid var(--bd); color: var(--txt); font-size: .9rem; font-weight: 600; }
  .rmd-log-close { padding: 0; background: none; border: none; color: var(--txt3); font-size: 1.3rem; cursor: pointer; transition: color .2s; }
  .rmd-log-close:hover { color: var(--txt); }
  .rmd-log-body { flex: 1; padding: 12px; background: var(--bg); color: var(--txt2); font-family: var(--mono); font-size: .78rem; overflow-y: auto; }
  .rmd-log-row { display: flex; gap: 12px; padding: 4px 0; border-bottom: 1px solid var(--bd); color: var(--txt3); }
  .rmd-log-ts { flex-shrink: 0; width: 65px; color: var(--txt3); }
  .rmd-log-dir { flex-shrink: 0; width: 28px; font-weight: bold; }
  .rmd-log-dir.TX { color: var(--ok); } .rmd-log-dir.RX { color: var(--err); }
  .rmd-log-msg { color: var(--txt2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  
  /* ── Responsive ── */
  @media (max-width: 1200px) {
    .rmd-u-stats { display: none !important; }
  }
  @media (max-width: 1000px) {
    .rmd-u-node { display: none !important; }
  }
  @media (max-width: 860px) {
    .rmd-mobile-btn { display: flex !important; }
    .rmd-nav-wrap {
      display: none; flex-direction: column; position: absolute;
      top: var(--header-h); left: 0; right: 0; z-index: 9100;
      padding: 16px; gap: 12px;
      background: var(--bg1); border-bottom: 1px solid var(--bd2);
      box-shadow: 0 12px 32px rgba(0,0,0,.6);
    }
    #rmd-mcb:checked ~ .rmd-nav-wrap { display: flex; }
    .rmd-nav { flex-wrap: wrap; justify-content: center; gap: 4px; }
    .rmd-div:not(.rmd-div-status) { display: none; }
    .rmd-has-drop .rmd-drop-menu {
      position: static; transform: none; width: 100%;
      box-shadow: none; border: 1px solid var(--bd2);
      animation: none;
    }
    .rmd-grid-3, .rmd-uptime-row { grid-template-columns: 1fr 1fr; }
  }
  @media (max-width: 600px) {
    .rmd-grid-3, .rmd-grid-2, .rmd-uptime-row { grid-template-columns: 1fr; }
    .rmd-wrap { padding: 0 .75rem; }
    .rmd-u-graph { display: none !important; }
  }
  `;
  
    // ═══════════════════════════════════════════════════════════════
    //  UTILITIES
    // ═══════════════════════════════════════════════════════════════
    function getVal(obj, ...keys) {
      for (const key of keys) {
        if (obj == null) continue;
        if (obj[key] !== undefined && obj[key] !== null) return obj[key];
        for (const n of ['deviceMetrics', 'user', 'position', 'localConfig'])
          if (obj[n] && obj[n][key] !== undefined && obj[n][key] !== null) return obj[n][key];
      }
      return null;
    }
  
    function cleanEnum(val) {
      if (!val) return null;
      const s = String(val);
      if (s === 'Unknown' || s === 'None' || s === 'null' || s === '0') return null;
      const dot = s.lastIndexOf('.');
      return dot !== -1 ? s.slice(dot + 1) : s;
    }
  
    function fmtUptime(sec, opts = {}) {
      sec = Math.floor(sec || 0);
      if (!sec) return opts.fallback || null;
      const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600),
            m = Math.floor((sec % 3600) / 60), s = sec % 60;
      if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
      if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
      if (m > 0) return m + 'm ' + s + 's';
      return s + 's';
    }
  
    function fmtUptimeLong(sec, startEpoch) {
      const primary = fmtUptime(sec);
      if (!primary) return { primary: '-', secondary: '' };
      let secondary = '';
      if (startEpoch) {
        secondary = 'since ' + new Date(startEpoch * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
      }
      return { primary: primary, secondary: secondary };
    }
  
    function fmtNum(n, dec, sfx) {
      if (n == null || isNaN(n)) return null;
      return Number(n).toFixed(dec || 0) + (sfx || '');
    }
  
    function batCls(pct) {
      if (pct == null) return 'grn';
      return pct > 60 ? 'grn' : pct > 20 ? 'org' : 'red';
    }
  
    function batStripCls(pct) {
      if (pct == null) return '';
      return pct > 60 ? 'hi' : pct > 20 ? 'mid' : 'lo';
    }
  
    function radioStateTuple(status) {
      var sl = (status || '').toLowerCase();
      if (sl === 'connected')
        return { dotCls: 'ok', valCls: 'ok', label: 'Connected', graphVal: 0.9 };
      if (sl.startsWith('reconnecting') || sl.includes('degrad') || sl.includes('search'))
        return { dotCls: 'warn', valCls: 'warn', label: status || 'Reconnecting', graphVal: 0.5 };
      if (sl === 'disconnected')
        return { dotCls: 'err', valCls: 'err', label: 'Disconnected', graphVal: 0.1 };
      if (sl === 'initializing' || sl.includes('init') || sl === '')
        return { dotCls: 'grey', valCls: 'grey', label: 'Initializing', graphVal: 0.1 };
      return { dotCls: 'warn', valCls: 'warn', label: status || 'Waiting', graphVal: 0.5 };
    }
  
    // ═══════════════════════════════════════════════════════════════
    //  GRAPH CLASSES
    // ═══════════════════════════════════════════════════════════════
    class MiniGraph {
      constructor(canvas, w, h) {
        this.c = canvas; this.w = w; this.h = h;
        this.tailSize = 60;
        if (!canvas) return;
        var dpr = window.devicePixelRatio || 1;
        canvas.width = w * dpr; canvas.height = h * dpr;
        canvas.style.width = w + 'px'; canvas.style.height = h + 'px';
        this.ctx = canvas.getContext('2d');
        if (this.ctx) this.ctx.scale(dpr, dpr);
      }
      draw(data) {
        if (!this.ctx || !this.w) return;
        var ctx = this.ctx, w = this.w, h = this.h;
        var d = data.length > this.tailSize ? data.slice(-this.tailSize) : data;
        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = '#08080f'; ctx.fillRect(0, 0, w, h);
        if (!d.length) return;
        var bw = w / d.length;
        for (var i = 0; i < d.length; i++) {
          var pct = Math.max(0, Math.min(1, isNaN(d[i]) ? 0 : d[i]));
          var bh = Math.max(2, pct * h);
          ctx.fillStyle = pct > 0.75 ? '#00f090' : pct > 0.3 ? '#ffa800' : '#ff3050';
          ctx.fillRect(i * bw, h - bh, Math.max(1, bw - 0.5), bh);
        }
      }
    }
  
    class FullGraph {
      constructor(id) {
        this.canvas = document.getElementById(id);
        this.ctx = this.canvas ? this.canvas.getContext('2d') : null;
        this._w = 0; this._h = 0;
        if (this.canvas) {
          this._ro = new ResizeObserver(function () { this._size(); }.bind(this));
          this._ro.observe(this.canvas.parentElement);
          this._size();
        }
      }
      _size() {
        if (!this.canvas) return;
        var r = this.canvas.parentElement.getBoundingClientRect();
        if (r.width < 1) return;
        var dpr = window.devicePixelRatio || 1;
        this.canvas.width = r.width * dpr;
        this.canvas.height = r.height * dpr;
        this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        this._w = r.width; this._h = r.height;
      }
      draw(data, labels) {
        if (!this.ctx || !this._w) return;
        var ctx = this.ctx, w = this._w, h = this._h;
        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = '#06060e'; ctx.fillRect(0, 0, w, h);
        if (!data.length) return;
        ctx.strokeStyle = 'rgba(255,255,255,0.025)'; ctx.lineWidth = 1;
        [0.25, 0.5, 0.75, 1].forEach(function (f) {
          ctx.beginPath(); ctx.moveTo(0, h - f * h); ctx.lineTo(w, h - f * h); ctx.stroke();
        });
        var bw = w / data.length;
        for (var i = 0; i < data.length; i++) {
          var pct = Math.max(0, Math.min(1, isNaN(data[i]) ? 0 : data[i]));
          var bh = Math.max(2, pct * h);
          var x = i * bw;
          if (pct > 0.75) {
            ctx.fillStyle = 'rgba(0,240,144,0.04)';
            ctx.fillRect(x, 0, Math.max(1, bw), h);
            ctx.fillStyle = '#00f090';
          } else if (pct > 0.3) {
            ctx.fillStyle = '#ffa800';
          } else {
            ctx.fillStyle = '#ff3050';
          }
          ctx.fillRect(x, h - bh, Math.max(1, bw - 0.5), bh);
        }
        if (labels && labels.length) {
          ctx.fillStyle = 'rgba(255,255,255,0.12)';
          ctx.font = '9px monospace'; ctx.textAlign = 'center';
          var step = Math.max(1, Math.ceil(data.length / 7));
          for (var j = 0; j < data.length; j += step) {
            if (labels[j]) ctx.fillText(labels[j], (j + 0.5) * bw, h - 3);
          }
        }
      }
      redraw(data, labels) {
        this._size();
        this.draw(data, labels);
      }
    }
  
    // ═══════════════════════════════════════════════════════════════
    //  HEADER CONTROLLER
    // ═══════════════════════════════════════════════════════════════
    class HeaderController {
      constructor() {
        this.es = null;
        this.reconnectTimer = null;
        this.heartbeatTimer = null;
        this.sessionUptimeTimer = null;
        this.wakeLock = null;
        this.miniGraph = null;
        this.fullGraph = null;
        this._graphData = [];
        this._graphLabels = [];
        this._historyLoaded = false;
        this._historyLoading = false;
        this._sessionStart = null;
        this._sessionSec = 0;
        this._lastRadioStatus = null;
        this._lastGraphPush = 0;
        this._graphPushMinInterval = 2000;
        this._rafPending = false;
        this.s = {
          nodeId: null, nodeLong: null, nodeShort: null,
          hw: null, fw: null, pioEnv: null,
          role: null, region: null, loraRegion: null,
          loraHops: null, loraTx: null, loraTxEnabled: null,
          battery: null, voltage: null, chUtil: null, airUtil: null,
          lat: null, lon: null, alt: null,
          wifiSsid: null, hasWifi: false, hasBt: false, btEnabled: false,
          nodeDbCount: null, maxCh: null, rebootCount: null,
          nodeUptime: null, scriptUptime: null, sessionUptime: 0,
          pkt: 0, msg: 0, pos: 0, tel: 0, nodes: 0, chans: 0,
          connStatus: 'Initializing', apiOnline: false,
          radioConnectedAt: null, scriptStartedAt: null
        };
      }
  
      init() {
        var self = this;
        requestAnimationFrame(function () {
          self._styles();
          self._dom();
          self._nav();
          self._initGraphs();
          self._events();
          document.addEventListener('visibilitychange', function () {
            if (document.visibilityState === 'visible') {
              self._sse();
              self._poll();
              if (!self._historyLoaded) self._loadHistory();
            }
          });
          setTimeout(function () { self._loadHistory(); }, 100);
          setTimeout(function () { self._sse(); }, 150);
          setTimeout(function () { self._poll(); }, 300);
          self._heartbeat();
          self._wakeLock();
          self._startSessionClock();
        });
      }
  
      _styles() {
        var s = document.createElement('style');
        s.textContent = STYLES;
        document.head.appendChild(s);
      }
  
      // ─── DOM CONSTRUCTION ──────────────────────────────────────
      _dom() {
        if (document.getElementById('rmd-root')) return;
  
        var navHtml = CONFIG.links.map(function (l) {
          if (l.isDropdown) {
            var items = l.items.map(function (sub) {
              return '<a href="' + sub.href + '" class="rmd-lnk">' +
                '<i class="fas ' + sub.icon + '"></i><span>' + sub.label + '</span></a>';
            }).join('');
            return '<li class="rmd-nav-item rmd-has-drop">' +
              '<div class="rmd-drop-trigger" tabindex="0">' +
                '<i class="fas ' + l.icon + '"></i>' +
                '<span>' + l.label + ' <i class="fas fa-chevron-down rmd-drop-caret"></i></span>' +
              '</div>' +
              '<div class="rmd-drop-menu">' + items + '</div></li>';
          }
          var style = l.color ? ' style="color:' + l.color + '"' : '';
          return '<li class="rmd-nav-item">' +
            '<a href="' + l.href + '" class="rmd-lnk"' + style + '>' +
              '<i class="fas ' + l.icon + '"></i><span>' + l.label + '</span></a></li>';
        }).join('');
  
        var root = document.createElement('div');
        root.id = 'rmd-root';
        root.innerHTML =
          '<div class="rmd-wrap">' +
            '<a href="/" class="rmd-logo">' +
              '<i class="fas fa-broadcast-tower"></i>' +
              '<span>Rusjp <span class="rmd-hl">MeshDash</span></span>' +
            '</a>' +
            '<input type="checkbox" id="rmd-mcb">' +
            '<button class="rmd-mobile-btn" id="rmd-hamburger"><i class="fas fa-bars"></i></button>' +
            '<div class="rmd-nav-wrap" id="rmd-nav-wrap">' +
              '<ul class="rmd-nav">' + navHtml + '</ul>' +
            '</div>' +
            '<div class="rmd-div rmd-div-status"></div>' +
            '<div class="rmd-unified" id="rmd-trigger" title="Click for system diagnostics">' +
              /* Node identity */
              '<div class="rmd-u-section">' +
                '<div class="rmd-u-node" id="rmd-u-node">' +
                  '<div class="rmd-u-node-id" id="rmd-u-nid">—</div>' +
                  '<div class="rmd-u-node-meta" id="rmd-u-nmeta">Connecting…</div>' +
                '</div>' +
              '</div>' +
              /* Stats */
              '<div class="rmd-u-section rmd-u-stats" id="rmd-u-stats">' +
                '<div class="rmd-u-stat"><i class="fas fa-sitemap"></i><span class="n" id="rs-nodes">0</span>nod</div>' +
                '<div class="rmd-u-stat"><i class="fas fa-envelope"></i><span class="n" id="rs-msg">0</span>msg</div>' +
                '<div class="rmd-u-stat"><i class="fas fa-location-dot"></i><span class="n" id="rs-pos">0</span>pos</div>' +
                '<div class="rmd-u-stat"><i class="fas fa-box"></i><span class="n" id="rs-pkt">0</span>pkt</div>' +
                '<div class="rmd-u-stat"><i class="fas fa-chart-line"></i><span class="n" id="rs-tel">0</span>tel</div>' +
                '<div class="rmd-u-stat"><i class="fas fa-broadcast-tower"></i><span class="n" id="rs-ch">0</span>ch</div>' +
              '</div>' +
              /* Connection dots */
              '<div class="rmd-u-section rmd-u-conn">' +
                '<div class="rmd-u-conn-dots">' +
                  '<div class="rmd-u-conn-row">' +
                    '<span class="rmd-u-dot" id="dot-api"></span>' +
                    '<span class="rmd-u-lbl">API</span>' +
                    '<span class="rmd-u-val grey" id="v-api">…</span>' +
                  '</div>' +
                  '<div class="rmd-u-conn-row">' +
                    '<span class="rmd-u-dot lg" id="dot-nod"></span>' +
                    '<span class="rmd-u-lbl">RF</span>' +
                    '<span class="rmd-u-val grey" id="v-nod">…</span>' +
                  '</div>' +
                '</div>' +
              '</div>' +
              /* Mini graph */
              '<div class="rmd-u-section rmd-u-graph">' +
                '<canvas id="rmd-mini" class="rmd-mini-canvas"></canvas>' +
              '</div>' +
              /* IO */
              '<div class="rmd-u-io" id="rmd-io-trigger" title="Traffic Monitor">' +
                '<div class="rmd-u-io-row"><div class="rmd-io-dot" id="rmd-io-tx"></div><div class="rmd-u-io-lbl">TX</div></div>' +
                '<div class="rmd-u-io-row"><div class="rmd-io-dot" id="rmd-io-rx"></div><div class="rmd-u-io-lbl">RX</div></div>' +
              '</div>' +
            '</div>' +
          '</div>';
  
        var logModal = document.createElement('div');
        logModal.id = 'rmd-log-modal';
        logModal.innerHTML =
          '<div class="rmd-log-box">' +
            '<div class="rmd-log-head">' +
              '<span><i class="fas fa-network-wired"></i> Traffic Monitor</span>' +
              '<button class="rmd-log-close" id="rmd-log-close">&times;</button>' +
            '</div>' +
            '<div class="rmd-log-body" id="rmd-log-body">' +
              '<div style="padding:20px;text-align:center;color:#444">Waiting for traffic…</div>' +
            '</div>' +
          '</div>';
  
        var sync = document.createElement('div');
        sync.id = 'rmd-sync-banner';
        sync.innerHTML = '<i class="fas fa-sync-alt fa-spin"></i>&nbsp; Syncing node database…';
  
        var modal = document.createElement('div');
        modal.id = 'rmd-modal';
        modal.innerHTML =
          '<div class="rmd-mc">' +
            '<div class="rmd-mh">' +
              '<div>' +
                '<h3 class="rmd-mt"><i class="fas fa-satellite-dish"></i> System Diagnostics</h3>' +
                '<p class="rmd-msub">Real-time node status · RF configuration · uptime &amp; connection history</p>' +
              '</div>' +
              '<button class="rmd-mx" id="rmd-mclose">&times;</button>' +
            '</div>' +
            '<div class="rmd-mb">' +
              '<div class="rmd-uptime-row">' +
                '<div class="rmd-uptime-card uc-session"><span class="rmd-uc-badge">Dashboard</span><div class="rmd-uc-val" id="mi-up-session">-</div><div class="rmd-uc-lbl">Tab / SSE Session</div><div class="rmd-uc-sub" id="mi-up-session-sub">SSE stream open time</div></div>' +
                '<div class="rmd-uptime-card uc-script"><span class="rmd-uc-badge">Process</span><div class="rmd-uc-val" id="mi-up-script">-</div><div class="rmd-uc-lbl">Python Script Runtime</div><div class="rmd-uc-sub" id="mi-up-script-sub">since process start</div></div>' +
                '<div class="rmd-uptime-card uc-node"><span class="rmd-uc-badge">Firmware</span><div class="rmd-uc-val" id="mi-up-node">-</div><div class="rmd-uc-lbl">Radio Node Uptime</div><div class="rmd-uc-sub" id="mi-up-node-sub">since last firmware boot</div></div>' +
              '</div>' +
              '<div class="rmd-grid-3">' +
                '<div class="rmd-card"><div class="rmd-card-hdr"><i class="fas fa-microchip"></i> Identity</div><div class="rmd-kv-grid">' +
                  '<div class="rmd-kv"><span class="rmd-k">Node ID</span><span class="rmd-v grn" id="mi-id">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Short Name</span><span class="rmd-v" id="mi-short">-</span></div>' +
                  '<div class="rmd-kv" style="grid-column:1/-1"><span class="rmd-k">Long Name</span><span class="rmd-v" id="mi-long">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Hardware</span><span class="rmd-v" id="mi-hw">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Firmware</span><span class="rmd-v blu" id="mi-fw">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">PIO Env</span><span class="rmd-v" id="mi-pio">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Role</span><span class="rmd-v pur" id="mi-role">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Reboots</span><span class="rmd-v" id="mi-reboot">-</span></div>' +
                '</div></div>' +
                '<div class="rmd-card"><div class="rmd-card-hdr"><i class="fas fa-broadcast-tower"></i> RF / LoRa</div><div class="rmd-kv-grid">' +
                  '<div class="rmd-kv"><span class="rmd-k">Region</span><span class="rmd-v grn" id="mi-region">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">LoRa Region</span><span class="rmd-v" id="mi-lregion">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Hop Limit</span><span class="rmd-v" id="mi-hops">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">TX Power</span><span class="rmd-v" id="mi-txpwr">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">TX State</span><span class="rmd-v" id="mi-txstate">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">NodeDB</span><span class="rmd-v" id="mi-nodedb">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Max Channels</span><span class="rmd-v" id="mi-maxch">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Ch Util</span><span class="rmd-v" id="mi-chutil">-</span></div>' +
                '</div></div>' +
                '<div class="rmd-card"><div class="rmd-card-hdr"><i class="fas fa-battery-three-quarters"></i> Power &amp; Network</div><div class="rmd-kv-grid">' +
                  '<div class="rmd-kv"><span class="rmd-k">Battery</span><span class="rmd-v" id="mi-bat">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Voltage</span><span class="rmd-v" id="mi-volt">-</span></div>' +
                  '<div class="rmd-kv" style="grid-column:1/-1"><span class="rmd-k">Battery Level</span><div class="rmd-bat-bar"><div class="rmd-bat-fill" id="mi-batbar" style="width:0%"></div></div></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Air Util TX</span><span class="rmd-v" id="mi-airutil">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">WiFi SSID</span><span class="rmd-v" id="mi-wifi">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Bluetooth</span><span class="rmd-v" id="mi-bt">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Position</span><span class="rmd-v" id="mi-pos-coords">-</span></div>' +
                  '<div class="rmd-kv"><span class="rmd-k">Altitude</span><span class="rmd-v" id="mi-alt">-</span></div>' +
                '</div></div>' +
              '</div>' +
              '<div class="rmd-card" style="margin-bottom:16px;"><div class="rmd-card-hdr"><i class="fas fa-chart-bar"></i> Session Statistics</div>' +
                '<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:8px;">' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num grn" id="mi-s-pkt">0</span><span class="rmd-stat-lbl">Packets</span></div>' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num" id="mi-s-msg">0</span><span class="rmd-stat-lbl">Messages</span></div>' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num blu" id="mi-s-pos">0</span><span class="rmd-stat-lbl">Positions</span></div>' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num" id="mi-s-tel">0</span><span class="rmd-stat-lbl">Telemetry</span></div>' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num org" id="mi-s-nodes">0</span><span class="rmd-stat-lbl">Nodes</span></div>' +
                  '<div class="rmd-stat-big"><span class="rmd-stat-num" id="mi-s-ch">0</span><span class="rmd-stat-lbl">Channels</span></div>' +
                '</div>' +
              '</div>' +
              '<div class="rmd-card">' +
                '<div class="rmd-card-hdr"><i class="fas fa-wave-square"></i> Connection Status &amp; History (24h)' +
                  '<span style="margin-left:auto;color:var(--txt3);font-size:.62rem;" id="mi-hist-meta"></span>' +
                '</div>' +
                '<div class="rmd-conn-rail" id="mi-rail-api"><div class="rmd-rail-dot grey" id="mi-rdot-api"></div><div class="rmd-rail-layer">API Stream</div><div class="rmd-rail-val grey" id="mi-rval-api">Connecting…</div><div class="rmd-rail-sub" id="mi-rsub-api"></div></div>' +
                '<div class="rmd-conn-rail" id="mi-rail-radio"><div class="rmd-rail-dot grey" id="mi-rdot-radio"></div><div class="rmd-rail-layer">Radio Link</div><div class="rmd-rail-val grey" id="mi-rval-radio">Initializing</div><div class="rmd-rail-sub" id="mi-rsub-radio"></div></div>' +
                '<div style="margin-top:16px;"><div class="rmd-graph-lbl">Radio Link History (24h)<span id="mi-hist-count"></span></div><div class="rmd-graph-box"><canvas id="rmd-full-canvas" class="rmd-full-canvas"></canvas></div></div>' +
                '<div class="rmd-legend"><span><span class="rmd-swatch" style="background:#00f090"></span>Connected</span><span><span class="rmd-swatch" style="background:#ffa800"></span>Reconnecting</span><span><span class="rmd-swatch" style="background:#ff3050"></span>Disconnected</span></div>' +
              '</div>' +
            '</div>' +
          '</div>';
  
        var spacer = document.createElement('div'); spacer.id = 'rmd-spacer';
        document.body.prepend(modal);
        document.body.prepend(sync);
        document.body.prepend(root);
        document.body.prepend(spacer);
        document.body.prepend(logModal);
      }
  
      _initGraphs() {
        var c = document.getElementById('rmd-mini');
        if (c) this.miniGraph = new MiniGraph(c, 48, 28);
        this.fullGraph = new FullGraph('rmd-full-canvas');
      }
  
      _nav() {
        var path = window.location.pathname;
        document.querySelectorAll('.rmd-lnk').forEach(function (a) {
          var h = a.getAttribute('href');
          if (h === '/' ? path === '/' : h !== '/logout' && path.startsWith(h))
            a.classList.add('rmd-active');
        });
      }
  
      _events() {
        var self = this;
        var trigger = document.getElementById('rmd-trigger');
        var modal = document.getElementById('rmd-modal');
        var close = document.getElementById('rmd-mclose');
  
        if (trigger) trigger.addEventListener('click', function (e) {
          if (e.target.closest('#rmd-io-trigger')) return;
          modal.classList.add('open');
          setTimeout(function () {
            self._renderModal();
            if (self.fullGraph) self.fullGraph.redraw(self._graphData, self._graphLabels);
          }, 30);
        });
        if (close) close.addEventListener('click', function () { modal.classList.remove('open'); });
        if (modal) modal.addEventListener('click', function (e) { if (e.target === modal) modal.classList.remove('open'); });
  
        var logTrig = document.getElementById('rmd-io-trigger');
        var logMod = document.getElementById('rmd-log-modal');
        var logCls = document.getElementById('rmd-log-close');
        if (logTrig) logTrig.addEventListener('click', function (e) { e.stopPropagation(); logMod.classList.add('open'); });
        if (logCls) logCls.addEventListener('click', function () { logMod.classList.remove('open'); });
        if (logMod) logMod.addEventListener('click', function (e) { if (e.target === logMod) logMod.classList.remove('open'); });
  
        // Hamburger
        var hamburger = document.getElementById('rmd-hamburger');
        var mcb = document.getElementById('rmd-mcb');
        if (hamburger && mcb) {
          hamburger.addEventListener('click', function () { mcb.checked = !mcb.checked; });
        }
  
        // Click-toggle dropdowns (works on mobile + desktop)
        document.querySelectorAll('.rmd-has-drop').forEach(function (dd) {
          var trig = dd.querySelector('.rmd-drop-trigger');
          if (!trig) return;
          trig.addEventListener('click', function (e) {
            e.preventDefault();
            e.stopPropagation();
            var wasOpen = dd.classList.contains('rmd-drop-open');
            // Close all other dropdowns first
            document.querySelectorAll('.rmd-has-drop.rmd-drop-open').forEach(function (other) {
              other.classList.remove('rmd-drop-open');
            });
            if (!wasOpen) dd.classList.add('rmd-drop-open');
          });
        });
        // Close dropdowns when clicking elsewhere
        document.addEventListener('click', function () {
          document.querySelectorAll('.rmd-has-drop.rmd-drop-open').forEach(function (dd) {
            dd.classList.remove('rmd-drop-open');
          });
        });
        // Close mobile nav when clicking a link
        document.querySelectorAll('.rmd-nav .rmd-lnk').forEach(function (a) {
          a.addEventListener('click', function () {
            if (mcb) mcb.checked = false;
          });
        });
      }
  
      // ─── GRAPH DATA ──────────────────────────────────────────
      _graphPush(value, label) {
        var now = Date.now();
        if (this._graphData.length > 0 && (now - this._lastGraphPush) < this._graphPushMinInterval) {
          this._graphData[this._graphData.length - 1] = value;
          if (label) this._graphLabels[this._graphLabels.length - 1] = label;
          this._scheduleRedraw();
          return;
        }
        this._lastGraphPush = now;
        this._graphData.push(value);
        if (!label) {
          var d = new Date();
          label = d.getHours().toString().padStart(2, '0') + ':' +
                  d.getMinutes().toString().padStart(2, '0');
        }
        this._graphLabels.push(label);
        var max = CONFIG.graphMaxPoints;
        if (this._graphData.length > max) {
          this._graphData.splice(0, this._graphData.length - max);
          this._graphLabels.splice(0, this._graphLabels.length - max);
        }
        this._scheduleRedraw();
      }
  
      _scheduleRedraw() {
        if (this._rafPending) return;
        this._rafPending = true;
        var self = this;
        requestAnimationFrame(function () {
          self._rafPending = false;
          self._redrawGraphs();
        });
      }
  
      _redrawGraphs() {
        if (this.miniGraph) this.miniGraph.draw(this._graphData);
        var modal = document.getElementById('rmd-modal');
        if (modal && modal.classList.contains('open') && this.fullGraph) {
          this.fullGraph.draw(this._graphData, this._graphLabels);
        }
      }
  
      // ─── SESSION CLOCK ───────────────────────────────────────
      _startSessionClock() {
        var self = this;
        if (this.sessionUptimeTimer) clearInterval(this.sessionUptimeTimer);
        this.sessionUptimeTimer = setInterval(function () {
          if (self._sessionStart) {
            self._sessionSec = Math.floor((Date.now() - self._sessionStart) / 1000);
            self.s.sessionUptime = self._sessionSec;
          }
        }, 1000);
      }
  
      _resetSessionClock() {
        this._sessionStart = Date.now();
        this._sessionSec = 0;
        this.s.sessionUptime = 0;
      }
  
      // ─── DOM HELPERS ─────────────────────────────────────────
      _setDot(id, cls) {
        var el = document.getElementById(id); if (!el) return;
        var sz = el.classList.contains('lg') ? ' lg' : '';
        el.className = 'rmd-u-dot' + sz + ' ' + cls;
      }
  
      _setVal(id, text, cls) {
        var el = document.getElementById(id); if (!el) return;
        el.textContent = text || '-';
        el.className = 'rmd-u-val ' + (cls || '');
      }
  
      _set(id, v, cls) {
        var el = document.getElementById(id); if (!el) return;
        el.textContent = (v != null && v !== '' && v !== 'Unknown' && v !== 'None') ? String(v) : '-';
        if (cls) el.className = 'rmd-v ' + cls;
      }
  
      _setText(id, v) {
        var el = document.getElementById(id);
        if (el) el.textContent = v != null ? String(v) : '-';
      }
  
      // ─── STATUS SETTERS ──────────────────────────────────────
      _setApiStatus(online, label) {
        this.s.apiOnline = online;
        this._setDot('dot-api', online ? 'ok' : 'err');
        this._setVal('v-api', label || (online ? 'Online' : 'Offline'), online ? 'ok' : 'err');
      }
  
      _setRadioStatus(status) {
        if (!status) return;
        this.s.connStatus = status;
        var t = radioStateTuple(status);
        this._setDot('dot-nod', t.dotCls);
        this._setVal('v-nod', t.label, t.valCls);
        if (status === 'Connected' && !this.s.radioConnectedAt) {
          this.s.radioConnectedAt = Date.now();
        } else if (status !== 'Connected') {
          this.s.radioConnectedAt = null;
        }
        if (status !== this._lastRadioStatus) {
          this._lastRadioStatus = status;
          this._graphPush(t.graphVal);
        }
      }
  
      // ─── NODE DATA ───────────────────────────────────────────
      _applyNodeData(data) {
        if (!data || typeof data !== 'object') return;
        var s = this.s;
        s.nodeId    = getVal(data, 'node_id', 'node_id_hex')            || s.nodeId;
        s.nodeLong  = getVal(data, 'long_name', 'longName')             || s.nodeLong;
        s.nodeShort = getVal(data, 'short_name', 'shortName')           || s.nodeShort;
        s.hw        = cleanEnum(getVal(data, 'hardware_model_string', 'hardware_model', 'hwModel')) || s.hw;
        s.fw        = getVal(data, 'firmware_version', 'firmwareVersion') || s.fw;
        s.pioEnv    = getVal(data, 'pio_env')    || s.pioEnv;
        s.rebootCount = getVal(data, 'reboot_count') != null ? getVal(data, 'reboot_count') : s.rebootCount;
        s.role       = cleanEnum(getVal(data, 'role'))       || s.role;
        s.region     = cleanEnum(getVal(data, 'region'))    || s.region;
        s.loraRegion = cleanEnum(getVal(data, 'lora_region')) || s.loraRegion;
        s.loraHops   = getVal(data, 'lora_hop_limit')  != null ? getVal(data, 'lora_hop_limit') : s.loraHops;
        s.loraTx     = getVal(data, 'lora_tx_power')   != null ? getVal(data, 'lora_tx_power') : s.loraTx;
        s.loraTxEnabled = getVal(data, 'lora_tx_enabled') != null ? getVal(data, 'lora_tx_enabled') : s.loraTxEnabled;
        s.nodeDbCount   = getVal(data, 'nodedb_count') != null ? getVal(data, 'nodedb_count') : s.nodeDbCount;
        s.maxCh      = getVal(data, 'max_channels')    != null ? getVal(data, 'max_channels') : s.maxCh;
        s.chUtil     = getVal(data, 'channel_utilization') != null ? getVal(data, 'channel_utilization') : s.chUtil;
        s.airUtil    = getVal(data, 'air_util_tx')      != null ? getVal(data, 'air_util_tx') : s.airUtil;
        s.battery    = getVal(data, 'battery_level', 'batteryLevel') != null ? getVal(data, 'battery_level', 'batteryLevel') : s.battery;
        s.voltage    = getVal(data, 'voltage')  != null ? getVal(data, 'voltage') : s.voltage;
        s.lat        = getVal(data, 'latitude')  != null ? getVal(data, 'latitude') : s.lat;
        s.lon        = getVal(data, 'longitude') != null ? getVal(data, 'longitude') : s.lon;
        s.alt        = getVal(data, 'altitude')  != null ? getVal(data, 'altitude') : s.alt;
        s.wifiSsid   = getVal(data, 'wifi_ssid') || s.wifiSsid;
        s.hasWifi    = getVal(data, 'has_wifi')  != null ? getVal(data, 'has_wifi') : s.hasWifi;
        s.hasBt      = getVal(data, 'has_bluetooth') != null ? getVal(data, 'has_bluetooth') : s.hasBt;
        s.btEnabled  = getVal(data, 'bluetooth_enabled') != null ? getVal(data, 'bluetooth_enabled') : s.btEnabled;
        var nu = getVal(data, 'uptime_seconds', 'uptimeSeconds');
        if (nu != null) s.nodeUptime = nu;
        this._refreshStrip();
      }
  
      _applyStats(stats) {
        if (!stats) return;
        var s = this.s;
        s.pkt   = stats.packets_received_session  != null ? stats.packets_received_session : s.pkt;
        s.msg   = stats.text_messages_session     != null ? stats.text_messages_session : s.msg;
        s.pos   = stats.position_updates_session  != null ? stats.position_updates_session : s.pos;
        s.tel   = stats.telemetry_reports_session != null ? stats.telemetry_reports_session : s.tel;
        s.nodes = stats.nodes_seen_session        != null ? stats.nodes_seen_session : s.nodes;
        s.chans = stats.channels_seen_session     != null ? stats.channels_seen_session : s.chans;
        var el = stats.elapsed_time_session;
        if (el != null) {
          s.scriptUptime = Math.floor(el);
          s.scriptStartedAt = Math.floor(Date.now() / 1000) - s.scriptUptime;
        }
        var ids = ['rs-nodes', 'rs-msg', 'rs-pos', 'rs-pkt', 'rs-tel', 'rs-ch'];
        var vals = [s.nodes, s.msg, s.pos, s.pkt, s.tel, s.chans];
        for (var i = 0; i < ids.length; i++) {
          var elem = document.getElementById(ids[i]);
          if (elem) elem.textContent = String(vals[i] || 0);
        }
      }
  
      _refreshStrip() {
        var s = this.s;
        var nid = document.getElementById('rmd-u-nid');
        var nmeta = document.getElementById('rmd-u-nmeta');
        if (nid && s.nodeId) nid.textContent = s.nodeId;
        if (nmeta) {
          var parts = [];
          if (s.nodeLong) parts.push(s.nodeLong);
          else if (s.nodeShort) parts.push(s.nodeShort);
          if (s.hw) parts.push(s.hw);
          if (s.role) parts.push(s.role);
          var html = parts.join(' · ') || 'Connecting…';
          if (s.battery != null) {
            var bc = batStripCls(s.battery);
            html += ' <span class="rmd-bat ' + bc + '"><i class="fas fa-bolt" style="font-size:.5rem"></i>' + s.battery + '%</span>';
          }
          var ut = fmtUptime(s.nodeUptime);
          if (ut) {
            html += ' <span style="color:var(--txt3)">·</span> <span style="color:var(--acc);font-size:.55rem"><i class="fas fa-clock" style="font-size:.5rem"></i> ' + ut + '</span>';
          }
          nmeta.innerHTML = html;
        }
      }
  
      // ─── POLLING ─────────────────────────────────────────────
      async _poll() {
        try {
          var r = await fetch(CONFIG.statusEndpoint);
          if (r.ok) {
            var d = await r.json();
            if (d.api_status === 'online') this._setApiStatus(true, 'Online');
            this._setRadioStatus(d.connection_status);
            if (d.local_node_info && Object.keys(d.local_node_info).length > 2)
              this._applyNodeData(d.local_node_info);
          }
        } catch (e) { /* silent */ }
        try {
          var r2 = await fetch(CONFIG.localNodeFullEndpoint);
          if (r2.ok) this._applyNodeData(await r2.json());
        } catch (e) { /* silent */ }
        try {
          var r3 = await fetch(CONFIG.statsEndpoint);
          if (r3.ok) this._applyStats(await r3.json());
        } catch (e) { /* silent */ }
      }
  
      // ─── HISTORY (24h cap) ───────────────────────────────────
      async _loadHistory() {
        if (this._historyLoading) return;
        this._historyLoading = true;
        try {
          var r = await fetch(CONFIG.historyEndpoint + '?limit=5000');
          if (!r.ok) return;
          var raw = await r.json();
          if (!raw || !raw.length) return;
          var cutoff = (Date.now() / 1000) - CONFIG.graphHistorySeconds;
          var vals = [], labels = [];
          for (var i = 0; i < raw.length; i++) {
            var d = raw[i];
            if (d.timestamp && d.timestamp < cutoff) continue;
            var v = d.value !== undefined ? parseFloat(d.value) : 0;
            vals.push(isNaN(v) ? 0 : v);
            if (d.timestamp) {
              var dt = new Date(d.timestamp * 1000);
              labels.push(
                dt.getHours().toString().padStart(2, '0') + ':' +
                dt.getMinutes().toString().padStart(2, '0')
              );
            } else {
              labels.push('');
            }
          }
          this._graphData = vals;
          this._graphLabels = labels;
          this._historyLoaded = true;
          this._redrawGraphs();
          var meta = document.getElementById('mi-hist-meta');
          var count = document.getElementById('mi-hist-count');
          if (vals.length > 0) {
            var filtered = raw.filter(function (d) { return !d.timestamp || d.timestamp >= cutoff; });
            if (filtered.length > 0) {
              var from = new Date(filtered[0].timestamp * 1000).toLocaleString();
              var to = new Date(filtered[filtered.length - 1].timestamp * 1000).toLocaleString();
              if (meta) meta.textContent = from + ' \u2192 ' + to;
            }
            if (count) count.textContent = vals.length + ' events (24h)';
          }
        } catch (e) { /* silent, retry on visibility */ }
        finally { this._historyLoading = false; }
      }
  
      // ─── MODAL RENDER ────────────────────────────────────────
      _renderModal() {
        var s = this.s;
        var sessionFmt = fmtUptimeLong(s.sessionUptime);
        this._setText('mi-up-session', sessionFmt.primary || '-');
        this._setText('mi-up-session-sub', sessionFmt.secondary || 'Tab opened recently');
        var scriptFmt = fmtUptimeLong(s.scriptUptime, s.scriptStartedAt);
        this._setText('mi-up-script', scriptFmt.primary || '-');
        this._setText('mi-up-script-sub', scriptFmt.secondary || 'Python process start');
        var nodeFmt = fmtUptimeLong(s.nodeUptime);
        this._setText('mi-up-node', nodeFmt.primary || '-');
        this._setText('mi-up-node-sub',
          s.rebootCount != null ? s.rebootCount + ' reboot' + (s.rebootCount !== 1 ? 's' : '') : 'since firmware boot');
  
        this._set('mi-id', s.nodeId, 'grn');
        this._set('mi-short', s.nodeShort);
        this._set('mi-long', s.nodeLong);
        this._set('mi-hw', s.hw);
        this._set('mi-fw', s.fw, 'blu');
        this._set('mi-pio', s.pioEnv);
        this._set('mi-role', s.role, 'pur');
        this._set('mi-reboot', s.rebootCount != null ? s.rebootCount + '\u00d7' : null);
  
        this._set('mi-region', s.region, 'grn');
        this._set('mi-lregion', s.loraRegion || s.region);
        this._set('mi-hops', s.loraHops != null ? String(s.loraHops) : null);
        this._set('mi-txpwr', s.loraTx != null ? s.loraTx + ' dBm' : null);
        this._set('mi-txstate', s.loraTxEnabled != null ? (s.loraTxEnabled ? 'Enabled' : 'Disabled') : null,
          s.loraTxEnabled ? 'grn' : 'red');
        this._set('mi-nodedb', s.nodeDbCount != null ? String(s.nodeDbCount) : null);
        this._set('mi-maxch', s.maxCh != null ? String(s.maxCh) : null);
        this._set('mi-chutil', s.chUtil != null ? fmtNum(s.chUtil, 1, '%') : null);
  
        var batEl = document.getElementById('mi-bat');
        var batBar = document.getElementById('mi-batbar');
        if (batEl) {
          batEl.textContent = s.battery != null ? s.battery + '%' : '-';
          batEl.className = 'rmd-v ' + (s.battery != null ? batCls(s.battery) : '');
        }
        if (batBar && s.battery != null) {
          batBar.style.width = Math.max(0, Math.min(100, s.battery)) + '%';
          batBar.style.background = s.battery > 60 ? '#00f090' : s.battery > 20 ? '#ffa800' : '#ff3050';
        }
        this._set('mi-volt', s.voltage != null ? fmtNum(s.voltage, 2, ' V') : null);
        this._set('mi-airutil', s.airUtil != null ? fmtNum(s.airUtil, 1, '%') : null);
        this._set('mi-wifi', s.wifiSsid || (s.hasWifi ? 'Available' : null));
        this._set('mi-bt', s.hasBt ? (s.btEnabled ? 'On' : 'Off') : null,
          s.hasBt && s.btEnabled ? 'grn' : 'grey');
        this._set('mi-pos-coords',
          s.lat != null && s.lon != null ? s.lat.toFixed(4) + '\u00b0, ' + s.lon.toFixed(4) + '\u00b0' : null);
        this._set('mi-alt', s.alt != null ? s.alt + ' m' : null);
  
        this._setText('mi-s-pkt', s.pkt.toLocaleString());
        this._setText('mi-s-msg', s.msg.toLocaleString());
        this._setText('mi-s-pos', s.pos.toLocaleString());
        this._setText('mi-s-tel', s.tel.toLocaleString());
        this._setText('mi-s-nodes', String(s.nodes));
        this._setText('mi-s-ch', String(s.chans));
  
        var apiDotCls = s.apiOnline ? 'ok' : 'err';
        var apiLabel = s.apiOnline ? 'Stream Open' : 'Offline';
        var apiSub = s.apiOnline && s.scriptStartedAt
          ? 'process started ' + new Date(s.scriptStartedAt * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
          : '';
        var apiDotEl = document.getElementById('mi-rdot-api');
        var apiValEl = document.getElementById('mi-rval-api');
        var apiSubEl = document.getElementById('mi-rsub-api');
        if (apiDotEl) apiDotEl.className = 'rmd-rail-dot ' + apiDotCls;
        if (apiValEl) { apiValEl.textContent = apiLabel; apiValEl.className = 'rmd-rail-val ' + apiDotCls; }
        if (apiSubEl) apiSubEl.textContent = apiSub;
  
        var t = radioStateTuple(s.connStatus);
        var rDot = document.getElementById('mi-rdot-radio');
        var rVal = document.getElementById('mi-rval-radio');
        var rSub = document.getElementById('mi-rsub-radio');
        if (rDot) rDot.className = 'rmd-rail-dot ' + t.dotCls;
        if (rVal) { rVal.textContent = t.label; rVal.className = 'rmd-rail-val ' + t.valCls; }
        if (rSub) {
          var sub = '';
          if (s.connStatus === 'Connected' && s.radioConnectedAt)
            sub = 'connected ' + fmtUptime(Math.floor((Date.now() - s.radioConnectedAt) / 1000), { fallback: 'just now' }) + ' ago';
          else if (s.connStatus.toLowerCase().startsWith('reconnecting'))
            sub = 'attempting reconnect \u2014 do not refresh';
          else if (s.connStatus === 'Disconnected')
            sub = 'all retry attempts exhausted';
          rSub.textContent = sub;
        }
      }
  
      // ─── SSE ─────────────────────────────────────────────────
      _sse() {
        var self = this;
        if (this.es && this.es.readyState === EventSource.OPEN) return;
        if (this.es) { this.es.close(); this.es = null; }
        this._setApiStatus(false, 'Connecting');
        this.es = new EventSource(CONFIG.ssePath);
  
        this.es.onopen = function () {
          self._setApiStatus(true, 'Online');
          self._resetSessionClock();
          if (self.reconnectTimer) { clearTimeout(self.reconnectTimer); self.reconnectTimer = null; }
          if (CONFIG.pollOnConnect) setTimeout(function () { self._poll(); }, 200);
        };
        this.es.onerror = function () {
          self.es.close(); self.es = null;
          self._setApiStatus(false, 'Offline');
          if (!self.reconnectTimer) {
            self.reconnectTimer = setTimeout(function () {
              self.reconnectTimer = null;
              self._sse();
            }, 5000);
          }
        };
        this.es.addEventListener('connection_status', function (e) {
          try { self._setRadioStatus(JSON.parse(e.data)); } catch (err) { /* skip */ }
        });
        this.es.addEventListener('sync_status', function (e) {
          try {
            var d = JSON.parse(e.data);
            var banner = document.getElementById('rmd-sync-banner');
            if (banner) banner.classList.toggle('vis', !!d.is_syncing);
            if (d.is_syncing) {
              self._setDot('dot-nod', 'sync');
              self._setVal('v-nod', d.current || 'Syncing', 'warn');
            } else {
              self._setRadioStatus(self.s.connStatus);
            }
          } catch (err) { /* skip */ }
        });
        this.es.addEventListener('stats', function (e) {
          try { self._applyStats(JSON.parse(e.data)); } catch (err) { /* skip */ }
        });
        this.es.addEventListener('local_node_info', function (e) {
          try {
            var d = JSON.parse(e.data);
            if (d && Object.keys(d).length > 2) {
              self._applyNodeData(d);
              setTimeout(function () { self._poll(); }, 400);
            }
          } catch (err) { /* skip */ }
        });
        this.es.addEventListener('node_update', function (e) {
          try {
            var d = JSON.parse(e.data);
            if (d && (d.isLocal === true || d.is_local === 1 || d.is_local === true))
              self._applyNodeData(d);
          } catch (err) { /* skip */ }
        });
        this.es.addEventListener('packet', function () { /* no graph push */ });
        this.es.addEventListener('activity', function (e) {
          try {
            var type = JSON.parse(e.data);
            self._flashIo(type);
            self._logIo(type);
          } catch (err) { /* skip */ }
        });
      }
  
      _flashIo(type) {
        var id = type === 'TX' ? 'rmd-io-tx' : 'rmd-io-rx';
        var cls = type === 'TX' ? 'tx-active' : 'rx-active';
        var el = document.getElementById(id);
        if (el) { el.classList.add(cls); setTimeout(function () { el.classList.remove(cls); }, 100); }
      }
  
      _logIo(type) {
        var box = document.getElementById('rmd-log-body');
        if (!box) return;
        if (box.children.length === 1 && box.innerText.includes('Waiting')) box.innerHTML = '';
        var row = document.createElement('div');
        row.className = 'rmd-log-row';
        row.innerHTML =
          '<div class="rmd-log-ts">' + new Date().toLocaleTimeString().split(' ')[0] + '</div>' +
          '<div class="rmd-log-dir ' + type + '">' + type + '</div>' +
          '<div class="rmd-log-msg">Packet Activity Detected</div>';
        box.prepend(row);
        if (box.children.length > 50) box.lastElementChild.remove();
      }
  
      _heartbeat() {
        var self = this;
        if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
        this.heartbeatTimer = setInterval(function () {
          fetch(CONFIG.statusEndpoint, { method: 'GET' }).catch(function () {});
          self._poll();
        }, CONFIG.heartbeatInterval);
      }
  
      async _wakeLock() {
        var self = this;
        if (!('wakeLock' in navigator)) return;
        try {
          this.wakeLock = await navigator.wakeLock.request('screen');
          document.addEventListener('visibilitychange', async function () {
            if (self.wakeLock && document.visibilityState === 'visible') {
              try { self.wakeLock = await navigator.wakeLock.request('screen'); } catch (e) { /* skip */ }
            }
          });
        } catch (e) { /* skip */ }
      }
    }
  
    // ─── Bootstrap ─────────────────────────────────────────────
    window.RusjpMeshHeader = new HeaderController();
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', function () { window.RusjpMeshHeader.init(); });
    } else {
      window.RusjpMeshHeader.init();
    }
  })();