|
77 | 77 | display: flex; |
78 | 78 | align-items: center; |
79 | 79 | gap: 10px; |
| 80 | + text-decoration: none; |
| 81 | + color: inherit; |
80 | 82 | } |
| 83 | +a.header-left:hover h1 { color: var(--accent); } |
81 | 84 | .header-logo { |
82 | 85 | width: 6px; |
83 | 86 | height: 20px; |
|
88 | 91 | .header h1 { |
89 | 92 | font-family: var(--sans); |
90 | 93 | font-size: 14px; |
91 | | - font-weight: 600; |
92 | | - color: var(--text); |
| 94 | + font-weight: 400; |
| 95 | + color: var(--text2); |
93 | 96 | letter-spacing: 0.5px; |
| 97 | + transition: color 0.15s; |
| 98 | +} |
| 99 | +.header h1 b { |
| 100 | + font-weight: 700; |
| 101 | + color: var(--text); |
| 102 | +} |
| 103 | +.header-right { |
| 104 | + display: flex; |
| 105 | + align-items: center; |
| 106 | + gap: 14px; |
94 | 107 | } |
95 | | -.header h1 span { |
| 108 | +.header-github { |
| 109 | + display: flex; |
| 110 | + align-items: center; |
96 | 111 | color: var(--text2); |
97 | | - font-weight: 400; |
| 112 | + transition: color 0.15s; |
98 | 113 | } |
| 114 | +.header-github:hover { color: var(--text); } |
| 115 | +.header-github svg { display: block; } |
99 | 116 | .status-badge { |
100 | 117 | display: flex; |
101 | 118 | align-items: center; |
|
450 | 467 | <body> |
451 | 468 |
|
452 | 469 | <div class="header"> |
453 | | - <div class="header-left"> |
| 470 | + <div class="header-left" id="header-left"> |
454 | 471 | <div class="header-logo"></div> |
455 | | - <h1>VXLAN <span>Controller</span></h1> |
| 472 | + <h1 id="header-title"><b>VXLAN</b> Controller</h1> |
456 | 473 | </div> |
457 | | - <div class="status-badge"> |
458 | | - <div class="status-dot" id="ws-dot"></div> |
459 | | - <span id="ws-status">connecting</span> |
460 | | - <span id="site-count"></span> |
| 474 | + <div class="header-right"> |
| 475 | + <div class="status-badge"> |
| 476 | + <div class="status-dot" id="ws-dot"></div> |
| 477 | + <span id="ws-status">connecting</span> |
| 478 | + <span id="site-count"></span> |
| 479 | + </div> |
| 480 | + <a class="header-github" href="https://github.com/PoemaIX/vxlan-controller" target="_blank" rel="noopener noreferrer" title="GitHub"> |
| 481 | + <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> |
| 482 | + <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"/> |
| 483 | + </svg> |
| 484 | + </a> |
461 | 485 | </div> |
462 | 486 | </div> |
463 | 487 |
|
@@ -657,23 +681,53 @@ <h3 id="site-detail-title"></h3> |
657 | 681 | return 'lat-bad'; |
658 | 682 | } |
659 | 683 |
|
660 | | - function getLatency(srcId, dstId) { |
| 684 | + function getLatencyEntry(srcId, dstId) { |
661 | 685 | if (!state?.latency_matrix) return null; |
662 | 686 | for (const row of state.latency_matrix) { |
663 | 687 | if (row.src === srcId) { |
664 | 688 | for (const e of (row.entries || [])) { |
665 | | - if (e.dst === dstId) return e.latency; |
| 689 | + if (e.dst === dstId) return e; |
666 | 690 | } |
667 | 691 | } |
668 | 692 | } |
669 | 693 | return null; |
670 | 694 | } |
671 | 695 |
|
672 | 696 | // --- Render --- |
| 697 | + function renderHeader() { |
| 698 | + const titleEl = document.getElementById('header-title'); |
| 699 | + const leftEl = document.getElementById('header-left'); |
| 700 | + const title = state?.config?.title; |
| 701 | + if (title) titleEl.innerHTML = title; |
| 702 | + const url = state?.config?.url; |
| 703 | + const isAnchor = leftEl.tagName === 'A'; |
| 704 | + if (url) { |
| 705 | + if (!isAnchor) { |
| 706 | + const a = document.createElement('a'); |
| 707 | + a.id = 'header-left'; |
| 708 | + a.className = 'header-left'; |
| 709 | + a.target = '_blank'; |
| 710 | + a.rel = 'noopener noreferrer'; |
| 711 | + while (leftEl.firstChild) a.appendChild(leftEl.firstChild); |
| 712 | + leftEl.parentNode.replaceChild(a, leftEl); |
| 713 | + a.href = url; |
| 714 | + } else if (leftEl.getAttribute('href') !== url) { |
| 715 | + leftEl.setAttribute('href', url); |
| 716 | + } |
| 717 | + } else if (isAnchor) { |
| 718 | + const d = document.createElement('div'); |
| 719 | + d.id = 'header-left'; |
| 720 | + d.className = 'header-left'; |
| 721 | + while (leftEl.firstChild) d.appendChild(leftEl.firstChild); |
| 722 | + leftEl.parentNode.replaceChild(d, leftEl); |
| 723 | + } |
| 724 | + } |
| 725 | + |
673 | 726 | function render() { |
674 | 727 | if (!state) return; |
675 | 728 | const online = state.clients.filter(c => c.online).length; |
676 | 729 | document.getElementById('site-count').textContent = online + '/' + state.clients.length + ' sites'; |
| 730 | + renderHeader(); |
677 | 731 | renderTopology(); |
678 | 732 | renderRouteTable(); |
679 | 733 | renderLatencyMatrix(); |
@@ -727,10 +781,14 @@ <h3 id="site-detail-title"></h3> |
727 | 781 | const key = sorted.join('-'); |
728 | 782 | if (!linkSet.has(key) && nodeMap[sorted[0]] && nodeMap[sorted[1]]) { |
729 | 783 | linkSet.add(key); |
| 784 | + const fwd = getLatencyEntry(sorted[0], sorted[1]); |
| 785 | + const rev = getLatencyEntry(sorted[1], sorted[0]); |
730 | 786 | links.push({ |
731 | 787 | src: nodeMap[sorted[0]], dst: nodeMap[sorted[1]], af: e.af, |
732 | | - latFwd: getLatency(sorted[0], sorted[1]), |
733 | | - latRev: getLatency(sorted[1], sorted[0]), |
| 788 | + latFwd: fwd ? fwd.latency : null, |
| 789 | + latRev: rev ? rev.latency : null, |
| 790 | + afFwd: fwd ? fwd.af : '', |
| 791 | + afRev: rev ? rev.af : '', |
734 | 792 | }); |
735 | 793 | } |
736 | 794 | }); |
@@ -788,14 +846,16 @@ <h3 id="site-detail-title"></h3> |
788 | 846 | // Always show both directions as normal |
789 | 847 | if (fwdOk) { |
790 | 848 | const isSPTDir = fwdInSPT; |
791 | | - const txt = l.latFwd.toFixed(1) + ' ms'; |
| 849 | + const afPart = l.afFwd ? l.afFwd + ' ' : ''; |
| 850 | + const txt = afPart + l.latFwd.toFixed(1) + ' ms'; |
792 | 851 | const tw = txt.length * 5.2 + 8; |
793 | 852 | html += '<rect class="topo-link-label-bg" x="'+(x1+ox-tw/2)+'" y="'+(y1+oy-7)+'" width="'+tw+'" height="14"/>'; |
794 | 853 | html += '<text class="topo-link-label" style="fill:'+(isSPTDir?'var(--green)':'#fff')+'" x="'+(x1+ox)+'" y="'+(y1+oy)+'">'+txt+'</text>'; |
795 | 854 | } |
796 | 855 | if (revOk) { |
797 | 856 | const isSPTDir = revInSPT; |
798 | | - const txt = l.latRev.toFixed(1) + ' ms'; |
| 857 | + const afPart = l.afRev ? l.afRev + ' ' : ''; |
| 858 | + const txt = afPart + l.latRev.toFixed(1) + ' ms'; |
799 | 859 | const tw = txt.length * 5.2 + 8; |
800 | 860 | html += '<rect class="topo-link-label-bg" x="'+(x2+ox-tw/2)+'" y="'+(y2+oy-7)+'" width="'+tw+'" height="14"/>'; |
801 | 861 | html += '<text class="topo-link-label" style="fill:'+(isSPTDir?'var(--green)':'#fff')+'" x="'+(x2+ox)+'" y="'+(y2+oy)+'">'+txt+'</text>'; |
@@ -909,7 +969,8 @@ <h3 id="site-detail-title"></h3> |
909 | 969 | } else { |
910 | 970 | const e = latMap[src.id+'|'+dst.id]; |
911 | 971 | if (e && e.latency != null && e.latency < 1e15) { |
912 | | - h += '<td class="lat-cell '+latClass(e.latency)+'"><span>'+e.latency.toFixed(1)+'</span></td>'; |
| 972 | + const afPart = e.af ? '<span style="color:var(--text3);margin-right:4px">'+e.af+'</span>' : ''; |
| 973 | + h += '<td class="lat-cell '+latClass(e.latency)+'">'+afPart+'<span>'+e.latency.toFixed(1)+'</span></td>'; |
913 | 974 | } else { |
914 | 975 | h += '<td class="lat-cell" style="color:var(--text3)">?</td>'; |
915 | 976 | } |
|
0 commit comments