From 66b368fb873eb1705eae0fd8825d6de4fece64d5 Mon Sep 17 00:00:00 2001 From: Seth Freiberg Date: Sun, 14 Jun 2026 16:16:00 -0400 Subject: [PATCH] feat: Repeaters card with per-step tune status --- panel.html | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/panel.html b/panel.html index ce8502c..cc408dc 100644 --- a/panel.html +++ b/panel.html @@ -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)} @@ -198,6 +208,13 @@ input[type=range]::-moz-range-thumb{width:22px;height:26px;border-radius:5px;
--
+
+
Repeaters · nearest first
+
+ +
+
+
@@ -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])=> + `${k} ${v?'✓':'✗'}`).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= + `
${rep.callsign||'—'}`+ + `${rep.mode==='DV'?'DV':''}
`+ + `
${rep.location||''} · ${(rep.output_mhz||0).toFixed(3)}${dup}${tone}
`+ + `
`; + 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='send failed'; } +} +let rbt; rbq.addEventListener('input',()=>{clearTimeout(rbt);rbt=setTimeout(loadRepeaters,250);}); +loadRepeaters(); refresh(); setInterval(refresh,900);