feat: Repeaters card with per-step tune status
This commit is contained in:
+52
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user