Files
pyMC_Repeater/repeater/templates/nav.html
T
2025-11-02 23:01:22 +00:00

724 lines
34 KiB
HTML

<!-- Shared Navigation Component -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<h1>pyMC Repeater</h1>
<button class="menu-toggle" id="menu-toggle" aria-label="Toggle menu">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="node-name">Node: {{ node_name }}</div>
<div class="node-pubkey">&lt;{{ pub_key }}&gt;</div>
</div>
<div class="sidebar-content-wrapper">
<nav class="sidebar-nav">
<div class="nav-section">
<div class="nav-section-title">Actions</div>
<button id="send-advert-btn" class="nav-item nav-action">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
Send Advert
</button>
</div>
<div class="nav-section">
<div class="nav-section-title">Monitoring</div>
<a href="/" class="nav-item{{ ' active' if page == 'dashboard' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
Dashboard
</a>
<a href="/neighbors" class="nav-item{{ ' active' if page == 'neighbors' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
Neighbors
</a>
<a href="/statistics" class="nav-item{{ ' active' if page == 'statistics' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="22"></line>
<path d="M17 8v12"></path>
<path d="M7 14v6"></path>
</svg>
Statistics
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">System</div>
<a href="/configuration" class="nav-item{{ ' active' if page == 'configuration' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 1 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
</svg>
Configuration
</a>
<a href="/logs" class="nav-item{{ ' active' if page == 'logs' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="13" x2="16" y2="13"></line>
<line x1="12" y1="17" x2="16" y2="17"></line>
</svg>
Logs
</a>
<a href="/help" class="nav-item{{ ' active' if page == 'help' else '' }}">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
Help
</a>
</div>
</nav>
<!-- Noise Floor Display -->
<div class="noise-floor-display">
<div class="noise-floor-label">RF Noise Floor</div>
<div class="noise-floor-value">
<span id="noise-floor-value">--</span>
<span class="noise-floor-unit">dBm</span>
</div>
<div class="noise-floor-sparkline-container">
<svg id="noise-floor-sparkline" width="220" height="48" viewBox="0 0 220 48" preserveAspectRatio="none"></svg>
</div>
</div>
<div class="sidebar-footer">
<div style="display: flex; gap: 8px; align-items: center; margin-bottom: var(--spacing-md);">
<div class="status-badge" id="status-badge" title="System operational status">Online</div>
</div>
<!-- Version badges -->
<div style="display: flex; gap: 6px; align-items: center; margin-bottom: var(--spacing-md); flex-wrap: wrap;">
<div class="version-badge" id="repeater-version-badge" title="pyMC_Repeater version">R:v1.0.1</div>
<div class="version-badge" id="core-version-badge" title="pyMC_Core version">C:v1.0.2</div>
</div>
<!-- Mode Toggle Buttons -->
<div class="control-buttons">
<button class="control-btn" id="mode-toggle-btn" title="Toggle between Forward and Monitor modes">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="17 1 21 5 17 9"></polyline>
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
<polyline points="7 23 3 19 7 15"></polyline>
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
</svg>
<span class="control-label">
<span class="control-title">Mode</span>
<span class="control-value" id="mode-status">Forward</span>
</span>
</button>
<button class="control-btn" id="duty-cycle-toggle-btn" title="Toggle duty cycle enforcement">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
<span class="control-label">
<span class="control-title">Duty Cycle</span>
<span class="control-value" id="duty-cycle-status">Enabled</span>
</span>
</button>
</div>
<div class="duty-cycle-stats">
<div class="duty-cycle-bar-container">
<div class="duty-cycle-bar" id="duty-cycle-bar"></div>
</div>
<small class="duty-cycle-text">
Duty Cycle: <strong id="duty-utilization">0.0%</strong> / <span id="duty-max">10.0%</span>
</small>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px;">
<small>Last updated: <span id="footer-update-time">{{ last_updated }}</span></small>
<div style="display: flex; gap: 12px; align-items: center;">
<a href="https://buymeacoffee.com/rightup" target="_blank" class="coffee-link" title="Buy me a coffee">
<svg width="18" height="18" viewBox="0 0 27 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.3206 17.9122C12.9282 18.5083 11.3481 19.1842 9.30013 19.1842C8.44341 19.1824 7.59085 19.0649 6.76562 18.8347L8.18203 33.3768C8.23216 33.9847 8.50906 34.5514 8.95772 34.9645C9.40638 35.3776 9.994 35.6069 10.6039 35.6068C10.6039 35.6068 12.6122 35.7111 13.2823 35.7111C14.0036 35.7111 16.1662 35.6068 16.1662 35.6068C16.776 35.6068 17.3635 35.3774 17.8121 34.9643C18.2606 34.5512 18.5374 33.9846 18.5876 33.3768L20.1046 17.3073C19.4267 17.0757 18.7425 16.9219 17.9712 16.9219C16.6372 16.9214 15.5623 17.3808 14.3206 17.9122Z" fill="#FFDD00"></path>
<path d="M26.6584 10.3609L26.4451 9.28509C26.2537 8.31979 25.8193 7.40768 24.8285 7.05879C24.5109 6.94719 24.1505 6.89922 23.907 6.66819C23.6634 6.43716 23.5915 6.07837 23.5351 5.74565C23.4308 5.13497 23.3328 4.52377 23.2259 3.91413C23.1336 3.39002 23.0606 2.80125 22.8202 2.32042C22.5073 1.6748 21.858 1.29723 21.2124 1.04743C20.8815 0.923938 20.5439 0.819467 20.2012 0.734533C18.5882 0.308987 16.8922 0.152536 15.2328 0.0633591C13.241 -0.046547 11.244 -0.0134338 9.25692 0.162444C7.77794 0.296992 6.22021 0.459701 4.81476 0.971295C4.30108 1.15851 3.77175 1.38328 3.38115 1.78015C2.90189 2.26775 2.74544 3.02184 3.09537 3.62991C3.34412 4.06172 3.7655 4.3668 4.21242 4.56862C4.79457 4.82867 5.40253 5.02654 6.02621 5.15896C7.76282 5.54279 9.56148 5.6935 11.3356 5.75765C13.302 5.83701 15.2716 5.77269 17.2286 5.56521C17.7126 5.51202 18.1956 5.44822 18.6779 5.37382C19.2458 5.28673 19.6103 4.54411 19.4429 4.02678C19.2427 3.40828 18.7045 3.16839 18.0959 3.26173C18.0062 3.27581 17.917 3.28885 17.8273 3.30189L17.7626 3.31128C17.5565 3.33735 17.3503 3.36169 17.1441 3.38429C16.7182 3.43018 16.2913 3.46773 15.8633 3.49693C14.9048 3.56368 13.9437 3.59445 12.9831 3.59602C12.0391 3.59602 11.0947 3.56942 10.1529 3.50736C9.72314 3.4792 9.29447 3.44339 8.86684 3.39993C8.67232 3.37959 8.47832 3.35821 8.28432 3.33422L8.0997 3.31076L8.05955 3.30502L7.86816 3.27738C7.47703 3.21845 7.0859 3.15066 6.69895 3.06878C6.6599 3.06012 6.62498 3.03839 6.59994 3.0072C6.57491 2.976 6.56127 2.9372 6.56127 2.8972C6.56127 2.85721 6.57491 2.81841 6.59994 2.78721C6.62498 2.75602 6.6599 2.73429 6.69895 2.72563H6.70625C7.04158 2.65418 7.37951 2.59317 7.71849 2.53997C7.83148 2.52224 7.94482 2.50486 8.05851 2.48782H8.06164C8.27389 2.47374 8.48718 2.43567 8.69839 2.41064C10.536 2.2195 12.3845 2.15434 14.231 2.2156C15.1275 2.24168 16.0234 2.29435 16.9157 2.38509C17.1076 2.40491 17.2985 2.42577 17.4894 2.44923C17.5624 2.4581 17.6359 2.46853 17.7094 2.47739L17.8575 2.49878C18.2893 2.56309 18.7189 2.64115 19.1462 2.73293C19.7793 2.87061 20.5923 2.91546 20.8739 3.60906C20.9636 3.82913 21.0043 4.07371 21.0538 4.30474L21.1169 4.59939C21.1186 4.60467 21.1198 4.61008 21.1206 4.61555C21.2697 5.31089 21.4191 6.00623 21.5686 6.70157C21.5795 6.75293 21.5798 6.80601 21.5693 6.85748C21.5589 6.90895 21.5379 6.95771 21.5078 7.00072C21.4776 7.04373 21.4389 7.08007 21.3941 7.10747C21.3493 7.13487 21.2993 7.15274 21.2473 7.15997H21.2431L21.1519 7.17248L21.0617 7.18448C20.7759 7.22168 20.4897 7.25644 20.2033 7.28878C19.639 7.3531 19.0739 7.40872 18.5079 7.45566C17.3831 7.54918 16.2562 7.61055 15.127 7.63975C14.5516 7.65505 13.9763 7.66217 13.4013 7.66113C11.1124 7.65933 8.82553 7.5263 6.55188 7.2627C6.30574 7.2335 6.05959 7.20221 5.81344 7.1704C6.00431 7.19491 5.67472 7.15162 5.60797 7.14224C5.45152 7.12033 5.29506 7.09756 5.13861 7.07392C4.61346 6.99517 4.09144 6.89817 3.56733 6.81317C2.9337 6.70887 2.32771 6.76102 1.75458 7.07392C1.28413 7.33136 0.903361 7.72614 0.663078 8.20558C0.415886 8.71665 0.342354 9.2731 0.231796 9.82224C0.121237 10.3714 -0.0508594 10.9622 0.0143284 11.526C0.154613 12.7427 1.00518 13.7314 2.22863 13.9525C3.37959 14.1611 4.5368 14.3301 5.69714 14.474C10.2552 15.0323 14.8601 15.0991 19.4325 14.6733C19.8048 14.6385 20.1767 14.6006 20.548 14.5596C20.6639 14.5468 20.7813 14.5602 20.8914 14.5987C21.0016 14.6372 21.1017 14.6998 21.1845 14.782C21.2673 14.8642 21.3307 14.9639 21.37 15.0737C21.4093 15.1836 21.4235 15.3009 21.4116 15.4169L21.2958 16.5423C21.0625 18.8164 20.8292 21.0903 20.596 23.3641C20.3526 25.7519 20.1077 28.1395 19.8612 30.5269C19.7916 31.1993 19.7221 31.8715 19.6526 32.5436C19.5858 33.2054 19.5764 33.888 19.4507 34.542C19.2526 35.5704 18.5564 36.2019 17.5405 36.433C16.6098 36.6448 15.659 36.756 14.7045 36.7646C13.6464 36.7704 12.5888 36.7234 11.5307 36.7292C10.4011 36.7354 9.01755 36.6311 8.1456 35.7905C7.37951 35.052 7.27365 33.8958 7.16935 32.8961C7.03028 31.5725 6.89243 30.2491 6.75579 28.9259L5.98918 21.568L5.49324 16.8072C5.48489 16.7285 5.47655 16.6508 5.46873 16.5715C5.40927 16.0036 5.0072 15.4477 4.37357 15.4764C3.83121 15.5004 3.21479 15.9614 3.27841 16.5715L3.64607 20.1011L4.40642 27.4021C4.62302 29.4759 4.8391 31.5501 5.05465 33.6247C5.09637 34.022 5.13548 34.4205 5.17929 34.8179C5.41762 36.9894 7.07599 38.1596 9.12967 38.4892C10.3291 38.6822 11.5578 38.7218 12.775 38.7416C14.3353 38.7667 15.9113 38.8267 17.4461 38.544C19.7203 38.1268 21.4267 36.6082 21.6702 34.2526C21.7398 33.5725 21.8093 32.8923 21.8788 32.2119C22.11 29.9618 22.3409 27.7115 22.5714 25.4611L23.3255 18.1079L23.6713 14.7379C23.6885 14.5708 23.759 14.4137 23.8725 14.2898C23.986 14.1659 24.1363 14.0819 24.3012 14.0501C24.9515 13.9233 25.5732 13.7069 26.0357 13.212C26.7721 12.424 26.9187 11.3967 26.6584 10.3609ZM2.19525 11.0879C2.20516 11.0832 2.18691 11.1682 2.17909 11.2079C2.17752 11.1479 2.18065 11.0947 2.19525 11.0879ZM2.25836 11.5761C2.26357 11.5724 2.27921 11.5933 2.29538 11.6183C2.27087 11.5953 2.25523 11.5781 2.25783 11.5761H2.25836ZM2.32041 11.6579C2.34284 11.696 2.35483 11.72 2.32041 11.6579V11.6579ZM2.44505 11.7591H2.44818C2.44818 11.7627 2.45392 11.7664 2.456 11.7701C2.45255 11.766 2.4487 11.7624 2.44453 11.7591H2.44505ZM24.271 11.6079C24.0373 11.83 23.6853 11.9333 23.3375 11.9849C19.4366 12.5638 15.479 12.8569 11.5354 12.7275C8.71299 12.6311 5.92035 12.3176 3.12613 11.9229C2.85234 11.8843 2.55561 11.8342 2.36735 11.6324C2.01273 11.2517 2.18691 10.4851 2.27921 10.0251C2.3637 9.60373 2.52536 9.04207 3.02653 8.9821C3.80878 8.89031 4.71724 9.22042 5.49115 9.33776C6.4229 9.47996 7.35813 9.59382 8.29683 9.67935C12.303 10.0444 16.3765 9.98755 20.3649 9.45354C21.0919 9.35584 21.8163 9.24233 22.538 9.11299C23.181 8.99774 23.8939 8.78132 24.2825 9.44728C24.5489 9.90098 24.5844 10.508 24.5432 11.0207C24.5305 11.244 24.4329 11.4541 24.2705 11.6079H24.271Z" fill="#0D0C22"></path>
</svg>
</a>
<a href="https://github.com/rightup" target="_blank" class="github-link" title="GitHub">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
</div>
</div>
</div>
</div> <!-- Close sidebar-content-wrapper -->
</aside>
<style>
/* GitHub and Coffee link styling */
.github-link, .coffee-link {
display: inline-flex;
align-items: center;
color: #d4d4d4;
text-decoration: none;
transition: color 0.2s, transform 0.2s;
}
.github-link:hover {
color: #4ec9b0;
transform: scale(1.1);
}
.coffee-link:hover {
color: #ff813f;
transform: scale(1.1);
}
.github-link svg, .coffee-link svg {
display: block;
}
/* Noise Floor Display Styling */
.noise-floor-sparkline-container {
width: 100%;
height: 56px;
margin-top: 8px;
background: none;
display: flex;
align-items: center;
justify-content: center;
}
#noise-floor-sparkline {
width: 220px;
height: 48px;
display: block;
background: none;
}
.noise-floor-display {
padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-md);
margin: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background: linear-gradient(135deg, rgba(78, 201, 176, 0.05) 0%, rgba(86, 156, 214, 0.05) 100%);
}
.noise-floor-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #8b949e;
margin-bottom: var(--spacing-xs);
}
.noise-floor-value {
display: flex;
align-items: baseline;
gap: 0.25rem;
margin-bottom: var(--spacing-sm);
}
#noise-floor-value {
font-size: 2rem;
font-weight: 300;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
color: #4ec9b0;
line-height: 1;
letter-spacing: -0.02em;
transition: color 0.3s ease;
}
.noise-floor-unit {
font-size: 0.875rem;
font-weight: 500;
color: #8b949e;
margin-left: 0.125rem;
}
/* Control button styling */
.control-buttons {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.control-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: #d4d4d4;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
}
.control-btn.control-btn-active {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.3);
color: #10b981;
}
.control-btn.control-btn-warning {
background: rgba(217, 119, 6, 0.1);
border-color: rgba(217, 119, 6, 0.3);
color: #d97706;
}
.control-btn .icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.control-label {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
}
.control-title {
font-weight: 500;
font-size: 0.875rem;
}
.control-value {
font-size: 0.75rem;
opacity: 0.8;
}
/* Duty cycle stats styling */
.duty-cycle-stats {
margin-bottom: var(--spacing-md);
}
.duty-cycle-bar-container {
width: 100%;
height: 4px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
margin-bottom: var(--spacing-xs);
}
.duty-cycle-bar {
height: 100%;
background-color: #4ade80;
border-radius: 2px;
transition: all 0.3s ease;
width: 0%;
}
.duty-cycle-text {
color: #8b949e;
font-size: 0.75rem;
}
/* Status and version badges */
.status-badge, .version-badge {
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.status-badge {
background-color: #10b981;
color: #ffffff;
}
.version-badge {
background-color: rgba(255, 255, 255, 0.1);
color: #8b949e;
}
/* Color states for noise floor value */
#noise-floor-value.excellent {
color: #4ade80;
}
#noise-floor-value.good {
color: #4ec9b0;
}
#noise-floor-value.moderate {
color: #dcdcaa;
}
#noise-floor-value.poor {
color: #f48771;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.noise-floor-display {
padding: var(--spacing-md);
}
#noise-floor-value {
font-size: 1.75rem;
}
}
</style>
<script>
// Mobile menu toggle
const menuToggle = document.getElementById('menu-toggle');
const sidebar = document.getElementById('sidebar');
if (menuToggle) {
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('menu-open');
// Prevent body scroll when menu is open on mobile
if (sidebar.classList.contains('menu-open')) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
});
// Close menu when clicking nav items
const navItems = sidebar.querySelectorAll('.nav-item, .nav-action');
navItems.forEach(item => {
item.addEventListener('click', () => {
sidebar.classList.remove('menu-open');
document.body.style.overflow = '';
});
});
}
// Update footer stats periodically
// --- Noise Floor Sparkline State ---
const noiseFloorHistory = [];
const noiseFloorHistoryMax = 40; // Number of points in sparkline
function renderNoiseFloorSparkline() {
const svg = document.getElementById('noise-floor-sparkline');
if (!svg) return;
const w = 220, h = 48;
const minDbm = -120, maxDbm = -80;
if (noiseFloorHistory.length < 2) {
svg.innerHTML = '';
return;
}
// Map dBm to Y (inverted: lower dBm = higher Y)
const points = noiseFloorHistory.map((v, i) => {
const x = (i / (noiseFloorHistoryMax - 1)) * w;
let y = h - ((v - minDbm) / (maxDbm - minDbm)) * h;
y = Math.max(0, Math.min(h, y));
return [x, y];
});
// Build SVG path
let path = `M${points[0][0]},${points[0][1]}`;
for (let i = 1; i < points.length; i++) {
path += ` L${points[i][0]},${points[i][1]}`;
}
svg.innerHTML = `<path d="${path}" fill="none" stroke="#4ec9b0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />`;
}
function updateFooterStats() {
fetch('/api/stats')
.then(r => r.json())
.then(data => {
// Update noise floor
const noiseFloor = data.noise_floor_dbm;
const noiseFloorValue = document.getElementById('noise-floor-value');
if (noiseFloor !== null && noiseFloor !== undefined) {
// Display noise floor value
noiseFloorValue.textContent = noiseFloor.toFixed(1);
// Color code based on noise level
noiseFloorValue.className = '';
if (noiseFloor < -115) {
noiseFloorValue.classList.add('excellent'); // Very quiet
} else if (noiseFloor < -105) {
noiseFloorValue.classList.add('good'); // Normal
} else if (noiseFloor < -95) {
noiseFloorValue.classList.add('moderate'); // Moderate noise
} else {
noiseFloorValue.classList.add('poor'); // High noise
}
// --- Sparkline update ---
noiseFloorHistory.push(noiseFloor);
if (noiseFloorHistory.length > noiseFloorHistoryMax) {
noiseFloorHistory.shift();
}
renderNoiseFloorSparkline();
} else {
noiseFloorValue.textContent = '--';
noiseFloorValue.className = '';
// Sparkline: clear if no data
noiseFloorHistory.length = 0;
renderNoiseFloorSparkline();
}
// Update version badges
if (data.version) {
document.getElementById('repeater-version-badge').textContent = 'R:v' + data.version;
}
if (data.core_version) {
document.getElementById('core-version-badge').textContent = 'C:v' + data.core_version;
}
// Update duty cycle
const utilization = data.utilization_percent || 0;
const maxPercent = data.config?.duty_cycle?.max_airtime_percent || 10;
document.getElementById('duty-utilization').textContent = utilization.toFixed(1) + '%';
document.getElementById('duty-max').textContent = maxPercent.toFixed(1) + '%';
// Update progress bar
const percentage = Math.min((utilization / maxPercent) * 100, 100);
const bar = document.getElementById('duty-cycle-bar');
bar.style.width = percentage + '%';
// Set minimum width so it's always visible
if (percentage === 0) {
bar.style.width = '100%';
bar.style.backgroundColor = '#4ade80'; // Green - plenty of capacity
} else {
// Color code the bar based on usage
if (percentage > 90) {
bar.style.backgroundColor = '#f48771'; // Red - critical
} else if (percentage > 70) {
bar.style.backgroundColor = '#dcdcaa'; // Yellow - warning
} else {
bar.style.backgroundColor = '#4ade80'; // Green - good
}
}
// Update control button states from config
const mode = data.config?.repeater?.mode || 'forward';
const dutyCycleEnabled = data.config?.duty_cycle?.enforcement_enabled !== false;
// Update status badge based on mode and duty cycle
const statusBadge = document.getElementById('status-badge');
if (mode === 'monitor') {
statusBadge.textContent = 'Monitor Mode';
statusBadge.style.backgroundColor = '#d97706'; // Orange for monitor
statusBadge.style.color = '#ffffff'; // White text
statusBadge.title = 'Monitoring only - not forwarding packets';
} else if (!dutyCycleEnabled) {
statusBadge.textContent = 'No Limits';
statusBadge.style.backgroundColor = '#dc2626'; // Red for unlimited
statusBadge.style.color = '#ffffff'; // White text
statusBadge.title = 'Forwarding without duty cycle enforcement';
} else {
statusBadge.textContent = 'Active';
statusBadge.style.backgroundColor = '#10b981'; // Green for normal
statusBadge.style.color = '#ffffff'; // White text
statusBadge.title = 'Forwarding with duty cycle enforcement';
}
document.getElementById('mode-status').textContent =
mode.charAt(0).toUpperCase() + mode.slice(1);
document.getElementById('duty-cycle-status').textContent =
dutyCycleEnabled ? 'Enabled' : 'Disabled';
// Update button states
const modeBtn = document.getElementById('mode-toggle-btn');
const dutyBtn = document.getElementById('duty-cycle-toggle-btn');
if (mode === 'monitor') {
modeBtn.classList.add('control-btn-warning');
modeBtn.classList.remove('control-btn-active');
} else {
modeBtn.classList.add('control-btn-active');
modeBtn.classList.remove('control-btn-warning');
}
if (!dutyCycleEnabled) {
dutyBtn.classList.add('control-btn-warning');
dutyBtn.classList.remove('control-btn-active');
} else {
dutyBtn.classList.add('control-btn-active');
dutyBtn.classList.remove('control-btn-warning');
}
// Update timestamp
document.getElementById('footer-update-time').textContent = new Date().toLocaleTimeString();
})
.catch(e => console.error('Error updating footer stats:', e));
}
// Handle Send Advert button - works on all pages
function sendAdvert() {
const btn = document.getElementById('send-advert-btn');
if (!btn) return;
const icon = btn.querySelector('.icon');
const iconHTML = icon ? icon.outerHTML : '';
btn.disabled = true;
btn.innerHTML = iconHTML + 'Sending...';
fetch('/api/send_advert', {
method: 'POST'
})
.then(r => r.json())
.then(data => {
if (data.success) {
btn.innerHTML = iconHTML + 'Sent!';
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
} else {
btn.innerHTML = iconHTML + 'Error';
console.error('Failed to send advert:', data.error);
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
}
})
.catch(e => {
console.error('Error sending advert:', e);
btn.innerHTML = iconHTML + 'Error';
setTimeout(() => {
btn.innerHTML = iconHTML + 'Send Advert';
btn.disabled = false;
}, 2000);
});
}
// Mode toggle handler
function toggleMode() {
const btn = document.getElementById('mode-toggle-btn');
const statusText = document.getElementById('mode-status');
const currentMode = statusText.textContent.toLowerCase();
const newMode = currentMode === 'forward' ? 'monitor' : 'forward';
btn.disabled = true;
statusText.textContent = 'Changing...';
fetch('/api/set_mode', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: newMode })
})
.then(r => r.json())
.then(data => {
if (data.success) {
statusText.textContent = newMode.charAt(0).toUpperCase() + newMode.slice(1);
updateFooterStats(); // Refresh to get updated state
} else {
statusText.textContent = currentMode.charAt(0).toUpperCase() + currentMode.slice(1);
alert('Failed to change mode: ' + (data.error || 'Unknown error'));
}
})
.catch(e => {
console.error('Error toggling mode:', e);
statusText.textContent = currentMode.charAt(0).toUpperCase() + currentMode.slice(1);
alert('Failed to change mode');
})
.finally(() => {
btn.disabled = false;
});
}
// Duty cycle toggle handler
function toggleDutyCycle() {
const btn = document.getElementById('duty-cycle-toggle-btn');
const statusText = document.getElementById('duty-cycle-status');
const currentEnabled = statusText.textContent === 'Enabled';
const newEnabled = !currentEnabled;
btn.disabled = true;
statusText.textContent = 'Changing...';
fetch('/api/set_duty_cycle', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newEnabled })
})
.then(r => r.json())
.then(data => {
if (data.success) {
statusText.textContent = newEnabled ? 'Enabled' : 'Disabled';
updateFooterStats(); // Refresh to get updated state
} else {
statusText.textContent = currentEnabled ? 'Enabled' : 'Disabled';
alert('Failed to change duty cycle: ' + (data.error || 'Unknown error'));
}
})
.catch(e => {
console.error('Error toggling duty cycle:', e);
statusText.textContent = currentEnabled ? 'Enabled' : 'Disabled';
alert('Failed to change duty cycle');
})
.finally(() => {
btn.disabled = false;
});
}
// Update immediately and then every 5 seconds
document.addEventListener('DOMContentLoaded', () => {
updateFooterStats();
setInterval(updateFooterStats, 5000);
// Attach toggle button handlers
const modeBtn = document.getElementById('mode-toggle-btn');
const dutyBtn = document.getElementById('duty-cycle-toggle-btn');
const sendAdvertBtn = document.getElementById('send-advert-btn');
if (modeBtn) {
modeBtn.addEventListener('click', toggleMode);
}
if (dutyBtn) {
dutyBtn.addEventListener('click', toggleDutyCycle);
}
// Attach send advert button handler - works on all pages
if (sendAdvertBtn) {
sendAdvertBtn.addEventListener('click', sendAdvert);
}
});
// Add data-label attributes to table cells for mobile display
function initMobileTableLabels() {
const tables = document.querySelectorAll('table');
tables.forEach(table => {
const headers = [];
// Get all header text
table.querySelectorAll('thead th').forEach(th => {
headers.push(th.textContent.trim());
});
// Add data-label to each cell
table.querySelectorAll('tbody td').forEach((td, index) => {
const headerIndex = index % headers.length;
if (headers[headerIndex]) {
td.setAttribute('data-label', headers[headerIndex]);
}
});
});
}
document.addEventListener('DOMContentLoaded', initMobileTableLabels);
</script>