feat: Repeaters card with per-step tune status

This commit is contained in:
2026-06-14 16:16:00 -04:00
parent 878e85aefd
commit 66b368fb87
+52
View File
@@ -151,6 +151,16 @@ input[type=range]::-moz-range-thumb{width:22px;height:26px;border-radius:5px;
text-transform:uppercase}
.reveal{animation:rise .7s both}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.rblist{display:flex;flex-direction:column;gap:6px;margin-top:10px;max-height:300px;overflow-y:auto}
.rbrow{display:flex;flex-direction:column;gap:3px;
background:#0c0b08;border:1px solid #3a352b;border-radius:8px;padding:9px 11px;cursor:pointer}
.rbrow:active{transform:translateY(1px)}
.rbrow .cs{font-family:'Saira Condensed',sans-serif;font-weight:800;color:var(--ink);letter-spacing:.06em}
.rbrow .dv{color:var(--accent-hi);border:1px solid var(--accent);border-radius:10px;
padding:1px 6px;font-size:9px;letter-spacing:.1em;margin-left:6px}
.rbrow .meta{color:var(--ink-dim);font-size:11px}
.rbrow .steps{font-family:'Chivo Mono';font-size:10px;letter-spacing:.04em;color:var(--ink-dim);min-height:12px}
.rbrow .steps .ok{color:var(--led-on)} .rbrow .steps .no{color:var(--red)}
</style>
</head>
<body>
@@ -198,6 +208,13 @@ input[type=range]::-moz-range-thumb{width:22px;height:26px;border-radius:5px;
<div class="fader"><input id="sql" type="range" min="0" max="255"><span id="sqlv" class="val">--</span></div>
</div>
</div>
<div class="card reveal" style="animation-delay:.40s">
<div class="label">Repeaters · nearest first</div>
<div class="freqset">
<input id="rbq" type="text" inputmode="search" placeholder="search callsign / city">
</div>
<div id="rblist" class="rblist"></div>
</div>
</div>
<div id="err" class="err"></div>
</div>
@@ -272,6 +289,41 @@ wireFader('sql','/api/sql','#sqlv');
$('#fset').onclick=()=>{const v=parseFloat($('#finp').value);if(!isNaN(v))post('/api/freq',{mhz:v});};
$('#finp').addEventListener('keydown',e=>{if(e.key==='Enter')$('#fset').click();});
const rbq=$('#rbq'), rblist=$('#rblist');
function stepHTML(steps){
if(!steps) return '';
return Object.entries(steps).map(([k,v])=>
`<span class="${v?'ok':'no'}">${k} ${v?'✓':'✗'}</span>`).join(' ');
}
async function loadRepeaters(){
try{
const r=await fetch('/api/repeaters?limit=40&q='+encodeURIComponent(rbq.value||''));
const j=await r.json(); if(!j.ok) return;
rblist.innerHTML='';
j.repeaters.forEach(rep=>{
const row=document.createElement('div'); row.className='rbrow';
const dup=rep.duplex==='simplex'?'':(' '+rep.duplex);
const tone=rep.tone_hz?(' · '+rep.tone_hz):'';
row.innerHTML=
`<div><span class="cs">${rep.callsign||'—'}</span>`+
`${rep.mode==='DV'?'<span class="dv">DV</span>':''}</div>`+
`<div class="meta">${rep.location||''} · ${(rep.output_mhz||0).toFixed(3)}${dup}${tone}</div>`+
`<div class="steps"></div>`;
row.onclick=()=>tuneRepeater(rep,row);
rblist.appendChild(row);
});
}catch(e){/* offline: leave list as-is */}
}
async function tuneRepeater(rep,row){
const s=row.querySelector('.steps'); s.textContent='tuning…';
try{
const r=await fetch('/api/tune-repeater',{method:'POST',
headers:{'Content-Type':'application/json'},body:JSON.stringify(rep)});
const j=await r.json(); s.innerHTML=stepHTML(j.steps); refresh();
}catch(e){ s.innerHTML='<span class="no">send failed</span>'; }
}
let rbt; rbq.addEventListener('input',()=>{clearTimeout(rbt);rbt=setTimeout(loadRepeaters,250);});
loadRepeaters();
refresh();
setInterval(refresh,900);
</script>