diff --git a/cmd/server/main.go b/cmd/server/main.go index 7bd23c0..89c502e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,9 @@ package main import ( + "embed" "fmt" + "io/fs" "log" "net/http" "os" @@ -13,6 +15,9 @@ import ( "git.rouggy.com/rouggy/ShackMaster/internal/config" ) +//go:embed web/dist +var webFS embed.FS + func main() { log.Println("Starting ShackMaster server...") @@ -39,10 +44,17 @@ func main() { log.Fatalf("Failed to start device manager: %v", err) } - // Create HTTP server + // Create HTTP server with embedded files server := api.NewServer(deviceManager, hub, cfg) mux := server.SetupRoutes() + // Serve embedded static files + distFS, err := fs.Sub(webFS, "web/dist") + if err != nil { + log.Fatalf("Failed to access embedded files: %v", err) + } + mux.Handle("/", http.FileServer(http.FS(distFS))) + // Setup HTTP server addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) httpServer := &http.Server{ diff --git a/cmd/server/web/dist/assets/index-DY7RBkJT.js b/cmd/server/web/dist/assets/index-DY7RBkJT.js new file mode 100644 index 0000000..d8ac3be --- /dev/null +++ b/cmd/server/web/dist/assets/index-DY7RBkJT.js @@ -0,0 +1,8 @@ +var is=Object.defineProperty;var cs=(e,l,n)=>l in e?is(e,l,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[l]=n;var Lt=(e,l,n)=>cs(e,typeof l!="symbol"?l+"":l,n);(function(){const l=document.createElement("link").relList;if(l&&l.supports&&l.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const h of o.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&r(h)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();function Fe(){}function ss(e){return e()}function $t(){return Object.create(null)}function it(e){e.forEach(ss)}function ls(e){return typeof e=="function"}function ft(e,l){return e!=e?l==l:e!==l||e&&typeof e=="object"||typeof e=="function"}function ds(e){return Object.keys(e).length===0}function t(e,l){e.appendChild(l)}function Oe(e,l,n){e.insertBefore(l,n||null)}function ge(e){e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,l){for(let n=0;ne.removeEventListener(l,n,r)}function s(e,l,n){n==null?e.removeAttribute(l):e.getAttribute(l)!==n&&e.setAttribute(l,n)}function ns(e){return e===""?null:+e}function us(e){return Array.from(e.childNodes)}function N(e,l){l=""+l,e.data!==l&&(e.data=l)}function Pt(e,l){e.value=l??""}function le(e,l,n,r){n==null?e.style.removeProperty(l):e.style.setProperty(l,n,"")}function Jt(e,l,n){for(let r=0;re.indexOf(r)===-1?l.push(r):n.push(r)),n.forEach(r=>r()),jt=l}const Et=new Set;let gs;function gt(e,l){e&&e.i&&(Et.delete(e),e.i(l))}function Ot(e,l,n,r){if(e&&e.o){if(Et.has(e))return;Et.add(e),gs.c.push(()=>{Et.delete(e)}),e.o(l)}}function yt(e){return(e==null?void 0:e.length)!==void 0?e:Array.from(e)}function Mt(e){e&&e.c()}function bt(e,l,n){const{fragment:r,after_update:i}=e.$$;r&&r.m(l,n),Bt(()=>{const o=e.$$.on_mount.map(ss).filter(ls);e.$$.on_destroy?e.$$.on_destroy.push(...o):it(o),e.$$.on_mount=[]}),i.forEach(Bt)}function _t(e,l){const n=e.$$;n.fragment!==null&&(ws(n.after_update),it(n.on_destroy),n.fragment&&n.fragment.d(l),n.on_destroy=n.fragment=null,n.ctx=[])}function bs(e,l){e.$$.dirty[0]===-1&&(wt.push(e),hs(),e.$$.dirty.fill(0)),e.$$.dirty[l/31|0]|=1<{const y=_.length?_[0]:w;return c.ctx&&i(c.ctx[v],c.ctx[v]=y)&&(!c.skip_bound&&c.bound[v]&&c.bound[v](y),j&&bs(e,v)),w}):[],c.update(),j=!0,it(c.before_update),c.fragment=r?r(c.ctx):!1,l.target){if(l.hydrate){const v=us(l.target);c.fragment&&c.fragment.l(v),v.forEach(ge)}else c.fragment&&c.fragment.c();l.intro&>(e.$$.fragment),bt(e,l.target,l.anchor),rs()}At(m)}class kt{constructor(){Lt(this,"$$");Lt(this,"$$set")}$destroy(){_t(this,1),this.$destroy=Fe}$on(l,n){if(!ls(n))return Fe;const r=this.$$.callbacks[l]||(this.$$.callbacks[l]=[]);return r.push(n),()=>{const i=r.indexOf(n);i!==-1&&r.splice(i,1)}}$set(l){this.$$set&&!ds(l)&&(this.$$.skip_bound=!0,this.$$set(l),this.$$.skip_bound=!1)}}const _s="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(_s);const mt=[];function zt(e,l=Fe){let n;const r=new Set;function i(f){if(ft(e,f)&&(e=f,n)){const m=!mt.length;for(const c of r)c[1](),mt.push(c,e);if(m){for(let c=0;c{r.delete(c),r.size===0&&n&&(n(),n=null)}}return{set:i,update:o,subscribe:h}}const Rt=zt(!1),os=zt(null),js=zt(null);class ys{constructor(){this.ws=null,this.reconnectTimeout=null,this.reconnectDelay=3e3}connect(){const n=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`;try{this.ws=new WebSocket(n),this.ws.onopen=()=>{console.log("WebSocket connected"),Rt.set(!0)},this.ws.onmessage=r=>{try{const i=JSON.parse(r.data);i.type==="update"&&(console.log("System status updated:",i.data),os.set(i.data),js.set(new Date(i.timestamp)))}catch(i){console.error("Error parsing message:",i)}},this.ws.onerror=r=>{console.error("WebSocket error:",r)},this.ws.onclose=()=>{console.log("WebSocket disconnected"),Rt.set(!1),this.scheduleReconnect()}}catch(r){console.error("Error creating WebSocket:",r),this.scheduleReconnect()}}scheduleReconnect(){this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.reconnectTimeout=setTimeout(()=>{console.log("Attempting to reconnect..."),this.connect()},this.reconnectDelay)}send(l){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(l))}disconnect(){this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.ws&&this.ws.close()}}const xt=new ys,Cs="/api";async function de(e,l={}){try{const n=await fetch(`${Cs}${e}`,{...l,headers:{"Content-Type":"application/json",...l.headers}});if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);return await n.json()}catch(n){throw console.error("API request failed:",n),n}}const Ee={getStatus:()=>de("/status"),getConfig:()=>de("/config"),webswitch:{relayOn:e=>de(`/webswitch/relay/on?relay=${e}`,{method:"POST"}),relayOff:e=>de(`/webswitch/relay/off?relay=${e}`,{method:"POST"}),allOn:()=>de("/webswitch/all/on",{method:"POST"}),allOff:()=>de("/webswitch/all/off",{method:"POST"})},rotator:{move:(e,l)=>de("/rotator/move",{method:"POST",body:JSON.stringify({rotator:e,azimuth:l})}),cw:e=>de(`/rotator/cw?rotator=${e}`,{method:"POST"}),ccw:e=>de(`/rotator/ccw?rotator=${e}`,{method:"POST"}),stop:()=>de("/rotator/stop",{method:"POST"})},tuner:{setOperate:e=>de("/tuner/operate",{method:"POST",body:JSON.stringify({value:e})}),setBypass:e=>de("/tuner/bypass",{method:"POST",body:JSON.stringify({value:e})}),autoTune:()=>de("/tuner/autotune",{method:"POST"})},antenna:{selectAntenna:(e,l)=>de("/antenna/select",{method:"POST",body:JSON.stringify({port:e,antenna:l})}),reboot:()=>de("/antenna/reboot",{method:"POST"})},power:{setFanMode:e=>de("/power/fanmode",{method:"POST",body:JSON.stringify({mode:e})}),setOperate:e=>de("/power/operate",{method:"POST",body:JSON.stringify({value:e})})},rotator:{setHeading:e=>de("/rotator/heading",{method:"POST",body:JSON.stringify({heading:e})}),rotateCW:()=>de("/rotator/cw",{method:"POST"}),rotateCCW:()=>de("/rotator/ccw",{method:"POST"}),stop:()=>de("/rotator/stop",{method:"POST"})}};function Vt(e,l,n){var h;const r=e.slice();r[10]=l[n];const i=r[1].find(function(...m){return e[9](r[10],...m)});r[11]=i;const o=((h=r[11])==null?void 0:h.state)||!1;return r[12]=o,r}function Yt(e){let l,n,r,i,o,h,f=e[12]?"ON":"OFF",m,c,j,v,w,_,y,g;function d(){return e[8](e[10])}return{c(){l=a("div"),n=a("div"),r=a("div"),i=a("div"),i.textContent=`${e[3][e[10]]}`,o=u(),h=a("div"),m=b(f),c=u(),j=a("button"),v=a("div"),v.innerHTML='
',_=u(),s(i,"class","relay-name svelte-z2csmj"),s(h,"class","relay-status svelte-z2csmj"),s(r,"class","relay-details svelte-z2csmj"),s(n,"class","relay-info svelte-z2csmj"),s(v,"class","toggle-track svelte-z2csmj"),s(j,"class","relay-toggle svelte-z2csmj"),j.disabled=w=e[0][e[10]],P(j,"active",e[12]),P(j,"loading",e[0][e[10]]),s(l,"class","relay-card svelte-z2csmj"),P(l,"relay-on",e[12])},m(T,C){Oe(T,l,C),t(l,n),t(n,r),t(r,i),t(r,o),t(r,h),t(h,m),t(l,c),t(l,j),t(j,v),t(l,_),y||(g=Ne(j,"click",d),y=!0)},p(T,C){e=T,C&2&&f!==(f=e[12]?"ON":"OFF")&&N(m,f),C&1&&w!==(w=e[0][e[10]])&&(j.disabled=w),C&2&&P(j,"active",e[12]),C&1&&P(j,"loading",e[0][e[10]]),C&2&&P(l,"relay-on",e[12])},d(T){T&&ge(l),y=!1,g()}}}function ks(e){let l,n,r,i,o,h,f,m,c,j,v,w,_,y,g,d=yt([1,2,3,4,5]),T=[];for(let C=0;C<5;C+=1)T[C]=Yt(Vt(e,d,C));return{c(){l=a("div"),n=a("div"),r=a("h2"),r.textContent="WebSwitch",i=u(),o=a("span"),h=u(),f=a("div"),m=a("div");for(let C=0;C<5;C+=1)T[C].c();c=u(),j=a("div"),v=a("button"),v.innerHTML=` + ALL ON`,w=u(),_=a("button"),_.innerHTML=` + ALL OFF`,s(r,"class","svelte-z2csmj"),s(o,"class","status-dot svelte-z2csmj"),P(o,"disconnected",!e[2]),s(n,"class","card-header svelte-z2csmj"),s(m,"class","relays svelte-z2csmj"),s(v,"class","control-btn all-on svelte-z2csmj"),s(_,"class","control-btn all-off svelte-z2csmj"),s(j,"class","controls svelte-z2csmj"),s(f,"class","metrics svelte-z2csmj"),s(l,"class","card svelte-z2csmj")},m(C,p){Oe(C,l,p),t(l,n),t(n,r),t(n,i),t(n,o),t(l,h),t(l,f),t(f,m);for(let O=0;O<5;O+=1)T[O]&&T[O].m(m,null);t(f,c),t(f,j),t(j,v),t(j,w),t(j,_),y||(g=[Ne(v,"click",e[5]),Ne(_,"click",e[6])],y=!0)},p(C,[p]){if(p&4&&P(o,"disconnected",!C[2]),p&27){d=yt([1,2,3,4,5]);let O;for(O=0;O<5;O+=1){const I=Vt(C,d,O);T[O]?T[O].p(I,p):(T[O]=Yt(I),T[O].c(),T[O].m(m,null))}for(;O<5;O+=1)T[O].d(1)}},i:Fe,o:Fe,d(C){C&&ge(l),Wt(T,C),y=!1,it(g)}}}function Ts(e,l,n){let r,i,{status:o}=l;const h={1:"Power Supply",2:"PGXL",3:"TGXL",4:"Flex Radio Start",5:"Reserve"};let f={};async function m(_){const y=r.find(d=>d.number===_),g=(y==null?void 0:y.state)||!1;n(0,f[_]=!0,f);try{g?await Ee.webswitch.relayOff(_):await Ee.webswitch.relayOn(_)}catch(d){console.error("Failed to toggle relay:",d),alert("Failed to control relay")}finally{n(0,f[_]=!1,f)}}async function c(){try{await Ee.webswitch.allOn()}catch(_){console.error("Failed to turn all on:",_)}}async function j(){try{await Ee.webswitch.allOff()}catch(_){console.error("Failed to turn all off:",_)}}const v=_=>m(_),w=(_,y)=>y.number===_;return e.$$set=_=>{"status"in _&&n(7,o=_.status)},e.$$.update=()=>{e.$$.dirty&128&&n(1,r=(o==null?void 0:o.relays)||[]),e.$$.dirty&128&&n(2,i=(o==null?void 0:o.connected)||!1)},[f,r,i,h,m,c,j,o,v,w]}class Ss extends kt{constructor(l){super(),Ct(this,l,Ts,ks,ft,{status:7})}}function Fs(e){let l;return{c(){l=a("span"),l.textContent="High!",s(l,"class","status-text danger svelte-utvwj6")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Os(e){let l;return{c(){l=a("span"),l.textContent="Caution",s(l,"class","status-text warning svelte-utvwj6")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Ms(e){let l;return{c(){l=a("span"),l.textContent="Good",s(l,"class","status-text ok svelte-utvwj6")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Ps(e){let l;return{c(){l=a("span"),l.textContent="Excellent",s(l,"class","status-text good svelte-utvwj6")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function As(e){let l,n,r,i,o,h,f,m,c,j,v,w,_,y,g=e[1].toFixed(0)+"",d,T,C,p,O,I,F,E,J,X,re,K,A,Z,Q=e[2].toFixed(2)+"",ne,be,z,B,k,U,S,W,he,oe=e[3].toFixed(0)+"",$e,ue,Ge,ae,He,me,H,x,_e,ee,Me=e[12].toFixed(0)+"",fe,Re,je,Pe,ye,ce,pe,xe,te,V,R,ve,q,Ce=e[15].toFixed(0)+"",Le,ct,ke,qe,nt,Je,We=e[14].toFixed(1)+"",Ie,De,Be,Ve,tt,Te,Ye=e[13].toFixed(1)+"",Ue,Ke,Ae,we,Se,ze,L,se,st,at,Ze,lt,dt,Qe,ie,rt,ut,Tt,Xe,ot,M,G,pt,St;function Ft(D,$){return D[2]<1.5?Ps:D[2]<2?Ms:D[2]<3?Os:Fs}let vt=Ft(e),et=vt(e);return{c(){l=a("div"),n=a("div"),r=a("h2"),r.textContent="Power Genius XL",i=u(),o=a("div"),h=a("button"),f=b(e[7]),m=u(),c=a("span"),j=u(),v=a("div"),w=a("div"),_=a("div"),y=a("div"),d=b(g),T=a("span"),T.textContent="W",C=u(),p=a("div"),p.textContent="Forward Power",O=u(),I=a("div"),F=a("div"),E=a("div"),J=u(),X=a("div"),X.innerHTML="0 1000 2000",re=u(),K=a("div"),A=a("div"),Z=a("div"),ne=b(Q),be=u(),z=a("div"),z.textContent="SWR",B=u(),k=a("div"),et.c(),U=u(),S=a("div"),W=a("div"),he=a("div"),$e=b(oe),ue=b("°"),Ge=u(),ae=a("div"),ae.textContent="PA Temp",He=u(),me=a("div"),H=a("div"),x=u(),_e=a("div"),ee=a("div"),fe=b(Me),Re=b("°"),je=u(),Pe=a("div"),Pe.textContent="HL Temp",ye=u(),ce=a("div"),pe=a("div"),xe=u(),te=a("div"),V=a("div"),R=a("div"),R.textContent="VAC",ve=u(),q=a("div"),Le=b(Ce),ct=u(),ke=a("div"),qe=a("div"),qe.textContent="VDD",nt=u(),Je=a("div"),Ie=b(We),De=u(),Be=a("div"),Ve=a("div"),Ve.textContent="ID Peak",tt=u(),Te=a("div"),Ue=b(Ye),Ke=u(),Ae=a("div"),we=a("div"),Se=a("span"),Se.textContent="Band A",ze=u(),L=a("span"),se=b(e[10]),st=u(),at=a("div"),Ze=a("span"),Ze.textContent="Band B",lt=u(),dt=a("span"),Qe=b(e[9]),ie=u(),rt=a("div"),ut=a("label"),ut.textContent="Fan Mode",Tt=u(),Xe=a("select"),ot=a("option"),ot.textContent="Standard",M=a("option"),M.textContent="Contest",G=a("option"),G.textContent="Broadcast",s(r,"class","svelte-utvwj6"),s(h,"class","state-badge svelte-utvwj6"),P(h,"idle",e[0]==="IDLE"),P(h,"transmit",e[0].includes("TRANSMIT")),s(c,"class","status-dot svelte-utvwj6"),P(c,"disconnected",!e[8]),s(o,"class","header-right svelte-utvwj6"),s(n,"class","card-header svelte-utvwj6"),s(T,"class","unit svelte-utvwj6"),s(y,"class","power-value svelte-utvwj6"),s(p,"class","power-label svelte-utvwj6"),s(_,"class","power-main svelte-utvwj6"),s(E,"class","power-bar-glow svelte-utvwj6"),s(F,"class","power-bar-fill svelte-utvwj6"),le(F,"width",e[4]+"%"),s(X,"class","power-scale svelte-utvwj6"),s(I,"class","power-bar svelte-utvwj6"),s(w,"class","power-display svelte-utvwj6"),s(Z,"class","swr-value svelte-utvwj6"),s(z,"class","swr-label svelte-utvwj6"),s(A,"class","swr-circle svelte-utvwj6"),le(A,"--swr-color",e[5]),s(k,"class","swr-status svelte-utvwj6"),s(K,"class","swr-container svelte-utvwj6"),s(he,"class","temp-value svelte-utvwj6"),le(he,"color",e[6]),s(ae,"class","temp-label svelte-utvwj6"),s(H,"class","temp-mini-fill svelte-utvwj6"),le(H,"width",e[3]/80*100+"%"),le(H,"background",e[6]),s(me,"class","temp-mini-bar svelte-utvwj6"),s(W,"class","temp-item svelte-utvwj6"),s(ee,"class","temp-value svelte-utvwj6"),le(ee,"color",e[6]),s(Pe,"class","temp-label svelte-utvwj6"),s(pe,"class","temp-mini-fill svelte-utvwj6"),le(pe,"width",e[12]/80*100+"%"),le(pe,"background",e[6]),s(ce,"class","temp-mini-bar svelte-utvwj6"),s(_e,"class","temp-item svelte-utvwj6"),s(S,"class","temp-group svelte-utvwj6"),s(R,"class","param-label svelte-utvwj6"),s(q,"class","param-value svelte-utvwj6"),s(V,"class","param-box svelte-utvwj6"),s(qe,"class","param-label svelte-utvwj6"),s(Je,"class","param-value svelte-utvwj6"),s(ke,"class","param-box svelte-utvwj6"),s(Ve,"class","param-label svelte-utvwj6"),s(Te,"class","param-value svelte-utvwj6"),s(Be,"class","param-box svelte-utvwj6"),s(te,"class","params-grid svelte-utvwj6"),s(Se,"class","band-label svelte-utvwj6"),s(L,"class","band-value svelte-utvwj6"),s(we,"class","band-item svelte-utvwj6"),s(Ze,"class","band-label svelte-utvwj6"),s(dt,"class","band-value svelte-utvwj6"),s(at,"class","band-item svelte-utvwj6"),s(Ae,"class","band-display svelte-utvwj6"),s(ut,"class","control-label svelte-utvwj6"),ot.__value="STANDARD",Pt(ot,ot.__value),M.__value="CONTEST",Pt(M,M.__value),G.__value="BROADCAST",Pt(G,G.__value),s(Xe,"class","svelte-utvwj6"),s(rt,"class","fan-control svelte-utvwj6"),s(v,"class","metrics svelte-utvwj6"),s(l,"class","card svelte-utvwj6")},m(D,$){Oe(D,l,$),t(l,n),t(n,r),t(n,i),t(n,o),t(o,h),t(h,f),t(o,m),t(o,c),t(l,j),t(l,v),t(v,w),t(w,_),t(_,y),t(y,d),t(y,T),t(_,C),t(_,p),t(w,O),t(w,I),t(I,F),t(F,E),t(I,J),t(I,X),t(v,re),t(v,K),t(K,A),t(A,Z),t(Z,ne),t(A,be),t(A,z),t(K,B),t(K,k),et.m(k,null),t(v,U),t(v,S),t(S,W),t(W,he),t(he,$e),t(he,ue),t(W,Ge),t(W,ae),t(W,He),t(W,me),t(me,H),t(S,x),t(S,_e),t(_e,ee),t(ee,fe),t(ee,Re),t(_e,je),t(_e,Pe),t(_e,ye),t(_e,ce),t(ce,pe),t(v,xe),t(v,te),t(te,V),t(V,R),t(V,ve),t(V,q),t(q,Le),t(te,ct),t(te,ke),t(ke,qe),t(ke,nt),t(ke,Je),t(Je,Ie),t(te,De),t(te,Be),t(Be,Ve),t(Be,tt),t(Be,Te),t(Te,Ue),t(v,Ke),t(v,Ae),t(Ae,we),t(we,Se),t(we,ze),t(we,L),t(L,se),t(Ae,st),t(Ae,at),t(at,Ze),t(at,lt),t(at,dt),t(dt,Qe),t(v,ie),t(v,rt),t(rt,ut),t(rt,Tt),t(rt,Xe),t(Xe,ot),t(Xe,M),t(Xe,G),Jt(Xe,e[11]),pt||(St=[Ne(h,"click",e[17]),Ne(Xe,"change",e[19])],pt=!0)},p(D,[$]){$&128&&N(f,D[7]),$&1&&P(h,"idle",D[0]==="IDLE"),$&1&&P(h,"transmit",D[0].includes("TRANSMIT")),$&256&&P(c,"disconnected",!D[8]),$&2&&g!==(g=D[1].toFixed(0)+"")&&N(d,g),$&16&&le(F,"width",D[4]+"%"),$&4&&Q!==(Q=D[2].toFixed(2)+"")&&N(ne,Q),$&32&&le(A,"--swr-color",D[5]),vt!==(vt=Ft(D))&&(et.d(1),et=vt(D),et&&(et.c(),et.m(k,null))),$&8&&oe!==(oe=D[3].toFixed(0)+"")&&N($e,oe),$&64&&le(he,"color",D[6]),$&8&&le(H,"width",D[3]/80*100+"%"),$&64&&le(H,"background",D[6]),$&4096&&Me!==(Me=D[12].toFixed(0)+"")&&N(fe,Me),$&64&&le(ee,"color",D[6]),$&4096&&le(pe,"width",D[12]/80*100+"%"),$&64&&le(pe,"background",D[6]),$&32768&&Ce!==(Ce=D[15].toFixed(0)+"")&&N(Le,Ce),$&16384&&We!==(We=D[14].toFixed(1)+"")&&N(Ie,We),$&8192&&Ye!==(Ye=D[13].toFixed(1)+"")&&N(Ue,Ye),$&1024&&N(se,D[10]),$&512&&N(Qe,D[9]),$&2048&&Jt(Xe,D[11])},i:Fe,o:Fe,d(D){D&&ge(l),et.d(),pt=!1,it(St)}}}function Ns(e,l,n){let r,i,o,h,f,m,c,j,v,w,_,y,g,d,T,C,{status:p}=l;async function O(E){try{await Ee.power.setFanMode(E)}catch(J){console.error("Failed to set fan mode:",J),alert("Failed to set fan mode")}}async function I(){try{const E=v==="IDLE"?0:1;await Ee.power.setOperate(E)}catch(E){console.error("Failed to toggle operate:",E),alert("Failed to toggle operate mode")}}const F=E=>O(E.target.value);return e.$$set=E=>{"status"in E&&n(18,p=E.status)},e.$$.update=()=>{e.$$.dirty&262144&&n(1,r=(p==null?void 0:p.power_forward)||0),e.$$.dirty&262144&&p!=null&&p.power_reflected,e.$$.dirty&262144&&n(2,i=(p==null?void 0:p.swr)||1),e.$$.dirty&262144&&n(15,o=(p==null?void 0:p.voltage)||0),e.$$.dirty&262144&&n(14,h=(p==null?void 0:p.vdd)||0),e.$$.dirty&262144&&p!=null&&p.current,e.$$.dirty&262144&&n(13,f=(p==null?void 0:p.peak_current)||0),e.$$.dirty&262144&&n(3,m=(p==null?void 0:p.temperature)||0),e.$$.dirty&262144&&n(12,c=(p==null?void 0:p.harmonic_load_temp)||0),e.$$.dirty&262144&&n(11,j=(p==null?void 0:p.fan_mode)||"CONTEST"),e.$$.dirty&262144&&n(0,v=(p==null?void 0:p.state)||"IDLE"),e.$$.dirty&262144&&n(10,w=(p==null?void 0:p.band_a)||"0"),e.$$.dirty&262144&&n(9,_=(p==null?void 0:p.band_b)||"0"),e.$$.dirty&262144&&n(8,y=(p==null?void 0:p.connected)||!1),e.$$.dirty&1&&n(7,g=v.replace("TRANSMIT_A","TRANSMIT").replace("TRANSMIT_B","TRANSMIT")),e.$$.dirty&8&&n(6,d=m<40?"#4caf50":m<60?"#ffc107":m<75?"#ff9800":"#f44336"),e.$$.dirty&4&&n(5,T=i<1.5?"#4caf50":i<2?"#ffc107":i<3?"#ff9800":"#f44336"),e.$$.dirty&2&&n(4,C=Math.min(r/2e3*100,100))},[v,r,i,m,C,T,d,g,y,_,w,j,c,f,h,o,O,I,p,F]}class Es extends kt{constructor(l){super(),Ct(this,l,Ns,As,ft,{status:18})}}function Ls(e){let l;return{c(){l=a("span"),l.textContent="High!",s(l,"class","status-text danger svelte-j97t2j")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Is(e){let l;return{c(){l=a("span"),l.textContent="Caution",s(l,"class","status-text warning svelte-j97t2j")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Ds(e){let l;return{c(){l=a("span"),l.textContent="Good",s(l,"class","status-text ok svelte-j97t2j")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Bs(e){let l;return{c(){l=a("span"),l.textContent="Excellent",s(l,"class","status-text good svelte-j97t2j")},m(n,r){Oe(n,l,r)},d(n){n&&ge(l)}}}function Rs(e){let l,n,r,i,o,h,f,m,c,j,v,w,_,y,g=e[0].toFixed(0)+"",d,T,C,p,O,I,F,E,J,X,re,K,A,Z,Q=e[1].toFixed(2)+"",ne,be,z,B,k,U,S,W,he,oe,$e,ue,Ge,ae,He,me,H,x,_e,ee,Me,fe,Re,je,Pe,ye,ce,pe,xe,te,V=(e[11]/1e3).toFixed(3)+"",R,ve,q,Ce,Le,ct,ke,qe=(e[10]/1e3).toFixed(3)+"",nt,Je,We,Ie,De,Be=e[8]===1?"OPERATE":"STANDBY",Ve,tt,Te,Ye,Ue,Ke,Ae;function we(L,se){return L[1]<1.5?Bs:L[1]<2?Ds:L[1]<3?Is:Ls}let Se=we(e),ze=Se(e);return{c(){l=a("div"),n=a("div"),r=a("h2"),r.textContent="Tuner Genius XL",i=u(),o=a("div"),h=a("span"),f=b(e[12]),m=u(),c=a("span"),j=u(),v=a("div"),w=a("div"),_=a("div"),y=a("div"),d=b(g),T=a("span"),T.textContent="W",C=u(),p=a("div"),p.textContent="Forward Power",O=u(),I=a("div"),F=a("div"),E=a("div"),J=u(),X=a("div"),X.innerHTML="0 1000 2000",re=u(),K=a("div"),A=a("div"),Z=a("div"),ne=b(Q),be=u(),z=a("div"),z.textContent="SWR",B=u(),k=a("div"),ze.c(),U=u(),S=a("div"),W=a("div"),he=a("div"),oe=b(e[7]),$e=u(),ue=a("div"),ue.textContent="C1",Ge=u(),ae=a("div"),He=a("div"),me=b(e[6]),H=u(),x=a("div"),x.textContent="L",_e=u(),ee=a("div"),Me=a("div"),fe=b(e[5]),Re=u(),je=a("div"),je.textContent="C2",Pe=u(),ye=a("div"),ce=a("div"),pe=a("div"),pe.textContent="Freq A",xe=u(),te=a("div"),R=b(V),ve=a("span"),ve.textContent="MHz",q=u(),Ce=a("div"),Le=a("div"),Le.textContent="Freq B",ct=u(),ke=a("div"),nt=b(qe),Je=a("span"),Je.textContent="MHz",We=u(),Ie=a("div"),De=a("button"),Ve=b(Be),tt=u(),Te=a("button"),Te.textContent="BYPASS",Ye=u(),Ue=a("button"),Ue.innerHTML=` + AUTO TUNE`,s(r,"class","svelte-j97t2j"),s(h,"class","tuning-badge svelte-j97t2j"),P(h,"tuning",e[12]==="TUNING"),s(c,"class","status-dot svelte-j97t2j"),P(c,"disconnected",!e[4]),s(o,"class","header-right svelte-j97t2j"),s(n,"class","card-header svelte-j97t2j"),s(T,"class","unit svelte-j97t2j"),s(y,"class","power-value svelte-j97t2j"),s(p,"class","power-label svelte-j97t2j"),s(_,"class","power-main svelte-j97t2j"),s(E,"class","power-bar-glow svelte-j97t2j"),s(F,"class","power-bar-fill svelte-j97t2j"),le(F,"width",e[2]+"%"),s(X,"class","power-scale svelte-j97t2j"),s(I,"class","power-bar svelte-j97t2j"),s(w,"class","power-display svelte-j97t2j"),s(Z,"class","swr-value svelte-j97t2j"),s(z,"class","swr-label svelte-j97t2j"),s(A,"class","swr-circle svelte-j97t2j"),le(A,"--swr-color",e[3]),s(k,"class","swr-status svelte-j97t2j"),s(K,"class","swr-container svelte-j97t2j"),s(he,"class","cap-value svelte-j97t2j"),s(ue,"class","cap-label svelte-j97t2j"),s(W,"class","cap-item svelte-j97t2j"),s(He,"class","cap-value svelte-j97t2j"),s(x,"class","cap-label svelte-j97t2j"),s(ae,"class","cap-item svelte-j97t2j"),s(Me,"class","cap-value svelte-j97t2j"),s(je,"class","cap-label svelte-j97t2j"),s(ee,"class","cap-item svelte-j97t2j"),s(S,"class","capacitors svelte-j97t2j"),s(pe,"class","freq-label svelte-j97t2j"),s(ve,"class","freq-unit svelte-j97t2j"),s(te,"class","freq-value svelte-j97t2j"),s(ce,"class","freq-item svelte-j97t2j"),s(Le,"class","freq-label svelte-j97t2j"),s(Je,"class","freq-unit svelte-j97t2j"),s(ke,"class","freq-value svelte-j97t2j"),s(Ce,"class","freq-item svelte-j97t2j"),s(ye,"class","freq-display svelte-j97t2j"),s(De,"class","control-btn operate svelte-j97t2j"),P(De,"active",e[8]===1),s(Te,"class","control-btn bypass svelte-j97t2j"),P(Te,"active",e[9]),s(Ie,"class","controls svelte-j97t2j"),s(Ue,"class","tune-btn svelte-j97t2j"),s(v,"class","metrics svelte-j97t2j"),s(l,"class","card svelte-j97t2j")},m(L,se){Oe(L,l,se),t(l,n),t(n,r),t(n,i),t(n,o),t(o,h),t(h,f),t(o,m),t(o,c),t(l,j),t(l,v),t(v,w),t(w,_),t(_,y),t(y,d),t(y,T),t(_,C),t(_,p),t(w,O),t(w,I),t(I,F),t(F,E),t(I,J),t(I,X),t(v,re),t(v,K),t(K,A),t(A,Z),t(Z,ne),t(A,be),t(A,z),t(K,B),t(K,k),ze.m(k,null),t(v,U),t(v,S),t(S,W),t(W,he),t(he,oe),t(W,$e),t(W,ue),t(S,Ge),t(S,ae),t(ae,He),t(He,me),t(ae,H),t(ae,x),t(S,_e),t(S,ee),t(ee,Me),t(Me,fe),t(ee,Re),t(ee,je),t(v,Pe),t(v,ye),t(ye,ce),t(ce,pe),t(ce,xe),t(ce,te),t(te,R),t(te,ve),t(ye,q),t(ye,Ce),t(Ce,Le),t(Ce,ct),t(Ce,ke),t(ke,nt),t(ke,Je),t(v,We),t(v,Ie),t(Ie,De),t(De,Ve),t(Ie,tt),t(Ie,Te),t(v,Ye),t(v,Ue),Ke||(Ae=[Ne(De,"click",e[17]),Ne(Te,"click",e[18]),Ne(Ue,"click",e[13])],Ke=!0)},p(L,[se]){se&4096&&N(f,L[12]),se&4096&&P(h,"tuning",L[12]==="TUNING"),se&16&&P(c,"disconnected",!L[4]),se&1&&g!==(g=L[0].toFixed(0)+"")&&N(d,g),se&4&&le(F,"width",L[2]+"%"),se&2&&Q!==(Q=L[1].toFixed(2)+"")&&N(ne,Q),se&8&&le(A,"--swr-color",L[3]),Se!==(Se=we(L))&&(ze.d(1),ze=Se(L),ze&&(ze.c(),ze.m(k,null))),se&128&&N(oe,L[7]),se&64&&N(me,L[6]),se&32&&N(fe,L[5]),se&2048&&V!==(V=(L[11]/1e3).toFixed(3)+"")&&N(R,V),se&1024&&qe!==(qe=(L[10]/1e3).toFixed(3)+"")&&N(nt,qe),se&256&&Be!==(Be=L[8]===1?"OPERATE":"STANDBY")&&N(Ve,Be),se&256&&P(De,"active",L[8]===1),se&512&&P(Te,"active",L[9])},i:Fe,o:Fe,d(L){L&&ge(l),ze.d(),Ke=!1,it(Ae)}}}function Ws(e,l,n){let r,i,o,h,f,m,c,j,v,w,_,y,g,{status:d}=l;async function T(){try{await Ee.tuner.autoTune()}catch(F){console.error("Failed to tune:",F),alert("Failed to start tuning")}}async function C(F){try{await Ee.tuner.setBypass(F)}catch(E){console.error("Failed to set bypass:",E),alert("Failed to set bypass")}}async function p(F){try{await Ee.tuner.setOperate(F)}catch(E){console.error("Failed to set operate:",E),alert("Failed to set operate")}}const O=()=>p(c===1?0:1),I=()=>C(m?0:1);return e.$$set=F=>{"status"in F&&n(16,d=F.status)},e.$$.update=()=>{e.$$.dirty&65536&&n(0,r=(d==null?void 0:d.power_forward)||0),e.$$.dirty&65536&&n(1,i=(d==null?void 0:d.swr)||1),e.$$.dirty&65536&&n(12,o=(d==null?void 0:d.tuning_status)||"READY"),e.$$.dirty&65536&&n(11,h=(d==null?void 0:d.frequency_a)||0),e.$$.dirty&65536&&n(10,f=(d==null?void 0:d.frequency_b)||0),e.$$.dirty&65536&&n(9,m=(d==null?void 0:d.bypass)||!1),e.$$.dirty&65536&&n(8,c=(d==null?void 0:d.state)||0),e.$$.dirty&65536&&n(7,j=(d==null?void 0:d.c1)||0),e.$$.dirty&65536&&n(6,v=(d==null?void 0:d.l)||0),e.$$.dirty&65536&&n(5,w=(d==null?void 0:d.c2)||0),e.$$.dirty&65536&&n(4,_=(d==null?void 0:d.connected)||!1),e.$$.dirty&2&&n(3,y=i<1.5?"#4caf50":i<2?"#ffc107":i<3?"#ff9800":"#f44336"),e.$$.dirty&1&&n(2,g=Math.min(r/2e3*100,100))},[r,i,g,y,_,w,v,j,c,m,f,h,o,T,C,p,d,O,I]}class zs extends kt{constructor(l){super(),Ct(this,l,Ws,Rs,ft,{status:16})}}function Kt(e,l,n){const r=e.slice();r[12]=l[n];const i=r[1].tx&&r[1].tx_ant===r[12].number;r[13]=i;const o=r[0].tx&&r[0].tx_ant===r[12].number;r[14]=o;const h=!r[1].tx&&(r[1].rx_ant===r[12].number||r[1].tx_ant===r[12].number);r[15]=h;const f=!r[0].tx&&(r[0].rx_ant===r[12].number||r[0].tx_ant===r[12].number);r[16]=f;const m=r[13]||r[14];r[17]=m;const c=r[15]||r[16];return r[18]=c,r}function Zt(e){let l,n,r=e[12].name+"",i,o,h,f,m,c,j,v,w;function _(){return e[9](e[12])}function y(){return e[10](e[12])}return{c(){l=a("div"),n=a("div"),i=b(r),o=u(),h=a("div"),f=a("button"),f.textContent="A",m=u(),c=a("button"),c.textContent="B",j=u(),s(n,"class","antenna-name svelte-1r71rs8"),s(f,"class","port-btn svelte-1r71rs8"),P(f,"active",e[1].tx_ant===e[12].number||e[1].rx_ant===e[12].number),s(c,"class","port-btn svelte-1r71rs8"),P(c,"active",e[0].tx_ant===e[12].number||e[0].rx_ant===e[12].number),s(h,"class","antenna-ports svelte-1r71rs8"),s(l,"class","antenna-card svelte-1r71rs8"),P(l,"tx",e[17]),P(l,"active-a",e[15]),P(l,"active-b",e[16])},m(g,d){Oe(g,l,d),t(l,n),t(n,i),t(l,o),t(l,h),t(h,f),t(h,m),t(h,c),t(l,j),v||(w=[Ne(f,"click",_),Ne(c,"click",y)],v=!0)},p(g,d){e=g,d&16&&r!==(r=e[12].name+"")&&N(i,r),d&18&&P(f,"active",e[1].tx_ant===e[12].number||e[1].rx_ant===e[12].number),d&17&&P(c,"active",e[0].tx_ant===e[12].number||e[0].rx_ant===e[12].number),d&19&&P(l,"tx",e[17]),d&18&&P(l,"active-a",e[15]),d&17&&P(l,"active-b",e[16])},d(g){g&&ge(l),v=!1,it(w)}}}function Gs(e){let l,n,r,i,o,h,f,m,c,j,v=(e[1].source||"FLEX")+"",w,_,y,g,d=(e[0].source||"FLEX")+"",T,C,p,O,I,F,E,J,X,re,K,A,Z,Q,ne,be,z=yt(e[4]),B=[];for(let k=0;k🔄 + REBOOT`,s(r,"class","svelte-1r71rs8"),s(o,"class","status-dot svelte-1r71rs8"),P(o,"disconnected",!e[5]),s(n,"class","card-header svelte-1r71rs8"),s(j,"class","source-label svelte-1r71rs8"),s(c,"class","source-item svelte-1r71rs8"),s(g,"class","source-label svelte-1r71rs8"),s(y,"class","source-item svelte-1r71rs8"),s(m,"class","sources svelte-1r71rs8"),s(I,"class","band-value svelte-1r71rs8"),s(O,"class","band-item svelte-1r71rs8"),s(X,"class","band-value svelte-1r71rs8"),s(J,"class","band-item svelte-1r71rs8"),s(p,"class","bands svelte-1r71rs8"),s(A,"class","antennas svelte-1r71rs8"),s(Q,"class","reboot-btn svelte-1r71rs8"),s(f,"class","metrics svelte-1r71rs8"),s(l,"class","card svelte-1r71rs8")},m(k,U){Oe(k,l,U),t(l,n),t(n,r),t(n,i),t(n,o),t(l,h),t(l,f),t(f,m),t(m,c),t(c,j),t(j,w),t(m,_),t(m,y),t(y,g),t(g,T),t(f,C),t(f,p),t(p,O),t(O,I),t(I,F),t(p,E),t(p,J),t(J,X),t(X,re),t(f,K),t(f,A);for(let S=0;Sv(1,g.number),y=g=>v(2,g.number);return e.$$set=g=>{"status"in g&&n(8,c=g.status)},e.$$.update=()=>{e.$$.dirty&256&&n(5,r=(c==null?void 0:c.connected)||!1),e.$$.dirty&256&&n(1,i=(c==null?void 0:c.port_a)||{}),e.$$.dirty&256&&n(0,o=(c==null?void 0:c.port_b)||{}),e.$$.dirty&256&&n(4,h=(c==null?void 0:c.antennas)||[]),e.$$.dirty&2&&n(3,f=j[i.band]||"None"),e.$$.dirty&1&&n(2,m=j[o.band]||"None")},[o,i,m,f,h,r,v,w,c,_,y]}class qs extends kt{constructor(l){super(),Ct(this,l,Hs,Gs,ft,{status:8})}}function Qt(e,l,n){const r=e.slice();r[9]=l[n];const i=150+125*Math.sin(r[9]*Math.PI/180);r[10]=i;const o=150-125*Math.cos(r[9]*Math.PI/180);return r[11]=o,r}function es(e){let l,n,r;return{c(){l=Y("text"),n=b(e[9]),r=b("°"),s(l,"x",e[10]),s(l,"y",e[11]),s(l,"text-anchor","middle"),s(l,"dominant-baseline","middle"),s(l,"class","degree-label svelte-1sgx2se")},m(i,o){Oe(i,l,o),t(l,n),t(l,r)},p:Fe,d(i){i&&ge(l)}}}function $s(e){let l,n,r,i,o,h,f,m,c,j,v,w,_,y,g,d,T,C,p,O,I,F,E,J,X,re,K,A,Z,Q,ne,be,z,B,k,U,S,W,he,oe,$e,ue,Ge,ae,He,me,H,x,_e,ee,Me,fe,Re,je,Pe,ye,ce,pe,xe,te=yt([45,135,225,315]),V=[];for(let R=0;R<4;R+=1)V[R]=es(Qt(e,te,R));return{c(){l=a("div"),n=a("div"),r=a("h2"),r.textContent="Rotator Genius",i=u(),o=a("span"),h=u(),f=a("div"),m=a("div"),c=a("div"),c.textContent="CURRENT HEADING",j=u(),v=a("div"),w=b(e[2]),_=b("°"),y=u(),g=a("div"),d=Y("svg"),T=Y("defs"),C=Y("radialGradient"),p=Y("stop"),O=Y("stop"),I=Y("circle"),F=Y("circle"),E=Y("circle"),J=Y("circle"),X=Y("g"),re=Y("g"),K=Y("path"),A=Y("line"),Z=Y("line"),Q=Y("g"),ne=Y("polygon"),z=Y("circle"),B=Y("animate"),k=Y("circle"),U=Y("animate"),S=Y("animate"),W=Y("text"),he=b("N"),oe=Y("text"),$e=b("E"),ue=Y("text"),Ge=b("S"),ae=Y("text"),He=b("W");for(let R=0;R<4;R+=1)V[R].c();me=u(),H=a("div"),x=a("input"),_e=u(),ee=a("button"),ee.textContent="GO",Me=u(),fe=a("div"),Re=a("button"),Re.innerHTML=` + CCW`,je=u(),Pe=a("button"),Pe.textContent="STOP",ye=u(),ce=a("button"),ce.innerHTML=` + CW`,s(r,"class","svelte-1sgx2se"),s(o,"class","status-dot svelte-1sgx2se"),P(o,"disconnected",!e[1]),s(n,"class","card-header svelte-1sgx2se"),s(c,"class","heading-label svelte-1sgx2se"),s(v,"class","heading-value svelte-1sgx2se"),s(m,"class","heading-display svelte-1sgx2se"),s(p,"offset","0%"),le(p,"stop-color","rgba(79, 195, 247, 0.7)"),le(p,"stop-opacity","1"),s(O,"offset","100%"),le(O,"stop-color","rgba(79, 195, 247, 0)"),le(O,"stop-opacity","0"),s(C,"id","beamGradient"),s(I,"cx","150"),s(I,"cy","150"),s(I,"r","140"),s(I,"fill","rgba(30, 64, 175, 0.15)"),s(I,"stroke","rgba(79, 195, 247, 0.4)"),s(I,"stroke-width","2"),s(F,"cx","150"),s(F,"cy","150"),s(F,"r","105"),s(F,"fill","none"),s(F,"stroke","rgba(79,195,247,0.2)"),s(F,"stroke-width","1"),s(F,"stroke-dasharray","3,3"),s(E,"cx","150"),s(E,"cy","150"),s(E,"r","70"),s(E,"fill","none"),s(E,"stroke","rgba(79,195,247,0.2)"),s(E,"stroke-width","1"),s(E,"stroke-dasharray","3,3"),s(J,"cx","150"),s(J,"cy","150"),s(J,"r","35"),s(J,"fill","none"),s(J,"stroke","rgba(79,195,247,0.2)"),s(J,"stroke-width","1"),s(J,"stroke-dasharray","3,3"),s(K,"d","M 0,0 L "+-Math.sin(15*Math.PI/180)*130+","+-Math.cos(15*Math.PI/180)*130+` \r + A 130,130 0 0,1 `+Math.sin(15*Math.PI/180)*130+","+-Math.cos(15*Math.PI/180)*130+" Z"),s(K,"fill","url(#beamGradient)"),s(K,"opacity","0.85"),s(A,"x1","0"),s(A,"y1","0"),s(A,"x2",-Math.sin(15*Math.PI/180)*130),s(A,"y2",-Math.cos(15*Math.PI/180)*130),s(A,"stroke","#4fc3f7"),s(A,"stroke-width","2"),s(A,"opacity","0.9"),s(Z,"x1","0"),s(Z,"y1","0"),s(Z,"x2",Math.sin(15*Math.PI/180)*130),s(Z,"y2",-Math.cos(15*Math.PI/180)*130),s(Z,"stroke","#4fc3f7"),s(Z,"stroke-width","2"),s(Z,"opacity","0.9"),s(ne,"points","0,-20 -8,5 0,0 8,5"),s(ne,"fill","#4fc3f7"),s(ne,"stroke","#0288d1"),s(ne,"stroke-width","2"),le(ne,"filter","drop-shadow(0 0 10px rgba(79, 195, 247, 1))"),s(Q,"transform","translate(0, -110)"),s(re,"transform",be="rotate("+e[2]+")"),s(B,"attributeName","r"),s(B,"values","5;7;5"),s(B,"dur","2s"),s(B,"repeatCount","indefinite"),s(z,"cx","0"),s(z,"cy","0"),s(z,"r","5"),s(z,"fill","#f44336"),s(z,"stroke","#fff"),s(z,"stroke-width","2"),s(U,"attributeName","r"),s(U,"values","10;16;10"),s(U,"dur","2s"),s(U,"repeatCount","indefinite"),s(S,"attributeName","opacity"),s(S,"values","0.5;0;0.5"),s(S,"dur","2s"),s(S,"repeatCount","indefinite"),s(k,"cx","0"),s(k,"cy","0"),s(k,"r","10"),s(k,"fill","none"),s(k,"stroke","#f44336"),s(k,"stroke-width","1.5"),s(k,"opacity","0.5"),s(X,"transform","translate(150, 150)"),s(W,"x","150"),s(W,"y","20"),s(W,"text-anchor","middle"),s(W,"class","cardinal svelte-1sgx2se"),s(oe,"x","280"),s(oe,"y","155"),s(oe,"text-anchor","middle"),s(oe,"class","cardinal svelte-1sgx2se"),s(ue,"x","150"),s(ue,"y","285"),s(ue,"text-anchor","middle"),s(ue,"class","cardinal svelte-1sgx2se"),s(ae,"x","20"),s(ae,"y","155"),s(ae,"text-anchor","middle"),s(ae,"class","cardinal svelte-1sgx2se"),s(d,"viewBox","0 0 300 300"),s(d,"class","map-svg svelte-1sgx2se"),s(g,"class","map-container svelte-1sgx2se"),s(x,"type","number"),s(x,"min","0"),s(x,"max","359"),s(x,"placeholder","Enter heading"),s(x,"class","heading-input svelte-1sgx2se"),s(ee,"class","go-btn svelte-1sgx2se"),s(H,"class","goto-container svelte-1sgx2se"),s(Re,"class","control-btn ccw svelte-1sgx2se"),s(Pe,"class","control-btn stop svelte-1sgx2se"),s(ce,"class","control-btn cw svelte-1sgx2se"),s(fe,"class","controls svelte-1sgx2se"),s(f,"class","metrics svelte-1sgx2se"),s(l,"class","card svelte-1sgx2se")},m(R,ve){Oe(R,l,ve),t(l,n),t(n,r),t(n,i),t(n,o),t(l,h),t(l,f),t(f,m),t(m,c),t(m,j),t(m,v),t(v,w),t(v,_),t(f,y),t(f,g),t(g,d),t(d,T),t(T,C),t(C,p),t(C,O),t(d,I),t(d,F),t(d,E),t(d,J),t(d,X),t(X,re),t(re,K),t(re,A),t(re,Z),t(re,Q),t(Q,ne),t(X,z),t(z,B),t(X,k),t(k,U),t(k,S),t(d,W),t(W,he),t(d,oe),t(oe,$e),t(d,ue),t(ue,Ge),t(d,ae),t(ae,He);for(let q=0;q<4;q+=1)V[q]&&V[q].m(d,null);t(f,me),t(f,H),t(H,x),Pt(x,e[0]),t(H,_e),t(H,ee),t(f,Me),t(f,fe),t(fe,Re),t(fe,je),t(fe,Pe),t(fe,ye),t(fe,ce),pe||(xe=[Ne(x,"input",e[8]),Ne(ee,"click",e[3]),Ne(Re,"click",e[5]),Ne(Pe,"click",e[6]),Ne(ce,"click",e[4])],pe=!0)},p(R,[ve]){if(ve&2&&P(o,"disconnected",!R[1]),ve&4&&N(w,R[2]),ve&4&&be!==(be="rotate("+R[2]+")")&&s(re,"transform",be),ve&0){te=yt([45,135,225,315]);let q;for(q=0;q<4;q+=1){const Ce=Qt(R,te,q);V[q]?V[q].p(Ce,ve):(V[q]=es(Ce),V[q].c(),V[q].m(d,null))}for(;q<4;q+=1)V[q].d(1)}ve&1&&ns(x.value)!==R[0]&&Pt(x,R[0])},i:Fe,o:Fe,d(R){R&&ge(l),Wt(V,R),pe=!1,it(xe)}}}function Js(e,l,n){let r,i,{status:o}=l,h=0;async function f(){if(h<0||h>359){alert("Heading must be between 0 and 359");return}try{const w=(h-10+360)%360;await Ee.rotator.setHeading(w)}catch(w){console.error("Failed to set heading:",w),alert("Failed to rotate")}}async function m(){try{await Ee.rotator.rotateCW()}catch(w){console.error("Failed to rotate CW:",w)}}async function c(){try{await Ee.rotator.rotateCCW()}catch(w){console.error("Failed to rotate CCW:",w)}}async function j(){try{await Ee.rotator.stop()}catch(w){console.error("Failed to stop:",w)}}function v(){h=ns(this.value),n(0,h)}return e.$$set=w=>{"status"in w&&n(7,o=w.status)},e.$$.update=()=>{e.$$.dirty&128&&n(2,r=(o==null?void 0:o.heading)||0),e.$$.dirty&128&&n(1,i=(o==null?void 0:o.connected)||!1)},[h,i,r,f,m,c,j,o,v]}class Us extends kt{constructor(l){super(),Ct(this,l,Js,$s,ft,{status:7})}}function Xs(e){var rt,ut,Tt,Xe,ot;let l,n,r,i,o,h,f,m,c,j,v=e[1]?"Connected":"Disconnected",w,_,y,g,d,T,C,p=e[5].sfi+"",O,I,F,E,J,X=e[5].sunspots+"",re,K,A,Z,Q,ne=e[5].a_index+"",be,z,B,k,U,S=e[5].k_index+"",W,he,oe,$e,ue,Ge=e[5].geomag+"",ae,He,me,H,x,_e,ee=e[4].wind_speed.toFixed(1)+"",Me,fe,Re,je,Pe,ye=e[4].wind_gust.toFixed(1)+"",ce,pe,xe,te,V,R=e[4].temp.toFixed(1)+"",ve,q,Ce,Le,ct,ke=e[4].feels_like.toFixed(1)+"",qe,nt,Je,We,Ie,De=ts(e[2])+"",Be,Ve,tt,Te=e[2].toLocaleDateString()+"",Ye,Ue,Ke,Ae,we,Se,ze,L,se,st,at,Ze,lt,dt,Qe,ie;return Se=new Ss({props:{status:(rt=e[0])==null?void 0:rt.webswitch}}),L=new Es({props:{status:(ut=e[0])==null?void 0:ut.power_genius}}),st=new zs({props:{status:(Tt=e[0])==null?void 0:Tt.tuner_genius}}),lt=new qs({props:{status:(Xe=e[0])==null?void 0:Xe.antenna_genius}}),Qe=new Us({props:{status:(ot=e[0])==null?void 0:ot.rotator_genius}}),{c(){l=a("div"),n=a("header"),r=a("div"),i=a("h1"),o=b(e[3]),h=b(" Shack"),f=u(),m=a("div"),c=a("span"),j=u(),w=b(v),_=u(),y=a("div"),g=a("div"),d=a("span"),T=b("SFI "),C=a("span"),O=b(p),I=u(),F=a("span"),E=b("Spots "),J=a("span"),re=b(X),K=u(),A=a("span"),Z=b("A "),Q=a("span"),be=b(ne),z=u(),B=a("span"),k=b("K "),U=a("span"),W=b(S),he=u(),oe=a("span"),$e=b("G "),ue=a("span"),ae=b(Ge),He=u(),me=a("div"),H=a("div"),x=a("span"),_e=b("🌬️ "),Me=b(ee),fe=b("m/s"),Re=u(),je=a("span"),Pe=b("💨 "),ce=b(ye),pe=b("m/s"),xe=u(),te=a("span"),V=b("🌡️ "),ve=b(R),q=b("°C"),Ce=u(),Le=a("span"),ct=b("→ "),qe=b(ke),nt=b("°C"),Je=u(),We=a("div"),Ie=a("span"),Be=b(De),Ve=u(),tt=a("span"),Ye=b(Te),Ue=u(),Ke=a("main"),Ae=a("div"),we=a("div"),Mt(Se.$$.fragment),ze=u(),Mt(L.$$.fragment),se=u(),Mt(st.$$.fragment),at=u(),Ze=a("div"),Mt(lt.$$.fragment),dt=u(),Mt(Qe.$$.fragment),s(i,"class","svelte-1hrhory"),s(c,"class","status-indicator"),P(c,"status-online",e[1]),P(c,"status-offline",!e[1]),s(m,"class","connection-status svelte-1hrhory"),s(r,"class","header-left svelte-1hrhory"),s(C,"class","value svelte-1hrhory"),s(d,"class","solar-item svelte-1hrhory"),s(J,"class","value svelte-1hrhory"),s(F,"class","solar-item svelte-1hrhory"),s(Q,"class","value svelte-1hrhory"),s(A,"class","solar-item svelte-1hrhory"),s(U,"class","value svelte-1hrhory"),s(B,"class","solar-item svelte-1hrhory"),s(ue,"class","value svelte-1hrhory"),s(oe,"class","solar-item svelte-1hrhory"),s(g,"class","solar-info svelte-1hrhory"),s(y,"class","header-center svelte-1hrhory"),s(x,"title","Wind"),s(je,"title","Gust"),s(te,"title","Temperature"),s(Le,"title","Feels like"),s(H,"class","weather-info svelte-1hrhory"),s(Ie,"class","time svelte-1hrhory"),s(tt,"class","date svelte-1hrhory"),s(We,"class","clock svelte-1hrhory"),s(me,"class","header-right svelte-1hrhory"),s(n,"class","svelte-1hrhory"),s(we,"class","row svelte-1hrhory"),s(Ze,"class","row svelte-1hrhory"),s(Ae,"class","dashboard-grid svelte-1hrhory"),s(Ke,"class","svelte-1hrhory"),s(l,"class","app svelte-1hrhory")},m(M,G){Oe(M,l,G),t(l,n),t(n,r),t(r,i),t(i,o),t(i,h),t(r,f),t(r,m),t(m,c),t(m,j),t(m,w),t(n,_),t(n,y),t(y,g),t(g,d),t(d,T),t(d,C),t(C,O),t(g,I),t(g,F),t(F,E),t(F,J),t(J,re),t(g,K),t(g,A),t(A,Z),t(A,Q),t(Q,be),t(g,z),t(g,B),t(B,k),t(B,U),t(U,W),t(g,he),t(g,oe),t(oe,$e),t(oe,ue),t(ue,ae),t(n,He),t(n,me),t(me,H),t(H,x),t(x,_e),t(x,Me),t(x,fe),t(H,Re),t(H,je),t(je,Pe),t(je,ce),t(je,pe),t(H,xe),t(H,te),t(te,V),t(te,ve),t(te,q),t(H,Ce),t(H,Le),t(Le,ct),t(Le,qe),t(Le,nt),t(me,Je),t(me,We),t(We,Ie),t(Ie,Be),t(We,Ve),t(We,tt),t(tt,Ye),t(l,Ue),t(l,Ke),t(Ke,Ae),t(Ae,we),bt(Se,we,null),t(we,ze),bt(L,we,null),t(we,se),bt(st,we,null),t(Ae,at),t(Ae,Ze),bt(lt,Ze,null),t(Ze,dt),bt(Qe,Ze,null),ie=!0},p(M,[G]){var D,$,Gt,Ht,qt;(!ie||G&8)&&N(o,M[3]),(!ie||G&2)&&P(c,"status-online",M[1]),(!ie||G&2)&&P(c,"status-offline",!M[1]),(!ie||G&2)&&v!==(v=M[1]?"Connected":"Disconnected")&&N(w,v),(!ie||G&32)&&p!==(p=M[5].sfi+"")&&N(O,p),(!ie||G&32)&&X!==(X=M[5].sunspots+"")&&N(re,X),(!ie||G&32)&&ne!==(ne=M[5].a_index+"")&&N(be,ne),(!ie||G&32)&&S!==(S=M[5].k_index+"")&&N(W,S),(!ie||G&32)&&Ge!==(Ge=M[5].geomag+"")&&N(ae,Ge),(!ie||G&16)&&ee!==(ee=M[4].wind_speed.toFixed(1)+"")&&N(Me,ee),(!ie||G&16)&&ye!==(ye=M[4].wind_gust.toFixed(1)+"")&&N(ce,ye),(!ie||G&16)&&R!==(R=M[4].temp.toFixed(1)+"")&&N(ve,R),(!ie||G&16)&&ke!==(ke=M[4].feels_like.toFixed(1)+"")&&N(qe,ke),(!ie||G&4)&&De!==(De=ts(M[2])+"")&&N(Be,De),(!ie||G&4)&&Te!==(Te=M[2].toLocaleDateString()+"")&&N(Ye,Te);const pt={};G&1&&(pt.status=(D=M[0])==null?void 0:D.webswitch),Se.$set(pt);const St={};G&1&&(St.status=($=M[0])==null?void 0:$.power_genius),L.$set(St);const Ft={};G&1&&(Ft.status=(Gt=M[0])==null?void 0:Gt.tuner_genius),st.$set(Ft);const vt={};G&1&&(vt.status=(Ht=M[0])==null?void 0:Ht.antenna_genius),lt.$set(vt);const et={};G&1&&(et.status=(qt=M[0])==null?void 0:qt.rotator_genius),Qe.$set(et)},i(M){ie||(gt(Se.$$.fragment,M),gt(L.$$.fragment,M),gt(st.$$.fragment,M),gt(lt.$$.fragment,M),gt(Qe.$$.fragment,M),ie=!0)},o(M){Ot(Se.$$.fragment,M),Ot(L.$$.fragment,M),Ot(st.$$.fragment,M),Ot(lt.$$.fragment,M),Ot(Qe.$$.fragment,M),ie=!1},d(M){M&&ge(l),_t(Se),_t(L),_t(st),_t(lt),_t(Qe)}}}function ts(e){return e.toTimeString().slice(0,8)}function xs(e,l,n){let r,i,o=null,h=!1,f=new Date,m="F4BPO";const c=os.subscribe(v=>{n(0,o=v)}),j=Rt.subscribe(v=>{n(1,h=v)});return vs(async()=>{xt.connect();try{const w=await Ee.getConfig();w.callsign&&n(3,m=w.callsign)}catch(w){console.error("Failed to fetch config:",w)}const v=setInterval(()=>{n(2,f=new Date)},1e3);return()=>{clearInterval(v)}}),fs(()=>{xt.disconnect(),c(),j()}),e.$$.update=()=>{e.$$.dirty&1&&n(5,r=(o==null?void 0:o.solar)||{sfi:0,sunspots:0,a_index:0,k_index:0,geomag:"Unknown"}),e.$$.dirty&1&&n(4,i=(o==null?void 0:o.weather)||{wind_speed:0,wind_gust:0,temp:0,feels_like:0})},[o,h,f,m,i,r]}class Vs extends kt{constructor(l){super(),Ct(this,l,xs,Xs,ft,{})}}new Vs({target:document.getElementById("app")}); diff --git a/cmd/server/web/dist/assets/index-PFp0U9rZ.css b/cmd/server/web/dist/assets/index-PFp0U9rZ.css new file mode 100644 index 0000000..8223aa7 --- /dev/null +++ b/cmd/server/web/dist/assets/index-PFp0U9rZ.css @@ -0,0 +1 @@ +.card.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-z2csmj.svelte-z2csmj{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-z2csmj.svelte-z2csmj{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-z2csmj.svelte-z2csmj{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-z2csmj.svelte-z2csmj{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-z2csmj.svelte-z2csmj{padding:16px;display:flex;flex-direction:column;gap:12px}.relays.svelte-z2csmj.svelte-z2csmj{display:flex;flex-direction:column;gap:8px}.relay-card.svelte-z2csmj.svelte-z2csmj{display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--bg-tertiary);border-radius:6px;border:1px solid var(--border-color);transition:all .3s}.relay-card.relay-on.svelte-z2csmj.svelte-z2csmj{background:#4caf501a;border-color:#4caf504d;box-shadow:0 0 15px #4caf5033}.relay-info.svelte-z2csmj.svelte-z2csmj{display:flex;align-items:center}.relay-details.svelte-z2csmj.svelte-z2csmj{display:flex;flex-direction:column;gap:2px}.relay-name.svelte-z2csmj.svelte-z2csmj{font-size:12px;color:var(--text-primary);font-weight:500}.relay-status.svelte-z2csmj.svelte-z2csmj{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.relay-card.relay-on.svelte-z2csmj .relay-status.svelte-z2csmj{color:#4caf50;font-weight:600}.relay-toggle.svelte-z2csmj.svelte-z2csmj{padding:0;background:transparent;border:none;cursor:pointer}.toggle-track.svelte-z2csmj.svelte-z2csmj{width:52px;height:28px;background:var(--bg-primary);border:2px solid var(--border-color);border-radius:14px;position:relative;transition:all .3s}.relay-toggle.svelte-z2csmj:hover .toggle-track.svelte-z2csmj{border-color:var(--accent-cyan)}.relay-toggle.active.svelte-z2csmj .toggle-track.svelte-z2csmj{background:linear-gradient(135deg,#4caf50,#66bb6a);border-color:#4caf50;box-shadow:0 0 15px #4caf5080}.toggle-thumb.svelte-z2csmj.svelte-z2csmj{width:20px;height:20px;background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:all .3s;box-shadow:0 2px 4px #0000004d}.relay-toggle.active.svelte-z2csmj .toggle-thumb.svelte-z2csmj{transform:translate(24px)}.relay-toggle.svelte-z2csmj.svelte-z2csmj:disabled{opacity:.5;cursor:not-allowed}.controls.svelte-z2csmj.svelte-z2csmj{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}.control-btn.svelte-z2csmj.svelte-z2csmj{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:6px}.control-btn.svelte-z2csmj.svelte-z2csmj:hover{transform:translateY(-2px)}.control-btn.svelte-z2csmj.svelte-z2csmj:active{transform:translateY(0)}.btn-icon.svelte-z2csmj.svelte-z2csmj{font-size:16px}.all-on.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#4caf50,#66bb6a);color:#fff;box-shadow:0 4px 12px #4caf5066}.all-on.svelte-z2csmj.svelte-z2csmj:hover{box-shadow:0 6px 16px #4caf5080}.all-off.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff;box-shadow:0 4px 12px #f4433666}.all-off.svelte-z2csmj.svelte-z2csmj:hover{box-shadow:0 6px 16px #f4433680}.card.svelte-utvwj6.svelte-utvwj6{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.header-right.svelte-utvwj6.svelte-utvwj6{display:flex;align-items:center;gap:8px}.state-badge.svelte-utvwj6.svelte-utvwj6{padding:4px 12px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;transition:all .2s}.state-badge.idle.svelte-utvwj6.svelte-utvwj6{background:#4caf5033;color:#4caf50}.state-badge.transmit.svelte-utvwj6.svelte-utvwj6{background:#ff980033;color:#ff9800;animation:svelte-utvwj6-pulse 1s infinite}@keyframes svelte-utvwj6-pulse{0%,to{opacity:1}50%{opacity:.7}}.status-dot.svelte-utvwj6.svelte-utvwj6{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-utvwj6.svelte-utvwj6{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-utvwj6.svelte-utvwj6{padding:16px;display:flex;flex-direction:column;gap:16px}.power-display.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:8px}.power-main.svelte-utvwj6.svelte-utvwj6{text-align:center}.power-value.svelte-utvwj6.svelte-utvwj6{font-size:48px;font-weight:200;color:var(--accent-cyan);line-height:1;text-shadow:0 0 20px rgba(79,195,247,.5)}.power-value.svelte-utvwj6 .unit.svelte-utvwj6{font-size:24px;color:var(--text-secondary);margin-left:4px}.power-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px}.power-bar.svelte-utvwj6.svelte-utvwj6{position:relative;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.power-bar-fill.svelte-utvwj6.svelte-utvwj6{position:relative;height:100%;background:linear-gradient(90deg,#4caf50,#ffc107,#ff9800,#f44336);border-radius:4px;transition:width .3s ease}.power-bar-glow.svelte-utvwj6.svelte-utvwj6{position:absolute;top:0;right:0;width:20px;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.5));animation:svelte-utvwj6-shimmer 2s infinite}@keyframes svelte-utvwj6-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}.power-scale.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);margin-top:4px}.swr-container.svelte-utvwj6.svelte-utvwj6{display:flex;align-items:center;gap:16px}.swr-circle.svelte-utvwj6.svelte-utvwj6{width:80px;height:80px;border-radius:50%;background:radial-gradient(circle,rgba(79,195,247,.1),transparent);border:3px solid var(--swr-color);display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 20px var(--swr-color)}.swr-value.svelte-utvwj6.svelte-utvwj6{font-size:24px;font-weight:300;color:var(--swr-color)}.swr-label.svelte-utvwj6.svelte-utvwj6{font-size:10px;color:var(--text-muted);text-transform:uppercase}.swr-status.svelte-utvwj6.svelte-utvwj6{flex:1}.status-text.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.status-text.good.svelte-utvwj6.svelte-utvwj6{color:#4caf50}.status-text.ok.svelte-utvwj6.svelte-utvwj6{color:#ffc107}.status-text.warning.svelte-utvwj6.svelte-utvwj6{color:#ff9800}.status-text.danger.svelte-utvwj6.svelte-utvwj6{color:#f44336}.temp-group.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:1fr 1fr;gap:12px}.temp-item.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:4px;padding:12px;background:var(--bg-tertiary);border-radius:6px}.temp-value.svelte-utvwj6.svelte-utvwj6{font-size:32px;font-weight:300;line-height:1}.temp-label.svelte-utvwj6.svelte-utvwj6{font-size:10px;color:var(--text-muted);text-transform:uppercase}.temp-mini-bar.svelte-utvwj6.svelte-utvwj6{height:4px;background:#ffffff1a;border-radius:2px;overflow:hidden;margin-top:4px}.temp-mini-fill.svelte-utvwj6.svelte-utvwj6{height:100%;border-radius:2px;transition:width .3s ease}.params-grid.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.param-box.svelte-utvwj6.svelte-utvwj6{padding:8px;background:var(--bg-tertiary);border-radius:4px;text-align:center}.param-label.svelte-utvwj6.svelte-utvwj6{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.param-value.svelte-utvwj6.svelte-utvwj6{font-size:18px;font-weight:300;color:var(--text-primary);margin-top:2px}.band-display.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:8px;background:#4fc3f70d;border-radius:6px}.band-item.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;align-items:center}.band-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted)}.band-value.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;color:var(--accent-cyan)}.fan-control.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:6px}.control-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}select.svelte-utvwj6.svelte-utvwj6{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:8px;font-size:12px;cursor:pointer;outline:none;transition:all .2s}select.svelte-utvwj6.svelte-utvwj6:hover{border-color:var(--accent-cyan)}select.svelte-utvwj6.svelte-utvwj6:focus{border-color:var(--accent-cyan);box-shadow:0 0 0 2px #4fc3f733}.card.svelte-j97t2j.svelte-j97t2j{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-j97t2j.svelte-j97t2j{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-j97t2j.svelte-j97t2j{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.header-right.svelte-j97t2j.svelte-j97t2j{display:flex;align-items:center;gap:8px}.tuning-badge.svelte-j97t2j.svelte-j97t2j{padding:4px 12px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#4caf5033;color:#4caf50}.tuning-badge.tuning.svelte-j97t2j.svelte-j97t2j{background:#ff980033;color:#ff9800;animation:svelte-j97t2j-pulse 1s infinite}@keyframes svelte-j97t2j-pulse{0%,to{opacity:1}50%{opacity:.7}}.status-dot.svelte-j97t2j.svelte-j97t2j{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-j97t2j.svelte-j97t2j{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-j97t2j.svelte-j97t2j{padding:16px;display:flex;flex-direction:column;gap:16px}.power-display.svelte-j97t2j.svelte-j97t2j{display:flex;flex-direction:column;gap:8px}.power-main.svelte-j97t2j.svelte-j97t2j{text-align:center}.power-value.svelte-j97t2j.svelte-j97t2j{font-size:48px;font-weight:200;color:var(--accent-cyan);line-height:1;text-shadow:0 0 20px rgba(79,195,247,.5)}.power-value.svelte-j97t2j .unit.svelte-j97t2j{font-size:24px;color:var(--text-secondary);margin-left:4px}.power-label.svelte-j97t2j.svelte-j97t2j{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px}.power-bar.svelte-j97t2j.svelte-j97t2j{position:relative;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.power-bar-fill.svelte-j97t2j.svelte-j97t2j{position:relative;height:100%;background:linear-gradient(90deg,#4caf50,#ffc107,#ff9800,#f44336);border-radius:4px;transition:width .3s ease}.power-bar-glow.svelte-j97t2j.svelte-j97t2j{position:absolute;top:0;right:0;width:20px;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.5));animation:svelte-j97t2j-shimmer 2s infinite}@keyframes svelte-j97t2j-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}.power-scale.svelte-j97t2j.svelte-j97t2j{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);margin-top:4px}.swr-container.svelte-j97t2j.svelte-j97t2j{display:flex;align-items:center;gap:16px}.swr-circle.svelte-j97t2j.svelte-j97t2j{width:80px;height:80px;border-radius:50%;background:radial-gradient(circle,rgba(79,195,247,.1),transparent);border:3px solid var(--swr-color);display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 20px var(--swr-color)}.swr-value.svelte-j97t2j.svelte-j97t2j{font-size:24px;font-weight:300;color:var(--swr-color)}.swr-label.svelte-j97t2j.svelte-j97t2j{font-size:10px;color:var(--text-muted);text-transform:uppercase}.swr-status.svelte-j97t2j.svelte-j97t2j{flex:1}.status-text.svelte-j97t2j.svelte-j97t2j{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.status-text.good.svelte-j97t2j.svelte-j97t2j{color:#4caf50}.status-text.ok.svelte-j97t2j.svelte-j97t2j{color:#ffc107}.status-text.warning.svelte-j97t2j.svelte-j97t2j{color:#ff9800}.status-text.danger.svelte-j97t2j.svelte-j97t2j{color:#f44336}.capacitors.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:16px;background:#4fc3f70d;border-radius:6px;border:1px solid rgba(79,195,247,.2)}.cap-item.svelte-j97t2j.svelte-j97t2j{display:flex;flex-direction:column;align-items:center;gap:4px}.cap-value.svelte-j97t2j.svelte-j97t2j{font-size:32px;font-weight:300;color:var(--accent-cyan);text-shadow:0 0 15px rgba(79,195,247,.5)}.cap-label.svelte-j97t2j.svelte-j97t2j{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px}.freq-display.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:1fr 1fr;gap:12px}.freq-item.svelte-j97t2j.svelte-j97t2j{padding:10px;background:var(--bg-tertiary);border-radius:6px;display:flex;flex-direction:column;gap:4px}.freq-label.svelte-j97t2j.svelte-j97t2j{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.freq-value.svelte-j97t2j.svelte-j97t2j{font-size:16px;font-weight:300;color:var(--text-primary)}.freq-unit.svelte-j97t2j.svelte-j97t2j{font-size:11px;color:var(--text-secondary);margin-left:2px}.controls.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:1fr 1fr;gap:8px}.control-btn.svelte-j97t2j.svelte-j97t2j{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-tertiary);color:var(--text-primary);transition:all .2s}.control-btn.svelte-j97t2j.svelte-j97t2j:hover{border-color:var(--accent-cyan);transform:translateY(-1px)}.control-btn.active.svelte-j97t2j.svelte-j97t2j{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000;box-shadow:0 0 15px #4fc3f780}.tune-btn.svelte-j97t2j.svelte-j97t2j{width:100%;padding:14px;border-radius:6px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 4px 12px #f4433666}.tune-btn.svelte-j97t2j.svelte-j97t2j:hover{transform:translateY(-2px);box-shadow:0 6px 16px #f4433680}.tune-btn.svelte-j97t2j.svelte-j97t2j:active{transform:translateY(0)}.tune-icon.svelte-j97t2j.svelte-j97t2j{font-size:18px}.card.svelte-1r71rs8{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-1r71rs8{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-1r71rs8{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-1r71rs8{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-1r71rs8{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-1r71rs8{padding:16px;display:flex;flex-direction:column;gap:12px}.sources.svelte-1r71rs8{display:grid;grid-template-columns:1fr 1fr;gap:8px}.source-item.svelte-1r71rs8{padding:8px;background:var(--bg-tertiary);border-radius:4px;text-align:center}.source-label.svelte-1r71rs8{font-size:12px;font-weight:600;color:var(--text-primary);text-transform:uppercase}.bands.svelte-1r71rs8{display:grid;grid-template-columns:1fr 1fr;gap:8px}.band-item.svelte-1r71rs8{padding:10px;background:#4fc3f71a;border:1px solid rgba(79,195,247,.3);border-radius:4px;text-align:center}.band-value.svelte-1r71rs8{font-size:16px;font-weight:600;color:var(--accent-cyan)}.antennas.svelte-1r71rs8{display:flex;flex-direction:column;gap:8px}.antenna-card.svelte-1r71rs8{display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--bg-tertiary);border:2px solid var(--border-color);border-radius:6px;transition:all .3s}.antenna-card.tx.svelte-1r71rs8{background:#f4433633;border-color:#f44336;box-shadow:0 0 20px #f4433666}.antenna-card.active-a.svelte-1r71rs8{background:#4caf5033;border-color:#4caf50;box-shadow:0 0 20px #4caf504d}.antenna-card.active-b.svelte-1r71rs8{background:#2196f333;border-color:#2196f3;box-shadow:0 0 20px #2196f34d}.antenna-name.svelte-1r71rs8{font-size:14px;font-weight:500;color:var(--text-primary)}.antenna-ports.svelte-1r71rs8{display:flex;gap:6px}.port-btn.svelte-1r71rs8{width:36px;height:36px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-secondary);transition:all .2s}.port-btn.svelte-1r71rs8:hover{border-color:var(--accent-cyan);transform:scale(1.05)}.port-btn.active.svelte-1r71rs8{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000;box-shadow:0 0 12px #4fc3f780}.reboot-btn.svelte-1r71rs8{width:100%;padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;background:linear-gradient(135deg,#ff9800,#f57c00);color:#fff;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:6px;box-shadow:0 4px 12px #ff980066;margin-top:8px}.reboot-btn.svelte-1r71rs8:hover{transform:translateY(-2px);box-shadow:0 6px 16px #ff980080}.reboot-btn.svelte-1r71rs8:active{transform:translateY(0)}.reboot-icon.svelte-1r71rs8{font-size:16px}.card.svelte-1sgx2se{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-1sgx2se{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-1sgx2se{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-1sgx2se{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-1sgx2se{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-1sgx2se{padding:16px;display:flex;flex-direction:column;gap:16px}.heading-display.svelte-1sgx2se{text-align:center;padding:12px;background:#4fc3f71a;border-radius:6px;border:1px solid rgba(79,195,247,.3)}.heading-label.svelte-1sgx2se{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}.heading-value.svelte-1sgx2se{font-size:42px;font-weight:200;color:var(--accent-cyan);text-shadow:0 0 20px rgba(79,195,247,.5)}.map-container.svelte-1sgx2se{display:flex;justify-content:center;padding:10px;background:#0a162899;border-radius:8px}.map-svg.svelte-1sgx2se{width:100%;max-width:300px;height:auto}.cardinal.svelte-1sgx2se{fill:var(--accent-cyan);font-size:18px;font-weight:700;text-shadow:0 0 10px rgba(79,195,247,.8)}.degree-label.svelte-1sgx2se{fill:#4fc3f7b3;font-size:12px;font-weight:600}.goto-container.svelte-1sgx2se{display:grid;grid-template-columns:1fr auto;gap:8px}.heading-input.svelte-1sgx2se{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:10px 12px;font-size:14px;outline:none;transition:all .2s}.heading-input.svelte-1sgx2se:focus{border-color:var(--accent-cyan);box-shadow:0 0 0 2px #4fc3f733}.go-btn.svelte-1sgx2se{padding:10px 24px;border-radius:4px;font-size:13px;font-weight:600;text-transform:uppercase;cursor:pointer;border:none;background:linear-gradient(135deg,var(--accent-cyan),#0288d1);color:#000;transition:all .2s;box-shadow:0 4px 12px #4fc3f766}.go-btn.svelte-1sgx2se:hover{transform:translateY(-2px);box-shadow:0 6px 16px #4fc3f780}.controls.svelte-1sgx2se{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.control-btn.svelte-1sgx2se{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-tertiary);color:var(--text-primary);transition:all .2s;display:flex;flex-direction:column;align-items:center;gap:4px}.control-btn.svelte-1sgx2se:hover{border-color:var(--accent-cyan);transform:translateY(-1px)}.control-btn.stop.svelte-1sgx2se{background:linear-gradient(135deg,#f44336,#d32f2f);border-color:#f44336;color:#fff;box-shadow:0 4px 12px #f4433666}.control-btn.stop.svelte-1sgx2se:hover{box-shadow:0 6px 16px #f4433680}.arrow.svelte-1sgx2se{font-size:20px;line-height:1}.app.svelte-1hrhory.svelte-1hrhory{min-height:100vh;display:flex;flex-direction:column}header.svelte-1hrhory.svelte-1hrhory{background:linear-gradient(135deg,#1e3c72,#2a5298);padding:16px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 8px #0000004d;flex-wrap:wrap;gap:16px}.header-left.svelte-1hrhory.svelte-1hrhory{display:flex;align-items:center;gap:16px}h1.svelte-1hrhory.svelte-1hrhory{font-size:24px;font-weight:500;margin:0;color:#fff}.connection-status.svelte-1hrhory.svelte-1hrhory{display:flex;align-items:center;gap:8px;font-size:14px;padding:6px 12px;background:#0000004d;border-radius:16px}.header-center.svelte-1hrhory.svelte-1hrhory{flex:1;display:flex;justify-content:center}.solar-info.svelte-1hrhory.svelte-1hrhory{display:flex;gap:20px;font-size:14px}.solar-item.svelte-1hrhory.svelte-1hrhory{color:#fffc}.solar-item.svelte-1hrhory .value.svelte-1hrhory{color:var(--accent-teal);font-weight:500;margin-left:4px}.header-right.svelte-1hrhory.svelte-1hrhory{display:flex;gap:20px;align-items:center}.weather-info.svelte-1hrhory.svelte-1hrhory{display:flex;gap:12px;font-size:14px;color:#ffffffe6}.clock.svelte-1hrhory.svelte-1hrhory{display:flex;flex-direction:column;align-items:flex-end}.time.svelte-1hrhory.svelte-1hrhory{font-size:18px;font-weight:500;color:#fff}.date.svelte-1hrhory.svelte-1hrhory{font-size:12px;color:#ffffffb3}main.svelte-1hrhory.svelte-1hrhory{flex:1;padding:24px;overflow-y:auto}.dashboard-grid.svelte-1hrhory.svelte-1hrhory{display:flex;flex-direction:column;gap:24px;max-width:1800px;margin:0 auto}.row.svelte-1hrhory.svelte-1hrhory{display:flex;gap:24px;flex-wrap:wrap}.row.svelte-1hrhory>*{flex:1;min-width:300px}@media (max-width: 1200px){.row.svelte-1hrhory.svelte-1hrhory{flex-direction:column}}@media (max-width: 768px){header.svelte-1hrhory.svelte-1hrhory{flex-direction:column;align-items:flex-start}.header-center.svelte-1hrhory.svelte-1hrhory,.header-right.svelte-1hrhory.svelte-1hrhory{width:100%;justify-content:flex-start}.solar-info.svelte-1hrhory.svelte-1hrhory{flex-wrap:wrap}}:root{--bg-primary: #0a1628;--bg-secondary: #1a2332;--bg-tertiary: #243447;--bg-hover: #2a3f5f;--text-primary: #e0e6ed;--text-secondary: #a0aec0;--text-muted: #718096;--accent-cyan: #4fc3f7;--accent-blue: #2196f3;--accent-green: #4caf50;--accent-orange: #ff9800;--accent-red: #f44336;--accent-purple: #9c27b0;--accent-yellow: #ffc107;--border-color: #2d3748;--border-light: #374151;--card-shadow: 0 1px 3px rgba(0, 0, 0, .3);--card-radius: 6px;--header-height: 56px;--spacing-xs: 4px;--spacing-sm: 8px;--spacing-md: 12px;--spacing-lg: 16px;--spacing-xl: 20px}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:var(--bg-primary);color:var(--text-primary);font-size:13px;line-height:1.4;overflow-x:hidden}.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}header{height:var(--header-height);background:var(--bg-secondary);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;padding:0 var(--spacing-lg);flex-shrink:0}.header-left{display:flex;align-items:center;gap:var(--spacing-lg)}.header-left h1{font-size:16px;font-weight:600;color:var(--accent-cyan);letter-spacing:.5px}.connection-status{display:flex;align-items:center;gap:var(--spacing-sm);font-size:12px;color:var(--text-secondary)}.status-indicator{width:8px;height:8px;border-radius:50%;background:var(--accent-red);transition:background .3s}.status-indicator.status-online{background:var(--accent-green);box-shadow:0 0 8px var(--accent-green)}.header-center{display:flex;gap:var(--spacing-xl)}.solar-info{display:flex;gap:var(--spacing-md);font-size:12px}.solar-item{color:var(--text-secondary)}.solar-item .value{color:var(--accent-cyan);font-weight:600;margin-left:var(--spacing-xs)}.header-right{display:flex;align-items:center;gap:var(--spacing-lg)}.weather-info{display:flex;gap:var(--spacing-md);font-size:12px;color:var(--text-secondary)}.clock{display:flex;flex-direction:column;align-items:flex-end;font-size:11px}.clock .time{font-size:14px;font-weight:600;color:var(--text-primary)}.clock .date{color:var(--text-secondary)}main{flex:1;overflow-y:auto;padding:var(--spacing-md)}.dashboard-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--spacing-md);max-width:1800px;margin:0 auto}.card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--card-radius);padding:var(--spacing-md);box-shadow:var(--card-shadow);transition:border-color .2s}.card:hover{border-color:var(--border-light)}.card h2{font-size:14px;font-weight:600;color:var(--accent-cyan);margin-bottom:var(--spacing-md);display:flex;align-items:center;gap:var(--spacing-sm);letter-spacing:.5px}.card h2:before{content:"";width:3px;height:14px;background:var(--accent-cyan);border-radius:2px}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--accent-green)}.status-dot.disconnected{background:var(--accent-red)}.label{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:var(--spacing-xs)}.value{font-size:18px;font-weight:300;color:var(--text-primary)}button,.button{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:var(--spacing-sm) var(--spacing-md);font-size:12px;font-weight:500;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:.5px}button:hover,.button:hover{background:var(--bg-hover);border-color:var(--border-light)}button:active,.button:active{transform:scale(.98)}button.primary{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000}button.primary:hover{background:#29b6f6;border-color:#29b6f6}button.success{background:var(--accent-green);border-color:var(--accent-green);color:#fff}button.danger{background:var(--accent-red);border-color:var(--accent-red);color:#fff}button:disabled{opacity:.5;cursor:not-allowed}select{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:var(--spacing-sm);font-size:12px;cursor:pointer;outline:none;transition:all .2s}select:hover{border-color:var(--border-light)}select:focus{border-color:var(--accent-cyan)}.badge{display:inline-block;padding:4px 10px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.badge.green{background:#4caf5033;color:var(--accent-green)}.badge.red{background:#f4433633;color:var(--accent-red)}.badge.orange{background:#ff980033;color:var(--accent-orange)}.badge.cyan{background:#4fc3f733;color:var(--accent-cyan)}.badge.purple{background:#9c27b033;color:var(--accent-purple)}.bar{width:100%;height:6px;background:var(--bg-tertiary);border-radius:3px;overflow:hidden;margin:var(--spacing-xs) 0}.bar-fill{height:100%;background:linear-gradient(90deg,var(--accent-green),var(--accent-orange),var(--accent-red));transition:width .3s ease;border-radius:3px}.scale{display:flex;justify-content:space-between;font-size:10px;color:var(--text-muted);margin-top:var(--spacing-xs)}.metrics{display:flex;flex-direction:column;gap:var(--spacing-md)}.metric{display:flex;flex-direction:column;gap:var(--spacing-xs)}.metric-row{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md)}.metric.small{min-width:0}.metric-value{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:var(--bg-tertiary);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--bg-hover)}@media (max-width: 1400px){.dashboard-grid{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}}@media (max-width: 768px){header{flex-direction:column;height:auto;padding:var(--spacing-sm);gap:var(--spacing-sm)}.dashboard-grid{grid-template-columns:1fr}.header-center{order:3;width:100%}} diff --git a/cmd/server/web/dist/index.html b/cmd/server/web/dist/index.html new file mode 100644 index 0000000..b415633 --- /dev/null +++ b/cmd/server/web/dist/index.html @@ -0,0 +1,16 @@ + + + + + + ShackMaster - F4BPO Shack + + + + + + + +
+ + \ No newline at end of file diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index ec35333..fd53fa0 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -79,10 +79,7 @@ func (dm *DeviceManager) Initialize() error { ) // Initialize Rotator Genius -<<<<<<< HEAD log.Printf("Initializing RotatorGenius: host=%s port=%d", dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port) -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 dm.rotatorGenius = rotatorgenius.New( dm.config.Devices.RotatorGenius.Host, dm.config.Devices.RotatorGenius.Port, @@ -98,7 +95,6 @@ func (dm *DeviceManager) Initialize() error { dm.config.Location.Longitude, ) -<<<<<<< HEAD // Start device polling in background (non-blocking) go func() { if err := dm.powerGenius.Start(); err != nil { @@ -126,12 +122,6 @@ func (dm *DeviceManager) Initialize() error { } }() log.Println("RotatorGenius goroutine launched") -======= - // Start PowerGenius continuous polling - if err := dm.powerGenius.Start(); err != nil { - log.Printf("Warning: Failed to start PowerGenius polling: %v", err) - } ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 log.Println("Device manager initialized") return nil @@ -196,7 +186,6 @@ func (dm *DeviceManager) updateStatus() { log.Printf("Power Genius error: %v", err) } -<<<<<<< HEAD // Tuner Genius if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil { status.TunerGenius = tgStatus @@ -217,28 +206,6 @@ func (dm *DeviceManager) updateStatus() { } else { log.Printf("Rotator Genius error: %v", err) } -======= - // // Tuner Genius - // if tgStatus, err := dm.tunerGenius.GetStatus(); err == nil { - // status.TunerGenius = tgStatus - // } else { - // log.Printf("Tuner Genius error: %v", err) - // } - - // // Antenna Genius - // if agStatus, err := dm.antennaGenius.GetStatus(); err == nil { - // status.AntennaGenius = agStatus - // } else { - // log.Printf("Antenna Genius error: %v", err) - // } - - // // Rotator Genius - // if rgStatus, err := dm.rotatorGenius.GetStatus(); err == nil { - // status.RotatorGenius = rgStatus - // } else { - // log.Printf("Rotator Genius error: %v", err) - // } ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Solar Data (fetched every 15 minutes, cached) if solarData, err := dm.solarClient.GetSolarData(); err == nil { diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 1e1ef91..9605554 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -49,18 +49,13 @@ func (s *Server) SetupRoutes() *http.ServeMux { mux.HandleFunc("/api/webswitch/all/off", s.handleWebSwitchAllOff) // Rotator endpoints -<<<<<<< HEAD mux.HandleFunc("/api/rotator/heading", s.handleRotatorHeading) -======= - mux.HandleFunc("/api/rotator/move", s.handleRotatorMove) ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 mux.HandleFunc("/api/rotator/cw", s.handleRotatorCW) mux.HandleFunc("/api/rotator/ccw", s.handleRotatorCCW) mux.HandleFunc("/api/rotator/stop", s.handleRotatorStop) // Tuner endpoints mux.HandleFunc("/api/tuner/operate", s.handleTunerOperate) -<<<<<<< HEAD mux.HandleFunc("/api/tuner/bypass", s.handleTunerBypass) mux.HandleFunc("/api/tuner/autotune", s.handleTunerAutoTune) @@ -71,19 +66,8 @@ func (s *Server) SetupRoutes() *http.ServeMux { // Power Genius endpoints mux.HandleFunc("/api/power/fanmode", s.handlePowerFanMode) mux.HandleFunc("/api/power/operate", s.handlePowerOperate) -======= - mux.HandleFunc("/api/tuner/tune", s.handleTunerAutoTune) - mux.HandleFunc("/api/tuner/antenna", s.handleTunerAntenna) - // Antenna Genius endpoints - mux.HandleFunc("/api/antenna/set", s.handleAntennaSet) - - // Power Genius endpoints - mux.HandleFunc("/api/power/fanmode", s.handlePowerFanMode) ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 - - // Static files (will be frontend) - mux.Handle("/", http.FileServer(http.Dir("./web/dist"))) + // Note: Static files are now served from embedded FS in main.go return mux } @@ -196,23 +180,14 @@ func (s *Server) handleWebSwitchAllOff(w http.ResponseWriter, r *http.Request) { } // Rotator handlers -<<<<<<< HEAD func (s *Server) handleRotatorHeading(w http.ResponseWriter, r *http.Request) { -======= -func (s *Server) handleRotatorMove(w http.ResponseWriter, r *http.Request) { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { -<<<<<<< HEAD Heading int `json:"heading"` -======= - Rotator int `json:"rotator"` - Azimuth int `json:"azimuth"` ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -220,11 +195,7 @@ func (s *Server) handleRotatorMove(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().SetHeading(req.Heading); err != nil { -======= - if err := s.deviceManager.RotatorGenius().MoveToAzimuth(req.Rotator, req.Azimuth); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -238,17 +209,7 @@ func (s *Server) handleRotatorCW(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().RotateCW(); err != nil { -======= - rotator, err := strconv.Atoi(r.URL.Query().Get("rotator")) - if err != nil || rotator < 1 || rotator > 2 { - http.Error(w, "Invalid rotator number", http.StatusBadRequest) - return - } - - if err := s.deviceManager.RotatorGenius().RotateCW(rotator); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -262,17 +223,7 @@ func (s *Server) handleRotatorCCW(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD if err := s.deviceManager.RotatorGenius().RotateCCW(); err != nil { -======= - rotator, err := strconv.Atoi(r.URL.Query().Get("rotator")) - if err != nil || rotator < 1 || rotator > 2 { - http.Error(w, "Invalid rotator number", http.StatusBadRequest) - return - } - - if err := s.deviceManager.RotatorGenius().RotateCCW(rotator); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -302,11 +253,7 @@ func (s *Server) handleTunerOperate(w http.ResponseWriter, r *http.Request) { } var req struct { -<<<<<<< HEAD Value int `json:"value"` -======= - Operate bool `json:"operate"` ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -314,7 +261,6 @@ func (s *Server) handleTunerOperate(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD if err := s.deviceManager.TunerGenius().SetOperate(req.Value); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -339,9 +285,6 @@ func (s *Server) handleTunerBypass(w http.ResponseWriter, r *http.Request) { } if err := s.deviceManager.TunerGenius().SetBypass(req.Value); err != nil { -======= - if err := s.deviceManager.TunerGenius().SetOperate(req.Operate); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -363,22 +306,15 @@ func (s *Server) handleTunerAutoTune(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } -<<<<<<< HEAD // Antenna Genius handlers func (s *Server) handleAntennaSelect(w http.ResponseWriter, r *http.Request) { -======= -func (s *Server) handleTunerAntenna(w http.ResponseWriter, r *http.Request) { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { -<<<<<<< HEAD Port int `json:"port"` -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 Antenna int `json:"antenna"` } @@ -387,11 +323,7 @@ func (s *Server) handleTunerAntenna(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD if err := s.deviceManager.AntennaGenius().SetAntenna(req.Port, req.Antenna); err != nil { -======= - if err := s.deviceManager.TunerGenius().ActivateAntenna(req.Antenna); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -399,32 +331,13 @@ func (s *Server) handleTunerAntenna(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } -<<<<<<< HEAD func (s *Server) handleAntennaReboot(w http.ResponseWriter, r *http.Request) { -======= -// Antenna Genius handlers -func (s *Server) handleAntennaSet(w http.ResponseWriter, r *http.Request) { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } -<<<<<<< HEAD if err := s.deviceManager.AntennaGenius().Reboot(); err != nil { -======= - var req struct { - Radio int `json:"radio"` - Antenna int `json:"antenna"` - } - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if err := s.deviceManager.AntennaGenius().SetRadioAntenna(req.Radio, req.Antenna); err != nil { ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -456,7 +369,6 @@ func (s *Server) handlePowerFanMode(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } -<<<<<<< HEAD func (s *Server) handlePowerOperate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) @@ -480,8 +392,6 @@ func (s *Server) handlePowerOperate(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 func (s *Server) sendJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) diff --git a/internal/devices/antennagenius/antennagenius.go b/internal/devices/antennagenius/antennagenius.go index de6dc0d..2fc21c1 100644 --- a/internal/devices/antennagenius/antennagenius.go +++ b/internal/devices/antennagenius/antennagenius.go @@ -3,7 +3,6 @@ package antennagenius import ( "bufio" "fmt" -<<<<<<< HEAD "log" "net" "strconv" @@ -53,43 +52,17 @@ type Antenna struct { RX string `json:"rx"` InBand string `json:"in_band"` Hotkey int `json:"hotkey"` -======= - "net" - "strconv" - "strings" - "time" - - . "git.rouggy.com/rouggy/ShackMaster/internal/devices" -) - -type Client struct { - host string - port int - conn net.Conn -} - -type Status struct { - Radio1Antenna int `json:"radio1_antenna"` // 0-7 (antenna index) - Radio2Antenna int `json:"radio2_antenna"` // 0-7 (antenna index) - Connected bool `json:"connected"` ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } func New(host string, port int) *Client { return &Client{ -<<<<<<< HEAD host: host, port: port, stopChan: make(chan struct{}), -======= - host: host, - port: port, ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } } func (c *Client) Connect() error { -<<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() @@ -97,26 +70,20 @@ func (c *Client) Connect() error { return nil } -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second) if err != nil { return fmt.Errorf("failed to connect: %w", err) } c.conn = conn -<<<<<<< HEAD c.reader = bufio.NewReader(c.conn) // Read and discard banner _, _ = c.reader.ReadString('\n') -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 return nil } func (c *Client) Close() error { -<<<<<<< HEAD c.connMu.Lock() defer c.connMu.Unlock() @@ -124,15 +91,12 @@ func (c *Client) Close() error { close(c.stopChan) } -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if c.conn != nil { return c.conn.Close() } return nil } -<<<<<<< HEAD func (c *Client) Start() error { if c.running { return nil @@ -299,46 +263,10 @@ func (c *Client) sendCommand(cmd string) (string, error) { func (c *Client) getAntennaList() ([]Antenna, error) { resp, err := c.sendCommand("antenna list") -======= -func (c *Client) sendCommand(cmd string) (string, error) { - if c.conn == nil { - if err := c.Connect(); err != nil { - return "", err - } - } - - // Get next command ID from global counter - cmdID := GetGlobalCommandID().GetNextID() - - // Format command with ID: C| - fullCmd := fmt.Sprintf("C%d|%s\n", cmdID, cmd) - - // Send command - _, err := c.conn.Write([]byte(fullCmd)) - if err != nil { - c.conn = nil - return "", fmt.Errorf("failed to send command: %w", err) - } - - // Read response - reader := bufio.NewReader(c.conn) - response, err := reader.ReadString('\n') - if err != nil { - c.conn = nil - return "", fmt.Errorf("failed to read response: %w", err) - } - - return strings.TrimSpace(response), nil -} - -func (c *Client) GetStatus() (*Status, error) { - resp, err := c.sendCommand("status") ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return nil, err } -<<<<<<< HEAD var antennas []Antenna // Response format: R|0|antenna name= tx= rx= inband= hotkey= @@ -405,58 +333,10 @@ func (c *Client) parseAntennaLine(line string) Antenna { func (c *Client) subscribeToPortUpdates() error { resp, err := c.sendCommand("sub port all") -======= - return c.parseStatus(resp) -} - -func (c *Client) parseStatus(resp string) (*Status, error) { - status := &Status{ - Connected: true, - } - - // Parse response format from 4O3A API - // Expected format will vary - this is a basic parser - pairs := strings.Fields(resp) - - for _, pair := range pairs { - parts := strings.SplitN(pair, "=", 2) - if len(parts) != 2 { - continue - } - - key := parts[0] - value := parts[1] - - switch key { - case "radio1", "r1": - status.Radio1Antenna, _ = strconv.Atoi(value) - case "radio2", "r2": - status.Radio2Antenna, _ = strconv.Atoi(value) - } - } - - return status, nil -} - -// SetRadioAntenna sets which antenna a radio should use -// radio: 1 or 2 -// antenna: 0-7 (antenna index) -func (c *Client) SetRadioAntenna(radio int, antenna int) error { - if radio < 1 || radio > 2 { - return fmt.Errorf("radio must be 1 or 2") - } - if antenna < 0 || antenna > 7 { - return fmt.Errorf("antenna must be between 0 and 7") - } - - cmd := fmt.Sprintf("set radio%d=%d", radio, antenna) - resp, err := c.sendCommand(cmd) ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { return err } -<<<<<<< HEAD // Parse initial port status from subscription response // The response may contain S0|port messages with current status lines := strings.Split(resp, "\n") @@ -465,18 +345,11 @@ func (c *Client) SetRadioAntenna(radio int, antenna int) error { if strings.HasPrefix(line, "S0|port") { c.parsePortStatus(line) } -======= - // Check response for success - if !strings.Contains(strings.ToLower(resp), "ok") && resp != "" { - // If response doesn't contain "ok" but isn't empty, assume success - // (some devices may return the new state instead of "ok") ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } return nil } -<<<<<<< HEAD func (c *Client) parsePortStatus(line string) { // Format: S0|port auto=<0|1> source= band= freq= nickname= rxant= txant= inband= tx=<0|1> inhibit= @@ -561,21 +434,4 @@ func (c *Client) SetAntenna(port, antenna int) error { func (c *Client) Reboot() error { _, err := c.sendCommand("reboot") return err -======= -// GetRadioAntenna gets which antenna a radio is currently using -func (c *Client) GetRadioAntenna(radio int) (int, error) { - if radio < 1 || radio > 2 { - return -1, fmt.Errorf("radio must be 1 or 2") - } - - status, err := c.GetStatus() - if err != nil { - return -1, err - } - - if radio == 1 { - return status.Radio1Antenna, nil - } - return status.Radio2Antenna, nil ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } diff --git a/internal/devices/powergenius/powergenius.go b/internal/devices/powergenius/powergenius.go index a42f212..f854fe3 100644 --- a/internal/devices/powergenius/powergenius.go +++ b/internal/devices/powergenius/powergenius.go @@ -3,10 +3,7 @@ package powergenius import ( "bufio" "fmt" -<<<<<<< HEAD -======= "log" ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 "math" "net" "strconv" @@ -26,6 +23,10 @@ type Client struct { statusMu sync.RWMutex stopChan chan struct{} running bool + + // Auto fan management + autoFanEnabled bool + lastFanMode string // Remember last manual mode } type Status struct { @@ -45,17 +46,19 @@ type Status struct { BandB string `json:"band_b"` FaultPresent bool `json:"fault_present"` Connected bool `json:"connected"` -<<<<<<< HEAD -======= - Meffa string `json:"meffa"` ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 + + // Peak hold for display (internal) + displayPower float64 + peakTime time.Time } func New(host string, port int) *Client { return &Client{ - host: host, - port: port, - stopChan: make(chan struct{}), + host: host, + port: port, + stopChan: make(chan struct{}), + autoFanEnabled: true, // Auto fan management enabled by default + lastFanMode: "Contest", // Default to Contest mode } } @@ -96,24 +99,14 @@ func (c *Client) Close() error { // Start begins continuous polling of the device func (c *Client) Start() error { -<<<<<<< HEAD -======= - if err := c.Connect(); err != nil { - return err - } - ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if c.running { return nil } -<<<<<<< HEAD // Try to connect, but don't fail if it doesn't work // The poll loop will keep trying _ = c.Connect() -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 c.running = true go c.pollLoop() @@ -128,7 +121,6 @@ func (c *Client) pollLoop() { for { select { case <-ticker.C: -<<<<<<< HEAD // Try to reconnect if not connected c.connMu.Lock() if c.conn == nil { @@ -152,12 +144,6 @@ func (c *Client) pollLoop() { status, err := c.queryStatus() if err != nil { // Connection lost, close and retry next tick -======= - status, err := c.queryStatus() - if err != nil { - log.Printf("PowerGenius query error: %v", err) - // Try to reconnect ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 c.connMu.Lock() if c.conn != nil { c.conn.Close() @@ -165,7 +151,6 @@ func (c *Client) pollLoop() { } c.connMu.Unlock() -<<<<<<< HEAD // Mark as disconnected and reset all values c.statusMu.Lock() c.lastStatus = &Status{ @@ -178,21 +163,37 @@ func (c *Client) pollLoop() { // Mark as connected status.Connected = true -======= - if err := c.Connect(); err != nil { - log.Printf("PowerGenius reconnect failed: %v", err) + // Peak hold logic - keep highest power for 1 second + now := time.Now() + if c.lastStatus != nil { + // If new power is higher, update peak + if status.PowerForward > c.lastStatus.displayPower { + status.displayPower = status.PowerForward + status.peakTime = now + } else { + // Check if peak has expired (1 second) + if now.Sub(c.lastStatus.peakTime) < 1*time.Second { + // Keep old peak + status.displayPower = c.lastStatus.displayPower + status.peakTime = c.lastStatus.peakTime + } else { + // Peak expired, use current value + status.displayPower = status.PowerForward + status.peakTime = now + } } - continue + } else { + status.displayPower = status.PowerForward + status.peakTime = now } ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 + // Override PowerForward with display power for frontend + status.PowerForward = status.displayPower + // Merge with existing status (spontaneous messages may only update some fields) c.statusMu.Lock() if c.lastStatus != nil { // Keep existing values for fields not in the new status - if status.PowerForward == 0 && c.lastStatus.PowerForward != 0 { - status.PowerForward = c.lastStatus.PowerForward - } if status.Temperature == 0 && c.lastStatus.Temperature != 0 { status.Temperature = c.lastStatus.Temperature } @@ -215,6 +216,32 @@ func (c *Client) pollLoop() { c.lastStatus = status c.statusMu.Unlock() + // Auto fan management based on temperature + if c.autoFanEnabled { + temp := status.Temperature + currentMode := status.FanMode + + // If temp >= 60°C, switch to Broadcast + if temp >= 60.0 && currentMode != "Broadcast" { + log.Printf("PowerGenius: Temperature %.1f°C >= 60°C, switching fan to Broadcast mode", temp) + if err := c.SetFanMode("Broadcast"); err != nil { + log.Printf("PowerGenius: Failed to set fan mode: %v", err) + } + } + + // If temp <= 55°C, switch back to Contest (or last manual mode) + if temp <= 55.0 && currentMode == "Broadcast" { + targetMode := c.lastFanMode + if targetMode == "" || targetMode == "Broadcast" { + targetMode = "Contest" + } + log.Printf("PowerGenius: Temperature %.1f°C <= 55°C, switching fan back to %s mode", temp, targetMode) + if err := c.SetFanMode(targetMode); err != nil { + log.Printf("PowerGenius: Failed to set fan mode: %v", err) + } + } + } + case <-c.stopChan: return } @@ -361,11 +388,6 @@ func (c *Client) parseStatus(resp string) (*Status, error) { } case "vac": status.Voltage, _ = strconv.ParseFloat(value, 64) -<<<<<<< HEAD -======= - case "meffa": - status.Meffa = value ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 case "vdd": status.VDD, _ = strconv.ParseFloat(value, 64) case "id": @@ -430,15 +452,20 @@ func (c *Client) SetFanMode(mode string) error { "BROADCAST": true, } - if !validModes[mode] { + // Normalize mode to title case for comparison + modeUpper := strings.ToUpper(mode) + if !validModes[modeUpper] { return fmt.Errorf("invalid fan mode: %s (must be STANDARD, CONTEST, or BROADCAST)", mode) } - cmd := fmt.Sprintf("setup fanmode=%s", mode) + // Remember last manual mode (if not triggered by auto-fan) + // We store it in title case: "Standard", "Contest", "Broadcast" + c.lastFanMode = strings.Title(strings.ToLower(mode)) + + cmd := fmt.Sprintf("setup fanmode=%s", modeUpper) _, err := c.sendCommand(cmd) return err } -<<<<<<< HEAD // SetOperate sets the operate mode // value can be: 0 (STANDBY) or 1 (OPERATE) @@ -451,5 +478,3 @@ func (c *Client) SetOperate(value int) error { _, err := c.sendCommand(cmd) return err } -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 diff --git a/internal/devices/rotatorgenius/rotatorgenius.go b/internal/devices/rotatorgenius/rotatorgenius.go index 536bed3..ee98fc0 100644 --- a/internal/devices/rotatorgenius/rotatorgenius.go +++ b/internal/devices/rotatorgenius/rotatorgenius.go @@ -8,8 +8,6 @@ import ( "strings" "sync" "time" - - . "git.rouggy.com/rouggy/ShackMaster/internal/devices" ) type Client struct { @@ -151,18 +149,7 @@ func (c *Client) sendCommand(cmd string) error { return fmt.Errorf("not connected") } -<<<<<<< HEAD _, err := c.conn.Write([]byte(cmd)) -======= - // Get next command ID from global counter - cmdID := GetGlobalCommandID().GetNextID() - - // Format command with ID: C| - fullCmd := fmt.Sprintf("C%d%s", cmdID, cmd) - - // Send command - _, err := c.conn.Write([]byte(fullCmd)) ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 if err != nil { c.conn = nil c.reader = nil diff --git a/internal/devices/tunergenius/tunergenius.go b/internal/devices/tunergenius/tunergenius.go index 77dee31..61225e7 100644 --- a/internal/devices/tunergenius/tunergenius.go +++ b/internal/devices/tunergenius/tunergenius.go @@ -14,7 +14,6 @@ import ( ) type Client struct { -<<<<<<< HEAD host string port int conn net.Conn @@ -23,11 +22,6 @@ type Client struct { statusMu sync.RWMutex stopChan chan struct{} running bool -======= - host string - port int - conn net.Conn ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } type Status struct { @@ -54,18 +48,17 @@ type Status struct { RelayC2 int `json:"c2"` TuningStatus string `json:"tuning_status"` Connected bool `json:"connected"` + + // Peak hold for display (internal) + displayPower float64 + peakTime time.Time } func New(host string, port int) *Client { return &Client{ -<<<<<<< HEAD host: host, port: port, stopChan: make(chan struct{}), -======= - host: host, - port: port, ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } } @@ -110,7 +103,6 @@ func (c *Client) Start() error { return nil } -<<<<<<< HEAD // Try to connect, but don't fail if it doesn't work // The poll loop will keep trying _ = c.Connect() @@ -171,6 +163,33 @@ func (c *Client) pollLoop() { // Mark as connected status.Connected = true + // Peak hold logic - keep highest power for 1 second + now := time.Now() + if c.lastStatus != nil { + // If new power is higher, update peak + if status.PowerForward > c.lastStatus.displayPower { + status.displayPower = status.PowerForward + status.peakTime = now + } else { + // Check if peak has expired (1 second) + if now.Sub(c.lastStatus.peakTime) < 1*time.Second { + // Keep old peak + status.displayPower = c.lastStatus.displayPower + status.peakTime = c.lastStatus.peakTime + } else { + // Peak expired, use current value + status.displayPower = status.PowerForward + status.peakTime = now + } + } + } else { + status.displayPower = status.PowerForward + status.peakTime = now + } + + // Override PowerForward with display power for frontend + status.PowerForward = status.displayPower + c.statusMu.Lock() c.lastStatus = status c.statusMu.Unlock() @@ -221,8 +240,6 @@ func (c *Client) sendCommand(cmd string) (string, error) { return "", fmt.Errorf("not connected") } -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 // Get next command ID from global counter cmdID := GetGlobalCommandID().GetNextID() diff --git a/internal/devices/webswitch/webswitch.go b/internal/devices/webswitch/webswitch.go index 40d205b..dcce155 100644 --- a/internal/devices/webswitch/webswitch.go +++ b/internal/devices/webswitch/webswitch.go @@ -142,12 +142,8 @@ func (c *Client) GetStatus() (*Status, error) { // Parse response format: "1,1\n2,1\n3,1\n4,1\n5,0\n" status := &Status{ -<<<<<<< HEAD Relays: make([]RelayState, 0, 5), Connected: true, -======= - Relays: make([]RelayState, 0, 5), ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } lines := strings.Split(strings.TrimSpace(string(body)), "\n") diff --git a/web/index.html b/web/index.html index fc899f8..d89e88a 100644 --- a/web/index.html +++ b/web/index.html @@ -3,11 +3,7 @@ -<<<<<<< HEAD ShackMaster - F4BPO Shack -======= - ShackMaster - XV9Q Shack ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 diff --git a/web/src/app.css b/web/src/app.css index d321249..c797f93 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,5 +1,4 @@ :root { -<<<<<<< HEAD /* Modern dark theme inspired by FlexDXCluster */ --bg-primary: #0a1628; --bg-secondary: #1a2332; @@ -436,132 +435,4 @@ select:focus { order: 3; width: 100%; } -======= - --bg-primary: #1a1a1a; - --bg-secondary: #2a2a2a; - --bg-card: #333333; - --text-primary: #ffffff; - --text-secondary: #b0b0b0; - --accent-teal: #00bcd4; - --accent-green: #4caf50; - --accent-red: #f44336; - --accent-blue: #2196f3; - --border-color: #444444; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - margin: 0; - padding: 0; - font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background-color: var(--bg-primary); - color: var(--text-primary); - overflow-x: hidden; -} - -#app { - min-height: 100vh; -} - -button { - font-family: inherit; - cursor: pointer; - border: none; - outline: none; - transition: all 0.2s ease; -} - -button:hover { - transform: translateY(-1px); -} - -button:active { - transform: translateY(0); -} - -input, select { - font-family: inherit; - outline: none; -} - -.card { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 16px; -} - -.status-indicator { - display: inline-block; - width: 12px; - height: 12px; - border-radius: 50%; - margin-right: 8px; -} - -.status-online { - background-color: var(--accent-green); - box-shadow: 0 0 8px var(--accent-green); -} - -.status-offline { - background-color: var(--text-secondary); -} - -.btn { - padding: 12px 24px; - border-radius: 6px; - font-weight: 500; - font-size: 14px; - text-transform: uppercase; - letter-spacing: 0.5px; - transition: all 0.2s ease; -} - -.btn-primary { - background: var(--accent-green); - color: white; -} - -.btn-primary:hover { - background: #45a049; -} - -.btn-danger { - background: var(--accent-red); - color: white; -} - -.btn-danger:hover { - background: #da190b; -} - -.btn-secondary { - background: var(--bg-secondary); - color: var(--text-primary); - border: 1px solid var(--border-color); -} - -.btn-secondary:hover { - background: var(--bg-card); -} - -.value-display { - font-size: 24px; - font-weight: 300; - color: var(--accent-teal); -} - -.label { - font-size: 12px; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 1px; - margin-bottom: 4px; ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 } \ No newline at end of file diff --git a/web/src/components/AntennaGenius.svelte b/web/src/components/AntennaGenius.svelte index 9565e57..1d7d596 100644 --- a/web/src/components/AntennaGenius.svelte +++ b/web/src/components/AntennaGenius.svelte @@ -1,6 +1,5 @@ -<<<<<<< HEAD

Antenna Genius

@@ -88,29 +72,30 @@ {#each antennas as antenna} {@const isPortATx = portA.tx && portA.tx_ant === antenna.number} {@const isPortBTx = portB.tx && portB.tx_ant === antenna.number} - {@const isPortARx = !portA.tx && (portA.rx_ant === antenna.number || portA.tx_ant === antenna.number)} - {@const isPortBRx = !portB.tx && (portB.rx_ant === antenna.number || portB.tx_ant === antenna.number)} + {@const isPortARx = !portA.tx && portA.rx_ant === antenna.number} + {@const isPortBRx = !portB.tx && portB.rx_ant === antenna.number} {@const isTx = isPortATx || isPortBTx} - {@const isActive = isPortARx || isPortBRx} + {@const isActiveA = isPortARx || isPortATx} + {@const isActiveB = isPortBRx || isPortBTx}
{antenna.name}
-======= -
-

- AG 8X2 - -

- -
-
Radio 1 / Radio 2
- -
-
-
Radio 1
-
- {#each Array(4) as _, i} - - {/each} -
-
- -
-
Radio 2
-
- {#each Array(4) as _, i} - - {/each} -
-
-
->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7
\ No newline at end of file diff --git a/web/src/components/PowerGenius.svelte b/web/src/components/PowerGenius.svelte index b314495..518f045 100644 --- a/web/src/components/PowerGenius.svelte +++ b/web/src/components/PowerGenius.svelte @@ -1,5 +1,4 @@ - -
-

- PGXL - -

- -
-
- {displayState} -
-
- -
-
-
FWD PWR (W)
-
{powerForward.toFixed(1)}
-
-
-
-
- 0 - 1000 - 2000 -
-
- -
-
PG XL SWR 1:1.00 use
-
{swr.toFixed(2)}
-
- -
-
Temp / HL Temp
-
{temperature.toFixed(0)}°C / {harmonicLoadTemp.toFixed(1)}°C
-
-
-
-
- 25 - 55 - 80 -
-
- -
-
-
VAC
-
{voltage.toFixed(0)}
-
-
-
VDD
-
{vdd.toFixed(1)}
-
-
-
ID peak
-
{peakCurrent.toFixed(1)}
-
-
- -
-
Fan Speed
- -
- -
-
Band A
-
{bandA}
-
-
-
Band B
-
{bandB}
-
-
-
- - - - \ No newline at end of file diff --git a/web/src/components/RotatorGenius.svelte b/web/src/components/RotatorGenius.svelte index 1525d1e..d8af86a 100644 --- a/web/src/components/RotatorGenius.svelte +++ b/web/src/components/RotatorGenius.svelte @@ -1,5 +1,4 @@
@@ -209,7 +63,7 @@
- + @@ -219,137 +73,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - + + + - + - - - + + - - + + stroke-width="2" + style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/> - - + + - - + + - N - E - S - W + N + E + S + W - - {#each [30, 60, 120, 150, 210, 240, 300, 330] as angle} - {@const x = 250 + 215 * Math.sin(angle * Math.PI / 180)} - {@const y = 250 - 215 * Math.cos(angle * Math.PI / 180)} + + {#each [45, 135, 225, 315] as angle} + {@const x = 150 + 125 * Math.sin(angle * Math.PI / 180)} + {@const y = 150 - 125 * Math.cos(angle * Math.PI / 180)} {angle}° {/each} @@ -467,7 +243,7 @@ .map-svg { width: 100%; - max-width: 500px; + max-width: 300px; height: auto; } @@ -569,187 +345,5 @@ .arrow { font-size: 20px; line-height: 1; -======= - - // Preset directions - const presets = [ - { name: 'EU-0', heading: 0 }, - { name: 'JA-35', heading: 35 }, - { name: 'AS-75', heading: 75 }, - { name: 'VK-120', heading: 120 }, - { name: 'AF-180', heading: 180 }, - { name: 'SA-230', heading: 230 }, - { name: 'WI-270', heading: 270 }, - { name: 'NA-300', heading: 300 } - ]; - - async function gotoPreset(heading) { - try { - await api.rotator.move(1, heading); - } catch (err) { - console.error('Failed to move to preset:', err); - } - } - - -
-

- ROTATOR GENIUS - -

- -
- CURRENT HEADING: {currentHeading}° -
- - {#if moving > 0} -
- {moving === 1 ? '↻ ROTATING CW' : '↺ ROTATING CCW'} -
- {/if} - - - -
-
- - -
- -
- - - -
-
- -
- {#each presets as preset} - - {/each} -
-
- - \ No newline at end of file diff --git a/web/src/components/TunerGenius.svelte b/web/src/components/TunerGenius.svelte index 571dc5a..5dd3f1f 100644 --- a/web/src/components/TunerGenius.svelte +++ b/web/src/components/TunerGenius.svelte @@ -1,6 +1,5 @@ -<<<<<<< HEAD

Tuner Genius XL

@@ -484,218 +440,10 @@ border-color: var(--accent-cyan); color: #000; box-shadow: 0 0 15px rgba(79, 195, 247, 0.5); -======= -
-

- TGXL - -

- -
-
Power 0.0w
-
1500
-
1650
-
- -
-
-
TG XL SWR 1.00 use
-
- -
- - - -
- -
-
-
{c1}
-
C1
-
-
-
{l}
-
L
-
-
-
{c2}
-
C2
-
-
-
- -
-
-
Tuning Status
-
- {tuningStatus} -
-
-
- -
-
-
Frequency A
-
{(frequencyA / 1000).toFixed(3)}
-
-
-
Frequency B
-
{(frequencyB / 1000).toFixed(3)}
-
-
- -
- - -
- - -
- - \ No newline at end of file diff --git a/web/src/components/WebSwitch.svelte b/web/src/components/WebSwitch.svelte index 296e1dd..65e1506 100644 --- a/web/src/components/WebSwitch.svelte +++ b/web/src/components/WebSwitch.svelte @@ -4,10 +4,7 @@ export let status; $: relays = status?.relays || []; -<<<<<<< HEAD $: connected = status?.connected || false; -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 const relayNames = { 1: 'Power Supply', @@ -55,7 +52,6 @@ } -<<<<<<< HEAD

WebSwitch

@@ -99,40 +95,10 @@ ALL OFF
-======= -
-

- 1216RH - 0} class:status-offline={relays.length === 0}> -

- -
- {#each [1, 2, 3, 4, 5] as relayNum} - {@const relay = relays.find(r => r.number === relayNum)} - {@const isOn = relay?.state || false} -
- {relayNames[relayNum]} - -
- {/each} -
- -
- - ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7
\ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 56121cc..3bdcde9 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -47,7 +47,6 @@ export const api = { // Tuner tuner: { -<<<<<<< HEAD setOperate: (value) => request('/tuner/operate', { method: 'POST', body: JSON.stringify({ value }), @@ -57,33 +56,15 @@ export const api = { body: JSON.stringify({ value }), }), autoTune: () => request('/tuner/autotune', { method: 'POST' }), -======= - operate: (operate) => request('/tuner/operate', { - method: 'POST', - body: JSON.stringify({ operate }), - }), - tune: () => request('/tuner/tune', { method: 'POST' }), - antenna: (antenna) => request('/tuner/antenna', { - method: 'POST', - body: JSON.stringify({ antenna }), - }), ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, // Antenna Genius antenna: { -<<<<<<< HEAD selectAntenna: (port, antenna) => request('/antenna/select', { method: 'POST', body: JSON.stringify({ port, antenna }), }), reboot: () => request('/antenna/reboot', { method: 'POST' }), -======= - set: (radio, antenna) => request('/antenna/set', { - method: 'POST', - body: JSON.stringify({ radio, antenna }), - }), ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, // Power Genius @@ -92,7 +73,6 @@ export const api = { method: 'POST', body: JSON.stringify({ mode }), }), -<<<<<<< HEAD setOperate: (value) => request('/power/operate', { method: 'POST', body: JSON.stringify({ value }), @@ -108,7 +88,5 @@ export const api = { rotateCW: () => request('/rotator/cw', { method: 'POST' }), rotateCCW: () => request('/rotator/ccw', { method: 'POST' }), stop: () => request('/rotator/stop', { method: 'POST' }), -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 }, }; \ No newline at end of file diff --git a/web/src/lib/websocket.js b/web/src/lib/websocket.js index d56f99f..ea195e3 100644 --- a/web/src/lib/websocket.js +++ b/web/src/lib/websocket.js @@ -28,10 +28,7 @@ class WebSocketService { const message = JSON.parse(event.data); if (message.type === 'update') { -<<<<<<< HEAD console.log('System status updated:', message.data); -======= ->>>>>>> 4ab192418e21065c68d59777493ea03b76c061e7 systemStatus.set(message.data); lastUpdate.set(new Date(message.timestamp)); }