feat(api): enhance neighbor scopes API with served scopes

Added a new `served` field to the neighbor scopes API response, which includes the node's own advertised scopes. This allows clients to identify which scopes they already share with neighbors without needing to re-derive the rules. Updated the OpenAPI specification to reflect this change and added a new private method to retrieve the served scopes. Additionally, rebuilt the UI assets to incorporate these changes.
This commit is contained in:
agessaman
2026-07-29 13:17:15 -07:00
parent 4585094a29
commit 2b8be8cb7e
60 changed files with 199 additions and 119 deletions
+27 -1
View File
@@ -2415,6 +2415,11 @@ class APIEndpoints:
answer meaning it serves unscoped traffic only. ``status``/``queried_at``
describe the most recent query, which may have failed after a good answer,
so ``responded_at`` is how fresh the scopes themselves are.
``served`` carries this node's own scopes in the same comma-separated form,
so a client can tell which of a neighbour's scopes it already shares without
re-deriving the rule (the wildcard plus every allow-flood region). It is the
very string this node sends when a neighbour asks *it* the same question.
"""
self._set_cors_headers()
@@ -2424,11 +2429,32 @@ class APIEndpoints:
try:
storage = self._get_storage()
scopes = storage.get_neighbor_scopes() or {}
return self._success(scopes, count=len(scopes))
# Keyword cannot be named `self` here -- it would collide with the
# method's own first argument.
return self._success(
scopes, count=len(scopes), served={"scopes": self._served_scopes()}
)
except Exception as e:
logger.error(f"Error reading neighbour scopes: {e}")
return self._error(str(e))
def _served_scopes(self) -> str:
"""This node's advertised region scopes, or "" when they cannot be read.
Deliberately the same formatter the anon-regions responder uses, rather
than a second reading of the transport-key table: whatever a neighbour
would be told is what a client comparing against it has to see.
"""
login_helper = getattr(self.daemon_instance, "login_helper", None)
formatter = getattr(login_helper, "_format_region_names", None) if login_helper else None
if not callable(formatter):
return ""
try:
return formatter() or ""
except Exception as e:
logger.debug(f"Could not read served region scopes: {e}")
return ""
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.tools.json_in()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
.ml-0[data-v-164d2832]{margin-left:0}.ml-4[data-v-164d2832]{margin-left:1rem}.ml-8[data-v-164d2832]{margin-left:2rem}.ml-12[data-v-164d2832]{margin-left:3rem}.ml-16[data-v-164d2832]{margin-left:4rem}.ml-20[data-v-164d2832]{margin-left:5rem}.ml-24[data-v-164d2832]{margin-left:6rem}.ml-28[data-v-164d2832]{margin-left:7rem}.ml-32[data-v-164d2832]{margin-left:8rem}.dropdown-enter-active[data-v-a57c4601],.dropdown-leave-active[data-v-a57c4601]{transition:opacity .12s,transform .12s}.dropdown-enter-from[data-v-a57c4601],.dropdown-leave-to[data-v-a57c4601]{opacity:0;transform:translateY(-4px)}.expand-enter-active[data-v-5bf02bea],.expand-leave-active[data-v-5bf02bea]{transition:all .2s;overflow:hidden}.expand-enter-from[data-v-5bf02bea],.expand-leave-to[data-v-5bf02bea]{opacity:0;max-height:0}.expand-enter-to[data-v-5bf02bea],.expand-leave-from[data-v-5bf02bea]{opacity:1;max-height:2000px}
.ml-0[data-v-72d5e310]{margin-left:0}.ml-4[data-v-72d5e310]{margin-left:1rem}.ml-8[data-v-72d5e310]{margin-left:2rem}.ml-12[data-v-72d5e310]{margin-left:3rem}.ml-16[data-v-72d5e310]{margin-left:4rem}.ml-20[data-v-72d5e310]{margin-left:5rem}.ml-24[data-v-72d5e310]{margin-left:6rem}.ml-28[data-v-72d5e310]{margin-left:7rem}.ml-32[data-v-72d5e310]{margin-left:8rem}.dropdown-enter-active[data-v-a57c4601],.dropdown-leave-active[data-v-a57c4601]{transition:opacity .12s,transform .12s}.dropdown-enter-from[data-v-a57c4601],.dropdown-leave-to[data-v-a57c4601]{opacity:0;transform:translateY(-4px)}.expand-enter-active[data-v-5bf02bea],.expand-leave-active[data-v-5bf02bea]{transition:all .2s;overflow:hidden}.expand-enter-from[data-v-5bf02bea],.expand-leave-to[data-v-5bf02bea]{opacity:0;max-height:0}.expand-enter-to[data-v-5bf02bea],.expand-leave-from[data-v-5bf02bea]{opacity:1;max-height:2000px}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
import{C as e,D as t,K as n,N as r,P as i,T as a,U as o,_t as s,c,h as l,ht as ee,i as te,j as u,l as d,m as f,o as p,p as m,r as h,s as g,u as _,x as ne,y as re,z as ie}from"./runtime-core.esm-bundler-CINEgm0a.js";import{a as ae,l as oe,o as se}from"./runtime-dom.esm-bundler-B3VeUO8l.js";import{t as ce}from"./api-CTDWH2Cm.js";import{t as le}from"./index-zOQrFmWP.js";import{_ as ue,c as de,f as fe,l as pe,m as me,o as v,p as he,t as y,u as ge,v as _e,y as ve}from"./ChartCard-BIn24ayw.js";import"./chartjs-adapter-date-fns.esm-B4XQpUBl.js";var ye={class:`space-y-5`,"data-testid":`neighbour-links-page`},be={class:`flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between`},xe={class:`flex flex-wrap items-center gap-2`},Se=[`value`],Ce={class:`grid grid-cols-2 lg:grid-cols-4 gap-3`},we={class:`glass-card rounded-[15px] p-4`},Te={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-total-links`},Ee={class:`glass-card rounded-[15px] p-4`},De={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-active-links`},Oe={class:`glass-card rounded-[15px] p-4`},ke={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-avg-score`},Ae={class:`glass-card rounded-[15px] p-4`},je={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-duplicate-ratio`},Me={class:`glass-card rounded-[15px] p-4 h-[320px]`},Ne={class:`glass-card rounded-[15px] p-4`,"data-testid":`links-table-wrapper`},Pe={class:`flex flex-col gap-3 md:flex-row md:items-center md:justify-between mb-3`},Fe={class:`flex flex-wrap items-center gap-2`},Ie=[`value`],Le={class:`overflow-x-auto`,"data-testid":`links-table-scroll`},Re={class:`w-full min-w-[980px] text-sm`,"data-testid":`links-table`},ze={class:`text-left border-b border-stroke-subtle`},Be={class:`py-2 pr-3`},Ve={class:`py-2 pr-3`},He={class:`py-2 pr-3`},Ue={class:`py-2 pr-3`},We={class:`py-2 pr-3`},Ge={class:`py-2 pr-3`},Ke=[`onClick`],qe={class:`py-2 pr-3 font-mono text-content-primary`},Je={class:`py-2 pr-3 text-content-primary`},Ye={class:`py-2 pr-3 text-content-primary`},Xe={class:`py-2 pr-3 text-content-primary`},Ze={class:`py-2 pr-3 text-content-primary`},Qe={class:`py-2 pr-3 text-content-primary`},$e={class:`py-2 pr-3 text-content-primary`},et={class:`py-2 pr-3 text-content-secondary`},tt={class:`py-2 pr-0`},nt={key:0},rt={key:0,class:`grid grid-cols-1 xl:grid-cols-3 gap-4`},it={class:`glass-card rounded-[15px] p-4 xl:col-span-2`},at={class:`mt-2 grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm`},ot={class:`font-mono text-content-primary`,"data-testid":`selected-peer`},st={class:`text-content-primary`},ct={class:`text-content-primary`},lt={class:`text-content-primary`},ut={class:`text-content-primary`,"data-testid":`selected-latest-rx-score`},dt={class:`text-content-primary`,"data-testid":`selected-ewma-rx-score`},ft={class:`text-content-primary`},pt={class:`glass-card rounded-[15px] p-4`,"data-testid":`stability-panel`},mt={class:`mt-2 text-xl font-semibold text-content-primary`,"data-testid":`stability-status`},ht={class:`text-sm text-content-secondary mt-1`},gt={class:`mt-3 text-sm text-content-secondary`},_t={class:`text-content-primary`},vt={class:`text-sm text-content-secondary`},yt={class:`text-content-primary`,"data-testid":`rx-score-stddev`},bt={class:`text-xs text-content-secondary mt-2`},xt={key:1,class:`grid grid-cols-1 xl:grid-cols-3 gap-4`,"data-testid":`history-charts`},St={class:`glass-card rounded-[15px] p-4 h-[280px]`},Ct={class:`glass-card rounded-[15px] p-4 h-[280px]`},wt={class:`glass-card rounded-[15px] p-4 h-[280px]`},Tt={key:2,class:`glass-card rounded-[15px] p-6 text-center`,"data-testid":`page-empty`},Et={class:`modal-card max-w-2xl`},Dt={class:`flex items-center justify-between mb-4`},Ot={class:`modal-actions mt-5`},kt=1e3,At=`RX score is a normalised value from 0 to 1 calculated by Core from received SNR, spreading factor and complete received frame length.
import{C as e,D as t,K as n,N as r,P as i,T as a,U as o,_t as s,c,h as l,ht as ee,i as te,j as u,l as d,m as f,o as p,p as m,r as h,s as g,u as _,x as ne,y as re,z as ie}from"./runtime-core.esm-bundler-CINEgm0a.js";import{a as ae,l as oe,o as se}from"./runtime-dom.esm-bundler-B3VeUO8l.js";import{t as ce}from"./api-CTBjApNX.js";import{t as le}from"./index-Cd7zc3iG.js";import{_ as ue,c as de,f as fe,l as pe,m as me,o as v,p as he,t as y,u as ge,v as _e,y as ve}from"./ChartCard-DvNDqE-i.js";import"./chartjs-adapter-date-fns.esm-C1pzr0Ah.js";var ye={class:`space-y-5`,"data-testid":`neighbour-links-page`},be={class:`flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between`},xe={class:`flex flex-wrap items-center gap-2`},Se=[`value`],Ce={class:`grid grid-cols-2 lg:grid-cols-4 gap-3`},we={class:`glass-card rounded-[15px] p-4`},Te={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-total-links`},Ee={class:`glass-card rounded-[15px] p-4`},De={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-active-links`},Oe={class:`glass-card rounded-[15px] p-4`},ke={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-avg-score`},Ae={class:`glass-card rounded-[15px] p-4`},je={class:`text-2xl font-semibold text-content-primary mt-1`,"data-testid":`kpi-duplicate-ratio`},Me={class:`glass-card rounded-[15px] p-4 h-[320px]`},Ne={class:`glass-card rounded-[15px] p-4`,"data-testid":`links-table-wrapper`},Pe={class:`flex flex-col gap-3 md:flex-row md:items-center md:justify-between mb-3`},Fe={class:`flex flex-wrap items-center gap-2`},Ie=[`value`],Le={class:`overflow-x-auto`,"data-testid":`links-table-scroll`},Re={class:`w-full min-w-[980px] text-sm`,"data-testid":`links-table`},ze={class:`text-left border-b border-stroke-subtle`},Be={class:`py-2 pr-3`},Ve={class:`py-2 pr-3`},He={class:`py-2 pr-3`},Ue={class:`py-2 pr-3`},We={class:`py-2 pr-3`},Ge={class:`py-2 pr-3`},Ke=[`onClick`],qe={class:`py-2 pr-3 font-mono text-content-primary`},Je={class:`py-2 pr-3 text-content-primary`},Ye={class:`py-2 pr-3 text-content-primary`},Xe={class:`py-2 pr-3 text-content-primary`},Ze={class:`py-2 pr-3 text-content-primary`},Qe={class:`py-2 pr-3 text-content-primary`},$e={class:`py-2 pr-3 text-content-primary`},et={class:`py-2 pr-3 text-content-secondary`},tt={class:`py-2 pr-0`},nt={key:0},rt={key:0,class:`grid grid-cols-1 xl:grid-cols-3 gap-4`},it={class:`glass-card rounded-[15px] p-4 xl:col-span-2`},at={class:`mt-2 grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm`},ot={class:`font-mono text-content-primary`,"data-testid":`selected-peer`},st={class:`text-content-primary`},ct={class:`text-content-primary`},lt={class:`text-content-primary`},ut={class:`text-content-primary`,"data-testid":`selected-latest-rx-score`},dt={class:`text-content-primary`,"data-testid":`selected-ewma-rx-score`},ft={class:`text-content-primary`},pt={class:`glass-card rounded-[15px] p-4`,"data-testid":`stability-panel`},mt={class:`mt-2 text-xl font-semibold text-content-primary`,"data-testid":`stability-status`},ht={class:`text-sm text-content-secondary mt-1`},gt={class:`mt-3 text-sm text-content-secondary`},_t={class:`text-content-primary`},vt={class:`text-sm text-content-secondary`},yt={class:`text-content-primary`,"data-testid":`rx-score-stddev`},bt={class:`text-xs text-content-secondary mt-2`},xt={key:1,class:`grid grid-cols-1 xl:grid-cols-3 gap-4`,"data-testid":`history-charts`},St={class:`glass-card rounded-[15px] p-4 h-[280px]`},Ct={class:`glass-card rounded-[15px] p-4 h-[280px]`},wt={class:`glass-card rounded-[15px] p-4 h-[280px]`},Tt={key:2,class:`glass-card rounded-[15px] p-6 text-center`,"data-testid":`page-empty`},Et={class:`modal-card max-w-2xl`},Dt={class:`flex items-center justify-between mb-4`},Ot={class:`modal-actions mt-5`},kt=1e3,At=`RX score is a normalised value from 0 to 1 calculated by Core from received SNR, spreading factor and complete received frame length.
A higher value generally means greater reception margin and/or a shorter frame. It should not be interpreted as a pure signal-strength measurement.`,jt=`The proportion of physical observations recorded as duplicates.
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{D as e,T as t,_t as n,h as r,ht as i,l as a,o,r as s,s as c,u as l}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as u}from"./system-AMlobbOe.js";import{t as d}from"./index-zOQrFmWP.js";var f={class:`space-y-4`},p={class:`glass-card rounded-[15px] p-4 sm:p-6`},m={class:`mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`},h={class:`text-xs uppercase tracking-wide text-content-muted`},g={class:`mt-2 text-lg font-semibold text-content-heading`},_={key:0,class:`glass-card rounded-[15px] p-5 text-content-muted`},v={class:`flex flex-wrap items-center justify-between gap-3`},y={class:`text-lg font-semibold text-content-heading`},b={class:`text-sm text-content-muted`},x={class:`mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2`},S={class:`text-sm`},C={class:`ml-2 text-content-heading`},w={key:0,class:`text-sm`},T={class:`ml-2 text-accent-red`},E={class:`mt-4 overflow-x-auto rounded-[12px] border border-stroke-subtle dark:border-white/opacity-light`},D={class:`min-w-full text-sm`},O={class:`px-3 py-2 font-medium text-content-heading`},k={class:`px-3 py-2 text-content-muted break-all`},A={key:0},j={key:1,class:`glass-card rounded-[15px] p-5 text-content-muted`},M=r({name:`SensorsView`,__name:`Sensors`,setup(r){let M=u(),N=o(()=>M.stats?.sensors??null),P=o(()=>N.value?.readings??[]),F=o(()=>{let e=N.value;return e?[{label:`Enabled`,value:e.enabled?`Yes`:`No`},{label:`Running`,value:e.running?`Yes`:`No`},{label:`Configured / Loaded`,value:`${e.configured??0} / ${e.loaded??0}`},{label:`Poll Interval`,value:typeof e.poll_interval_seconds==`number`?`${e.poll_interval_seconds.toFixed(1)}s`:`n/a`}]:[{label:`Enabled`,value:`n/a`},{label:`Running`,value:`n/a`},{label:`Configured`,value:`n/a`},{label:`Poll Interval`,value:`n/a`}]}),I=e=>{if(e==null)return`n/a`;if(typeof e==`boolean`)return e?`true`:`false`;if(typeof e==`number`)return Number.isFinite(e)?String(e):`n/a`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}},L=e=>{if(!e)return`n/a`;let t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString()},R=async()=>{await M.fetchStats()};return d(async()=>{await M.fetchStats()},{intervalMs:1e4,immediate:!0}),(r,o)=>(t(),l(`div`,f,[c(`div`,p,[c(`div`,{class:`flex items-start justify-between gap-4`},[o[0]||=c(`div`,null,[c(`h1`,{class:`text-ui-title sm:text-ui-title-lg font-semibold text-content-heading`},`Sensors`),c(`p`,{class:`mt-1 text-ui-label sm:text-ui-body text-content-muted`},` Live sensor summary from the existing stats API. `)],-1),c(`button`,{class:`rounded-[10px] border border-stroke-subtle dark:border-white/opacity-light px-3 py-2 text-sm hover:bg-black/opacity-light dark:hover:bg-white/opacity-light`,onClick:R},` Refresh `)]),c(`div`,m,[(t(!0),l(s,null,e(F.value,e=>(t(),l(`div`,{key:e.label,class:`rounded-[12px] border border-stroke-subtle dark:border-white/opacity-light p-3`},[c(`p`,h,n(e.label),1),c(`p`,g,n(e.value),1)]))),128))])]),N.value?a(``,!0):(t(),l(`div`,_,` Sensor data is not available yet. Ensure the repeater has started and stats are loading. `)),(t(!0),l(s,null,e(P.value,(r,u)=>(t(),l(`div`,{key:`${r.name||`sensor`}-${u}`,class:`glass-card rounded-[15px] p-4 sm:p-5`},[c(`div`,v,[c(`div`,null,[c(`h2`,y,n(r.name||`Sensor ${u+1}`),1),c(`p`,b,`Type: `+n(r.type||`unknown`),1)]),c(`span`,{class:i([`rounded-full px-3 py-1 text-xs font-semibold`,r.ok?`bg-accent-green/opacity-light text-accent-green dark:bg-accent-green/opacity-medium dark:text-accent-green`:`bg-accent-red/opacity-light text-accent-red dark:bg-accent-red/opacity-medium dark:text-accent-red`])},n(r.ok?`OK`:`Error`),3)]),c(`div`,x,[c(`div`,S,[o[1]||=c(`span`,{class:`text-content-muted`},`Timestamp:`,-1),c(`span`,C,n(L(r.timestamp)),1)]),r.error?(t(),l(`div`,w,[o[2]||=c(`span`,{class:`text-content-muted`},`Error:`,-1),c(`span`,T,n(r.error),1)])):a(``,!0)]),c(`div`,E,[c(`table`,D,[o[4]||=c(`thead`,{class:`bg-black/opacity-light dark:bg-white/opacity-subtle`},[c(`tr`,null,[c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Field`),c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Value`)])],-1),c(`tbody`,null,[(t(!0),l(s,null,e(r.data||{},(e,r)=>(t(),l(`tr`,{key:String(r),class:`border-t border-stroke-subtle dark:border-white/opacity-light`},[c(`td`,O,n(r),1),c(`td`,k,n(I(e)),1)]))),128)),!r.data||Object.keys(r.data).length===0?(t(),l(`tr`,A,[...o[3]||=[c(`td`,{class:`px-3 py-3 text-content-muted`,colspan:`2`},`No fields in payload`,-1)]])):a(``,!0)])])])]))),128)),N.value&&P.value.length===0?(t(),l(`div`,j,` Sensors are configured but no readings are available yet. `)):a(``,!0)]))}});export{M as default};
import{D as e,T as t,_t as n,h as r,ht as i,l as a,o,r as s,s as c,u as l}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as u}from"./system-CYhvNqj-.js";import{t as d}from"./index-Cd7zc3iG.js";var f={class:`space-y-4`},p={class:`glass-card rounded-[15px] p-4 sm:p-6`},m={class:`mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4`},h={class:`text-xs uppercase tracking-wide text-content-muted`},g={class:`mt-2 text-lg font-semibold text-content-heading`},_={key:0,class:`glass-card rounded-[15px] p-5 text-content-muted`},v={class:`flex flex-wrap items-center justify-between gap-3`},y={class:`text-lg font-semibold text-content-heading`},b={class:`text-sm text-content-muted`},x={class:`mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2`},S={class:`text-sm`},C={class:`ml-2 text-content-heading`},w={key:0,class:`text-sm`},T={class:`ml-2 text-accent-red`},E={class:`mt-4 overflow-x-auto rounded-[12px] border border-stroke-subtle dark:border-white/opacity-light`},D={class:`min-w-full text-sm`},O={class:`px-3 py-2 font-medium text-content-heading`},k={class:`px-3 py-2 text-content-muted break-all`},A={key:0},j={key:1,class:`glass-card rounded-[15px] p-5 text-content-muted`},M=r({name:`SensorsView`,__name:`Sensors`,setup(r){let M=u(),N=o(()=>M.stats?.sensors??null),P=o(()=>N.value?.readings??[]),F=o(()=>{let e=N.value;return e?[{label:`Enabled`,value:e.enabled?`Yes`:`No`},{label:`Running`,value:e.running?`Yes`:`No`},{label:`Configured / Loaded`,value:`${e.configured??0} / ${e.loaded??0}`},{label:`Poll Interval`,value:typeof e.poll_interval_seconds==`number`?`${e.poll_interval_seconds.toFixed(1)}s`:`n/a`}]:[{label:`Enabled`,value:`n/a`},{label:`Running`,value:`n/a`},{label:`Configured`,value:`n/a`},{label:`Poll Interval`,value:`n/a`}]}),I=e=>{if(e==null)return`n/a`;if(typeof e==`boolean`)return e?`true`:`false`;if(typeof e==`number`)return Number.isFinite(e)?String(e):`n/a`;if(typeof e==`string`)return e;try{return JSON.stringify(e)}catch{return String(e)}},L=e=>{if(!e)return`n/a`;let t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString()},R=async()=>{await M.fetchStats()};return d(async()=>{await M.fetchStats()},{intervalMs:1e4,immediate:!0}),(r,o)=>(t(),l(`div`,f,[c(`div`,p,[c(`div`,{class:`flex items-start justify-between gap-4`},[o[0]||=c(`div`,null,[c(`h1`,{class:`text-ui-title sm:text-ui-title-lg font-semibold text-content-heading`},`Sensors`),c(`p`,{class:`mt-1 text-ui-label sm:text-ui-body text-content-muted`},` Live sensor summary from the existing stats API. `)],-1),c(`button`,{class:`rounded-[10px] border border-stroke-subtle dark:border-white/opacity-light px-3 py-2 text-sm hover:bg-black/opacity-light dark:hover:bg-white/opacity-light`,onClick:R},` Refresh `)]),c(`div`,m,[(t(!0),l(s,null,e(F.value,e=>(t(),l(`div`,{key:e.label,class:`rounded-[12px] border border-stroke-subtle dark:border-white/opacity-light p-3`},[c(`p`,h,n(e.label),1),c(`p`,g,n(e.value),1)]))),128))])]),N.value?a(``,!0):(t(),l(`div`,_,` Sensor data is not available yet. Ensure the repeater has started and stats are loading. `)),(t(!0),l(s,null,e(P.value,(r,u)=>(t(),l(`div`,{key:`${r.name||`sensor`}-${u}`,class:`glass-card rounded-[15px] p-4 sm:p-5`},[c(`div`,v,[c(`div`,null,[c(`h2`,y,n(r.name||`Sensor ${u+1}`),1),c(`p`,b,`Type: `+n(r.type||`unknown`),1)]),c(`span`,{class:i([`rounded-full px-3 py-1 text-xs font-semibold`,r.ok?`bg-accent-green/opacity-light text-accent-green dark:bg-accent-green/opacity-medium dark:text-accent-green`:`bg-accent-red/opacity-light text-accent-red dark:bg-accent-red/opacity-medium dark:text-accent-red`])},n(r.ok?`OK`:`Error`),3)]),c(`div`,x,[c(`div`,S,[o[1]||=c(`span`,{class:`text-content-muted`},`Timestamp:`,-1),c(`span`,C,n(L(r.timestamp)),1)]),r.error?(t(),l(`div`,w,[o[2]||=c(`span`,{class:`text-content-muted`},`Error:`,-1),c(`span`,T,n(r.error),1)])):a(``,!0)]),c(`div`,E,[c(`table`,D,[o[4]||=c(`thead`,{class:`bg-black/opacity-light dark:bg-white/opacity-subtle`},[c(`tr`,null,[c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Field`),c(`th`,{class:`px-3 py-2 text-left text-content-muted`},`Value`)])],-1),c(`tbody`,null,[(t(!0),l(s,null,e(r.data||{},(e,r)=>(t(),l(`tr`,{key:String(r),class:`border-t border-stroke-subtle dark:border-white/opacity-light`},[c(`td`,O,n(r),1),c(`td`,k,n(I(e)),1)]))),128)),!r.data||Object.keys(r.data).length===0?(t(),l(`tr`,A,[...o[3]||=[c(`td`,{class:`px-3 py-3 text-content-muted`,colspan:`2`},`No fields in payload`,-1)]])):a(``,!0)])])])]))),128)),N.value&&P.value.length===0?(t(),l(`div`,j,` Sensors are configured but no readings are available yet. `)):a(``,!0)]))}});export{M as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{D as e,T as t,h as n,ht as r,o as i,r as a,s as o,u as s}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as c}from"./system-AMlobbOe.js";var l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},u=-116,d=8,f=4,p=5;function m(e,t){return e-t}function h(e){return l[e]??l[d]}function g(e,t){if(e<t)return{bars:0,color:`text-accent-red`,bgColor:`bg-accent-red`,snr:e,quality:`None`};let n=Math.min(p,Math.floor((e-t)/f)+1);return{bars:n,snr:e,...{1:{color:`text-accent-red`,bgColor:`bg-accent-red`,quality:`Poor`},2:{color:`text-accent-orange`,bgColor:`bg-accent-orange`,quality:`Poor`},3:{color:`text-accent-amber`,bgColor:`bg-accent-amber`,quality:`Fair`},4:{color:`text-accent-green-light`,bgColor:`bg-accent-green-light`,quality:`Good`},5:{color:`text-accent-green`,bgColor:`bg-accent-green`,quality:`Excellent`}}[n]}}function _(){let e=c(),t=i(()=>e.noiseFloorDbm??u),n=i(()=>e.stats?.config?.radio?.spreading_factor??d),r=i(()=>h(n.value));return{getSignalQuality:e=>{if(!e||e>0||e<-120)return{bars:0,color:`text-content-muted`,bgColor:`bg-content-muted`,snr:-999,quality:`None`};let n=m(e,t.value);return g(Math.max(-30,Math.min(20,n)),r.value)},getSignalQualityFromSNR:e=>e===null||!Number.isFinite(e)?{bars:0,color:`text-content-muted`,bgColor:`bg-content-muted`,snr:-999,quality:`None`}:g(Math.max(-30,Math.min(20,e)),r.value),noiseFloor:t,spreadingFactor:n,minSNR:r}}var v={class:`flex items-end gap-0.5`},y=n({name:`SignalBars`,__name:`SignalBars`,props:{bars:{},color:{},size:{default:`sm`}},setup(n){let i=n,c={sm:[`h-1.5`,`h-2`,`h-2.5`,`h-3`,`h-3.5`],md:[`h-2`,`h-2.5`,`h-3`,`h-3.5`,`h-4`]},l={sm:`w-1`,md:`w-1.5`};return(n,u)=>(t(),s(`div`,v,[(t(),s(a,null,e(5,e=>o(`div`,{key:e,class:r([`transition-colors`,l[i.size],c[i.size][e-1],e<=i.bars?i.color:`text-content-muted`])},[...u[0]||=[o(`div`,{class:`w-full h-full bg-current rounded-sm`},null,-1)]],2)),64))]))}});export{_ as n,y as t};
import{D as e,T as t,h as n,ht as r,o as i,r as a,s as o,u as s}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as c}from"./system-CYhvNqj-.js";var l={7:-7.5,8:-10,9:-12.5,10:-15,11:-17.5,12:-20},u=-116,d=8,f=4,p=5;function m(e,t){return e-t}function h(e){return l[e]??l[d]}function g(e,t){if(e<t)return{bars:0,color:`text-accent-red`,bgColor:`bg-accent-red`,snr:e,quality:`None`};let n=Math.min(p,Math.floor((e-t)/f)+1);return{bars:n,snr:e,...{1:{color:`text-accent-red`,bgColor:`bg-accent-red`,quality:`Poor`},2:{color:`text-accent-orange`,bgColor:`bg-accent-orange`,quality:`Poor`},3:{color:`text-accent-amber`,bgColor:`bg-accent-amber`,quality:`Fair`},4:{color:`text-accent-green-light`,bgColor:`bg-accent-green-light`,quality:`Good`},5:{color:`text-accent-green`,bgColor:`bg-accent-green`,quality:`Excellent`}}[n]}}function _(){let e=c(),t=i(()=>e.noiseFloorDbm??u),n=i(()=>e.stats?.config?.radio?.spreading_factor??d),r=i(()=>h(n.value));return{getSignalQuality:e=>{if(!e||e>0||e<-120)return{bars:0,color:`text-content-muted`,bgColor:`bg-content-muted`,snr:-999,quality:`None`};let n=m(e,t.value);return g(Math.max(-30,Math.min(20,n)),r.value)},getSignalQualityFromSNR:e=>e===null||!Number.isFinite(e)?{bars:0,color:`text-content-muted`,bgColor:`bg-content-muted`,snr:-999,quality:`None`}:g(Math.max(-30,Math.min(20,e)),r.value),noiseFloor:t,spreadingFactor:n,minSNR:r}}var v={class:`flex items-end gap-0.5`},y=n({name:`SignalBars`,__name:`SignalBars`,props:{bars:{},color:{},size:{default:`sm`}},setup(n){let i=n,c={sm:[`h-1.5`,`h-2`,`h-2.5`,`h-3`,`h-3.5`],md:[`h-2`,`h-2.5`,`h-3`,`h-3.5`,`h-4`]},l={sm:`w-1`,md:`w-1.5`};return(n,u)=>(t(),s(`div`,v,[(t(),s(a,null,e(5,e=>o(`div`,{key:e,class:r([`transition-colors`,l[i.size],c[i.size][e-1],e<=i.bars?i.color:`text-content-muted`])},[...u[0]||=[o(`div`,{class:`w-full h-full bg-current rounded-sm`},null,-1)]],2)),64))]))}});export{_ as n,y as t};
@@ -1 +1 @@
import{T as e,_t as t,c as n,gt as r,h as i,l as a,m as o,o as s,p as c,r as l,s as u,u as d}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as f}from"./Spinner-CMJUE3iy.js";import{d as p}from"./index-zOQrFmWP.js";var m={class:`sparkline-card`},h={class:`card-header`},g={class:`card-title`},_={class:`card-subtitle`},v={key:0,class:`card-chart`},y={key:0,class:`chart-loader`},b={key:1,class:`chart-error`},x={key:2,class:`chart-text`},S={class:`percent-value`},C=[`id`,`viewBox`],w=[`d`,`fill`],T=[`d`,`stroke`],E=100,D=40,O=p(i({name:`SparklineChart`,__name:`Sparkline`,props:{title:{},value:{},color:{},data:{default:()=>[]},showChart:{type:Boolean,default:!0},variant:{default:`smooth`},loading:{type:Boolean,default:!1},error:{default:null},centerText:{default:``},subtitle:{default:``},minY:{default:void 0},maxY:{default:void 0}},emits:[`retry`],setup(i,{emit:p}){let O=i,k=p,A=e=>{if(e.length<3)return e;let t=Math.min(15,Math.max(3,Math.floor(e.length*.2))),n=[];for(let r=0;r<e.length;r++){let i=Math.floor(t/2),a=Math.max(0,r-i),o=Math.min(e.length,r+i+1),s=e.slice(a,o);n.push(s.reduce((e,t)=>e+t,0)/s.length)}let r=Math.min(10,n.length),i=n.length/r,a=[];for(let e=0;e<r;e++){let t=Math.floor(e*i);a.push(n[t])}return a},j=s(()=>!O.data||O.data.length===0?[]:O.variant===`smooth`?A(O.data):O.data),M=e=>{if(e.length<2)return``;let t=O.maxY??Math.max(...e),n=O.minY??Math.min(...e),r=t-n||1,i=O.variant===`classic`?4:2,a=``;return e.forEach((t,o)=>{let s=o/(e.length-1)*E,c=(t-n)/r,l=i+(D-i*2)*(1-c);if(o===0)a+=`M ${s.toFixed(2)} ${l.toFixed(2)}`;else{let t=((o-1)/(e.length-1)*E+s)/2;a+=` Q ${t.toFixed(2)} ${l.toFixed(2)} ${s.toFixed(2)} ${l.toFixed(2)}`}}),a},N=s(()=>M(j.value)),P=s(()=>N.value?`${N.value} L ${E} ${D} L 0 ${D} Z`:``),F=s(()=>`sparkline-${O.title.replace(/\s+/g,`-`).toLowerCase()}`);return(s,p)=>(e(),d(`div`,m,[u(`div`,h,[u(`div`,null,[u(`p`,g,t(i.title),1),u(`p`,_,t(i.subtitle),1)]),u(`span`,{class:`card-value`,style:r({color:i.color})},[i.loading?(e(),n(f,{key:0,size:`sm`,color:`current`})):(e(),d(l,{key:1},[c(t(typeof i.value==`number`?i.value.toLocaleString():i.value),1)],64))],4)]),i.showChart?(e(),d(`div`,v,[i.loading&&i.variant===`classic`?(e(),d(`div`,y,[o(f,{size:`sm`})])):i.error?(e(),d(`div`,b,[u(`button`,{class:`chart-retry-btn`,onClick:p[0]||=e=>k(`retry`)},`↺ Retry`)])):i.centerText?(e(),d(`div`,x,[u(`span`,S,t(i.centerText),1)])):(e(),d(`svg`,{key:3,id:F.value,class:`chart-svg`,viewBox:`0 0 ${E} ${D}`,preserveAspectRatio:`none`},[i.variant===`classic`?(e(),d(l,{key:0},[j.value.length>1?(e(),d(`path`,{key:0,d:P.value,fill:i.color,"fill-opacity":`0.8`,class:`sparkline-path`},null,8,w)):a(``,!0)],64)):(e(),d(l,{key:1},[j.value.length>1?(e(),d(`path`,{key:0,d:N.value,stroke:i.color,"stroke-width":`2.5`,"stroke-linecap":`round`,"stroke-linejoin":`round`,fill:`none`,class:`sparkline-path`},null,8,T)):a(``,!0)],64))],8,C))])):a(``,!0)]))}}),[[`__scopeId`,`data-v-eb0d809d`]]);export{O as t};
import{T as e,_t as t,c as n,gt as r,h as i,l as a,m as o,o as s,p as c,r as l,s as u,u as d}from"./runtime-core.esm-bundler-CINEgm0a.js";import{t as f}from"./Spinner-D-jFxDsV.js";import{d as p}from"./index-Cd7zc3iG.js";var m={class:`sparkline-card`},h={class:`card-header`},g={class:`card-title`},_={class:`card-subtitle`},v={key:0,class:`card-chart`},y={key:0,class:`chart-loader`},b={key:1,class:`chart-error`},x={key:2,class:`chart-text`},S={class:`percent-value`},C=[`id`,`viewBox`],w=[`d`,`fill`],T=[`d`,`stroke`],E=100,D=40,O=p(i({name:`SparklineChart`,__name:`Sparkline`,props:{title:{},value:{},color:{},data:{default:()=>[]},showChart:{type:Boolean,default:!0},variant:{default:`smooth`},loading:{type:Boolean,default:!1},error:{default:null},centerText:{default:``},subtitle:{default:``},minY:{default:void 0},maxY:{default:void 0}},emits:[`retry`],setup(i,{emit:p}){let O=i,k=p,A=e=>{if(e.length<3)return e;let t=Math.min(15,Math.max(3,Math.floor(e.length*.2))),n=[];for(let r=0;r<e.length;r++){let i=Math.floor(t/2),a=Math.max(0,r-i),o=Math.min(e.length,r+i+1),s=e.slice(a,o);n.push(s.reduce((e,t)=>e+t,0)/s.length)}let r=Math.min(10,n.length),i=n.length/r,a=[];for(let e=0;e<r;e++){let t=Math.floor(e*i);a.push(n[t])}return a},j=s(()=>!O.data||O.data.length===0?[]:O.variant===`smooth`?A(O.data):O.data),M=e=>{if(e.length<2)return``;let t=O.maxY??Math.max(...e),n=O.minY??Math.min(...e),r=t-n||1,i=O.variant===`classic`?4:2,a=``;return e.forEach((t,o)=>{let s=o/(e.length-1)*E,c=(t-n)/r,l=i+(D-i*2)*(1-c);if(o===0)a+=`M ${s.toFixed(2)} ${l.toFixed(2)}`;else{let t=((o-1)/(e.length-1)*E+s)/2;a+=` Q ${t.toFixed(2)} ${l.toFixed(2)} ${s.toFixed(2)} ${l.toFixed(2)}`}}),a},N=s(()=>M(j.value)),P=s(()=>N.value?`${N.value} L ${E} ${D} L 0 ${D} Z`:``),F=s(()=>`sparkline-${O.title.replace(/\s+/g,`-`).toLowerCase()}`);return(s,p)=>(e(),d(`div`,m,[u(`div`,h,[u(`div`,null,[u(`p`,g,t(i.title),1),u(`p`,_,t(i.subtitle),1)]),u(`span`,{class:`card-value`,style:r({color:i.color})},[i.loading?(e(),n(f,{key:0,size:`sm`,color:`current`})):(e(),d(l,{key:1},[c(t(typeof i.value==`number`?i.value.toLocaleString():i.value),1)],64))],4)]),i.showChart?(e(),d(`div`,v,[i.loading&&i.variant===`classic`?(e(),d(`div`,y,[o(f,{size:`sm`})])):i.error?(e(),d(`div`,b,[u(`button`,{class:`chart-retry-btn`,onClick:p[0]||=e=>k(`retry`)},`↺ Retry`)])):i.centerText?(e(),d(`div`,x,[u(`span`,S,t(i.centerText),1)])):(e(),d(`svg`,{key:3,id:F.value,class:`chart-svg`,viewBox:`0 0 ${E} ${D}`,preserveAspectRatio:`none`},[i.variant===`classic`?(e(),d(l,{key:0},[j.value.length>1?(e(),d(`path`,{key:0,d:P.value,fill:i.color,"fill-opacity":`0.8`,class:`sparkline-path`},null,8,w)):a(``,!0)],64)):(e(),d(l,{key:1},[j.value.length>1?(e(),d(`path`,{key:0,d:N.value,stroke:i.color,"stroke-width":`2.5`,"stroke-linecap":`round`,"stroke-linejoin":`round`,fill:`none`,class:`sparkline-path`},null,8,T)):a(``,!0)],64))],8,C))])):a(``,!0)]))}}),[[`__scopeId`,`data-v-eb0d809d`]]);export{O as t};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{t as e}from"./dataService-T3gtqUro.js";export{e as useDataService};
@@ -1 +0,0 @@
import{t as e}from"./dataService-BL9935kq.js";export{e as useDataService};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
import{t as e}from"./packets-CkIDwy78.js";export{e as usePacketStore};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{t as e}from"./packets-CHXdenlB.js";export{e as usePacketStore};
@@ -1 +1 @@
import{t as e}from"./api-CTDWH2Cm.js";async function t(t,n,r={}){let{connectTimeoutMs:i=15e3,idleTimeoutMs:a=5e3,onPhaseChange:o}=r,s=new AbortController,c=!1,l=null,u=setTimeout(()=>{c||s.abort(Error(`Connection timeout`))},i),d=()=>{l&&clearTimeout(l),l=setTimeout(()=>{s.abort(Error(`Stream stalled`))},a)};o?.(`connecting`);try{return await e.get(t,n,{signal:s.signal,timeout:0,onDownloadProgress:e=>{!c&&(e.loaded??0)>0?(c=!0,clearTimeout(u),o?.(`receiving`),d()):c&&d()}})}finally{clearTimeout(u),l&&clearTimeout(l)}}export{t};
import{t as e}from"./api-CTBjApNX.js";async function t(t,n,r={}){let{connectTimeoutMs:i=15e3,idleTimeoutMs:a=5e3,onPhaseChange:o}=r,s=new AbortController,c=!1,l=null,u=setTimeout(()=>{c||s.abort(Error(`Connection timeout`))},i),d=()=>{l&&clearTimeout(l),l=setTimeout(()=>{s.abort(Error(`Stream stalled`))},a)};o?.(`connecting`);try{return await e.get(t,n,{signal:s.signal,timeout:0,onDownloadProgress:e=>{!c&&(e.loaded??0)>0?(c=!0,clearTimeout(u),o?.(`receiving`),d()):c&&d()}})}finally{clearTimeout(u),l&&clearTimeout(l)}}export{t};
@@ -0,0 +1 @@
import{t as e}from"./system-CYhvNqj-.js";export{e as useSystemStore};
@@ -1 +1 @@
import{M as e,U as t,o as n}from"./runtime-core.esm-bundler-CINEgm0a.js";import{n as r,t as i,v as a}from"./api-CTDWH2Cm.js";import{t as o}from"./packets-CkIDwy78.js";var s=`pymc_config_cache`;function c(){try{let e=sessionStorage.getItem(s);return e?JSON.parse(e):null}catch{return null}}function l(e){if(e)try{sessionStorage.setItem(s,JSON.stringify(e))}catch{}}function u(){try{sessionStorage.removeItem(s)}catch{}}var d=a(`system`,()=>{let a=c(),s=t(a?{config:a}:null),d=t(!1),f=t(null),p=t(null),m=t(`forward`),h=t(!0),g=t(0),_=t(10),v=t(!1),y=n(()=>s.value?.config?.node_name??`Unknown`),b=n(()=>s.value?.site_name??``);e(()=>{let e=b.value;document.title=e?`${e} — Repeater`:`Repeater Dashboard`});let x=n(()=>{let e=s.value?.public_key;return!e||e===`Unknown`?`Unknown`:e.length>=16?`${e.slice(0,8)} ... ${e.slice(-8)}`:`${e}`}),S=n(()=>s.value!==null),C=n(()=>s.value?.version??`Unknown`),w=n(()=>s.value?.core_version??`Unknown`),T=n(()=>s.value?.noise_floor_dbm??null),E=n(()=>_.value>0?Math.min(g.value/_.value*100,100):0),D=n(()=>m.value===`no_tx`?{text:`No TX`,title:`No repeat, no local TX; adverts skipped`}:m.value===`monitor`?{text:`Monitor Mode`,title:`Monitoring only - not forwarding packets`}:h.value?{text:`Active`,title:`Forwarding with duty cycle enforcement`}:{text:`No Limits`,title:`Forwarding without duty cycle enforcement`}),O=n(()=>({mode:m.value})),k=n(()=>h.value?{active:!0,warning:!1}:{active:!1,warning:!0}),A=e=>{v.value=e},j=null;async function M(e){return j===null?(j=(async()=>{try{d.value=!0,f.value=null;let t=new AbortController,n=15e3,i=window.setTimeout(()=>t.abort(),n),a=!1,c=()=>{a||(a=!0,e?.onFirstByte?.()),clearTimeout(i),i=window.setTimeout(()=>t.abort(),n)},u;try{u=await r.get(`/stats`,{signal:t.signal,onDownloadProgress:c,timeout:0})}finally{clearTimeout(i)}let m=u.data,h;if(m.success&&m.data)h=m.data;else if(m&&`version`in m)h=m;else throw Error(m.error||`Failed to fetch stats`);return s.value=h,p.value=new Date,N(h),l(h.config),o().systemStats=h,h}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error fetching stats:`,e),e}finally{d.value=!1}})(),j.finally(()=>{j=null}),j):j}function N(e){if(e.config){let t=e.config.repeater?.mode;t===`forward`||t===`monitor`||t===`no_tx`?m.value=t:t!==void 0&&(m.value=`forward`);let n=e.config.duty_cycle;if(n){h.value=n.enforcement_enabled!==!1;let e=n.max_airtime_percent;typeof e==`number`?_.value=e:e&&typeof e==`object`&&`parsedValue`in e&&(_.value=e.parsedValue||10)}}let t=e.utilization_percent;typeof t==`number`?g.value=t:t&&typeof t==`object`&&`parsedValue`in t&&(g.value=t.parsedValue||0)}async function P(e){try{let t=await i.post(`/set_mode`,{mode:e});if(t.success)return m.value=e,!0;throw Error(t.error||`Failed to set mode`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting mode:`,e),e}}async function F(e){try{let t=await i.post(`/set_duty_cycle`,{enabled:e});if(t.success)return h.value=e,!0;throw Error(t.error||`Failed to set duty cycle`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting duty cycle:`,e),e}}async function I(){try{let e=await i.post(`/send_advert`,{},{timeout:1e4});if(e.success)return!0;throw Error(e.error||`Failed to send advert`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error sending advert:`,e),e}}async function L(){return await F(!h.value)}function R(e){s.value?(e.uptime_seconds!==void 0&&(s.value.uptime_seconds=e.uptime_seconds),e.noise_floor_dbm!==void 0&&(s.value.noise_floor_dbm=e.noise_floor_dbm)):s.value=e,p.value=new Date,N(e)}async function z(e=5e3,t=!1){t||await M();let n=null;return t||(n=setInterval(async()=>{try{await M()}catch(e){console.error(`Auto-refresh error:`,e)}},e)),()=>{n&&clearInterval(n)}}function B(){s.value=null,f.value=null,p.value=null,d.value=!1,m.value=`forward`,h.value=!0,g.value=0,_.value=10,u()}return{stats:s,isLoading:d,error:f,lastUpdated:p,currentMode:m,dutyCycleEnabled:h,dutyCycleUtilization:g,dutyCycleMax:_,cadCalibrationRunning:v,nodeName:y,siteName:b,pubKey:x,hasStats:S,version:C,coreVersion:w,noiseFloorDbm:T,dutyCyclePercentage:E,statusBadge:D,modeButtonState:O,dutyCycleButtonState:k,fetchStats:M,setMode:P,setDutyCycle:F,sendAdvert:I,toggleDutyCycle:L,startAutoRefresh:z,updateRealtimeStats:R,reset:B,setCadCalibrationRunning:A}});export{d as t};
import{M as e,U as t,o as n}from"./runtime-core.esm-bundler-CINEgm0a.js";import{n as r,t as i,v as a}from"./api-CTBjApNX.js";import{t as o}from"./packets-CHXdenlB.js";var s=`pymc_config_cache`;function c(){try{let e=sessionStorage.getItem(s);return e?JSON.parse(e):null}catch{return null}}function l(e){if(e)try{sessionStorage.setItem(s,JSON.stringify(e))}catch{}}function u(){try{sessionStorage.removeItem(s)}catch{}}var d=a(`system`,()=>{let a=c(),s=t(a?{config:a}:null),d=t(!1),f=t(null),p=t(null),m=t(`forward`),h=t(!0),g=t(0),_=t(10),v=t(!1),y=n(()=>s.value?.config?.node_name??`Unknown`),b=n(()=>s.value?.site_name??``);e(()=>{let e=b.value;document.title=e?`${e} — Repeater`:`Repeater Dashboard`});let x=n(()=>{let e=s.value?.public_key;return!e||e===`Unknown`?`Unknown`:e.length>=16?`${e.slice(0,8)} ... ${e.slice(-8)}`:`${e}`}),S=n(()=>s.value!==null),C=n(()=>s.value?.version??`Unknown`),w=n(()=>s.value?.core_version??`Unknown`),T=n(()=>s.value?.noise_floor_dbm??null),E=n(()=>_.value>0?Math.min(g.value/_.value*100,100):0),D=n(()=>m.value===`no_tx`?{text:`No TX`,title:`No repeat, no local TX; adverts skipped`}:m.value===`monitor`?{text:`Monitor Mode`,title:`Monitoring only - not forwarding packets`}:h.value?{text:`Active`,title:`Forwarding with duty cycle enforcement`}:{text:`No Limits`,title:`Forwarding without duty cycle enforcement`}),O=n(()=>({mode:m.value})),k=n(()=>h.value?{active:!0,warning:!1}:{active:!1,warning:!0}),A=e=>{v.value=e},j=null;async function M(e){return j===null?(j=(async()=>{try{d.value=!0,f.value=null;let t=new AbortController,n=15e3,i=window.setTimeout(()=>t.abort(),n),a=!1,c=()=>{a||(a=!0,e?.onFirstByte?.()),clearTimeout(i),i=window.setTimeout(()=>t.abort(),n)},u;try{u=await r.get(`/stats`,{signal:t.signal,onDownloadProgress:c,timeout:0})}finally{clearTimeout(i)}let m=u.data,h;if(m.success&&m.data)h=m.data;else if(m&&`version`in m)h=m;else throw Error(m.error||`Failed to fetch stats`);return s.value=h,p.value=new Date,N(h),l(h.config),o().systemStats=h,h}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error fetching stats:`,e),e}finally{d.value=!1}})(),j.finally(()=>{j=null}),j):j}function N(e){if(e.config){let t=e.config.repeater?.mode;t===`forward`||t===`monitor`||t===`no_tx`?m.value=t:t!==void 0&&(m.value=`forward`);let n=e.config.duty_cycle;if(n){h.value=n.enforcement_enabled!==!1;let e=n.max_airtime_percent;typeof e==`number`?_.value=e:e&&typeof e==`object`&&`parsedValue`in e&&(_.value=e.parsedValue||10)}}let t=e.utilization_percent;typeof t==`number`?g.value=t:t&&typeof t==`object`&&`parsedValue`in t&&(g.value=t.parsedValue||0)}async function P(e){try{let t=await i.post(`/set_mode`,{mode:e});if(t.success)return m.value=e,!0;throw Error(t.error||`Failed to set mode`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting mode:`,e),e}}async function F(e){try{let t=await i.post(`/set_duty_cycle`,{enabled:e});if(t.success)return h.value=e,!0;throw Error(t.error||`Failed to set duty cycle`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error setting duty cycle:`,e),e}}async function I(){try{let e=await i.post(`/send_advert`,{},{timeout:1e4});if(e.success)return!0;throw Error(e.error||`Failed to send advert`)}catch(e){throw f.value=e instanceof Error?e.message:`Unknown error occurred`,console.error(`Error sending advert:`,e),e}}async function L(){return await F(!h.value)}function R(e){s.value?(e.uptime_seconds!==void 0&&(s.value.uptime_seconds=e.uptime_seconds),e.noise_floor_dbm!==void 0&&(s.value.noise_floor_dbm=e.noise_floor_dbm)):s.value=e,p.value=new Date,N(e)}async function z(e=5e3,t=!1){t||await M();let n=null;return t||(n=setInterval(async()=>{try{await M()}catch(e){console.error(`Auto-refresh error:`,e)}},e)),()=>{n&&clearInterval(n)}}function B(){s.value=null,f.value=null,p.value=null,d.value=!1,m.value=`forward`,h.value=!0,g.value=0,_.value=10,u()}return{stats:s,isLoading:d,error:f,lastUpdated:p,currentMode:m,dutyCycleEnabled:h,dutyCycleUtilization:g,dutyCycleMax:_,cadCalibrationRunning:v,nodeName:y,siteName:b,pubKey:x,hasStats:S,version:C,coreVersion:w,noiseFloorDbm:T,dutyCyclePercentage:E,statusBadge:D,modeButtonState:O,dutyCycleButtonState:k,fetchStats:M,setMode:P,setDutyCycle:F,sendAdvert:I,toggleDutyCycle:L,startAutoRefresh:z,updateRealtimeStats:R,reset:B,setCadCalibrationRunning:A}});export{d as t};
@@ -1 +0,0 @@
import{t as e}from"./system-AMlobbOe.js";export{e as useSystemStore};
@@ -1 +0,0 @@
import{t as e}from"./websocket-Dq6ODStZ.js";export{e as useWebSocketStore};
@@ -1 +1 @@
import{U as e,o as t}from"./runtime-core.esm-bundler-CINEgm0a.js";import{c as n,d as r,i,l as a,v as o}from"./api-CTDWH2Cm.js";import{t as s}from"./packets-CkIDwy78.js";import{t as c}from"./system-AMlobbOe.js";import{t as l}from"./dataService-BL9935kq.js";var u=o(`websocket`,()=>{let o=e(null),u=e(`idle`),d=e(0),f=e(Date.now()),p=e(null),m=e(null),h=e(!1),g=e(!1),_=e(!1),v=e({visible:!1,message:``,variant:`info`}),y=null,b=s(),x=c(),S=i(),C=l(),w=t(()=>u.value===`open`);function T(e,t,n=0){y!==null&&(clearTimeout(y),y=null),v.value={visible:!0,message:e,variant:t},n>0&&(y=window.setTimeout(()=>{E()},n))}function E(){y!==null&&(clearTimeout(y),y=null),v.value.visible=!1}function D(){p.value!==null&&(clearTimeout(p.value),p.value=null)}function O(){m.value!==null&&(clearInterval(m.value),m.value=null)}function k(){T(`Reconnecting...`,`info`)}function A(){let e=a();return!h.value&&!g.value&&!!e&&!r()&&S.canMaintainConnections}function j(){let e,t=a(),r=n(),i=new URLSearchParams;return t&&i.set(`token`,t),r&&i.set(`client_id`,r),e=`${window.location.protocol===`https:`?`wss:`:`ws:`}//${``?.trim()?new URL(``).host:window.location.host}/ws/packets?${i.toString()}`,e}async function M(){await C.onReconnect()}function N(e=!1){O(),o.value&&e&&(o.value.onopen=null,o.value.onmessage=null,o.value.onerror=null,o.value.onclose=null)}function P(){if(D(),!A()){if(a()&&r()){S.handleAuthFailure(`expired`);return}u.value=`closed`;return}if(d.value>=6){u.value=`closed`,T(`Connection lost`,`error`,5e3);return}u.value=`reconnecting`,k();let e=Math.min(1e3*2**d.value,3e4);d.value+=1,p.value=window.setTimeout(()=>{p.value=null,F(!0)},e)}function F(e=!1){if(!A()||o.value?.readyState===WebSocket.OPEN||o.value?.readyState===WebSocket.CONNECTING)return;D(),N(!0),u.value=e||d.value>0||_.value?`reconnecting`:`connecting`,_.value&&k();let t=new WebSocket(j());o.value=t,t.onopen=()=>{u.value=`open`,f.value=Date.now();let e=d.value>0||_.value;d.value=0,_.value=!1,O(),m.value=window.setInterval(()=>{o.value?.readyState===WebSocket.OPEN&&(o.value.send(JSON.stringify({type:`ping`})),Date.now()-f.value>6e4&&(N(!0),o.value?.close()))},3e4),e?(C.onReconnect(),T(`Back online`,`success`,2500)):E()},t.onmessage=e=>{try{let t=JSON.parse(e.data);t.type===`packet`?b.addRealtimePacket(t.data):t.type===`stats`?(t.data?.packet_stats&&b.updateRealtimeStats({packet_stats:t.data.packet_stats}),t.data?.system_stats&&x.updateRealtimeStats(t.data.system_stats)):t.type===`packet_stats`?b.updateRealtimeStats(t.data):t.type===`system_stats`?x.updateRealtimeStats(t.data):(t.type===`pong`||t.type===`ping`)&&(f.value=Date.now(),t.type===`ping`&&o.value?.readyState===WebSocket.OPEN&&o.value.send(JSON.stringify({type:`pong`})))}catch(e){console.error(`[WebSocket] Parse error:`,e)}},t.onerror=()=>{u.value=d.value>0?`reconnecting`:`closed`},t.onclose=e=>{let t=o.value;if(N(),t===o.value&&(o.value=null),h.value||g.value){u.value=`closed`;return}if(e.code===1008||e.code===4001||e.code===4003){S.handleAuthFailure(`expired`);return}C.noteDisconnect(),P()}}function I(e=`lifecycle`){if(g.value=!0,D(),u.value=`closed`,e===`offline`?(_.value=!0,T(`Connection lost`,`error`,4e3)):e===`hidden`?(_.value=!0,E()):e===`logout`&&(_.value=!1,E()),o.value){let e=o.value;o.value=null,N(!0),e.close()}}function L(){h.value=!1,g.value=!1}function R(e={}){h.value=e.preventReconnect??h.value,e.silent||E(),I(e.preventReconnect?`logout`:`lifecycle`),d.value=0}return{isConnected:w,connectionState:u,reconnectAttempts:d,snackbar:v,connect:F,disconnect:R,pause:I,allowReconnect:L,hideSnackbar:E,resyncData:M}});export{u as t};
import{U as e,o as t}from"./runtime-core.esm-bundler-CINEgm0a.js";import{c as n,d as r,i,l as a,v as o}from"./api-CTBjApNX.js";import{t as s}from"./packets-CHXdenlB.js";import{t as c}from"./system-CYhvNqj-.js";import{t as l}from"./dataService-T3gtqUro.js";var u=o(`websocket`,()=>{let o=e(null),u=e(`idle`),d=e(0),f=e(Date.now()),p=e(null),m=e(null),h=e(!1),g=e(!1),_=e(!1),v=e({visible:!1,message:``,variant:`info`}),y=null,b=s(),x=c(),S=i(),C=l(),w=t(()=>u.value===`open`);function T(e,t,n=0){y!==null&&(clearTimeout(y),y=null),v.value={visible:!0,message:e,variant:t},n>0&&(y=window.setTimeout(()=>{E()},n))}function E(){y!==null&&(clearTimeout(y),y=null),v.value.visible=!1}function D(){p.value!==null&&(clearTimeout(p.value),p.value=null)}function O(){m.value!==null&&(clearInterval(m.value),m.value=null)}function k(){T(`Reconnecting...`,`info`)}function A(){let e=a();return!h.value&&!g.value&&!!e&&!r()&&S.canMaintainConnections}function j(){let e,t=a(),r=n(),i=new URLSearchParams;return t&&i.set(`token`,t),r&&i.set(`client_id`,r),e=`${window.location.protocol===`https:`?`wss:`:`ws:`}//${``?.trim()?new URL(``).host:window.location.host}/ws/packets?${i.toString()}`,e}async function M(){await C.onReconnect()}function N(e=!1){O(),o.value&&e&&(o.value.onopen=null,o.value.onmessage=null,o.value.onerror=null,o.value.onclose=null)}function P(){if(D(),!A()){if(a()&&r()){S.handleAuthFailure(`expired`);return}u.value=`closed`;return}if(d.value>=6){u.value=`closed`,T(`Connection lost`,`error`,5e3);return}u.value=`reconnecting`,k();let e=Math.min(1e3*2**d.value,3e4);d.value+=1,p.value=window.setTimeout(()=>{p.value=null,F(!0)},e)}function F(e=!1){if(!A()||o.value?.readyState===WebSocket.OPEN||o.value?.readyState===WebSocket.CONNECTING)return;D(),N(!0),u.value=e||d.value>0||_.value?`reconnecting`:`connecting`,_.value&&k();let t=new WebSocket(j());o.value=t,t.onopen=()=>{u.value=`open`,f.value=Date.now();let e=d.value>0||_.value;d.value=0,_.value=!1,O(),m.value=window.setInterval(()=>{o.value?.readyState===WebSocket.OPEN&&(o.value.send(JSON.stringify({type:`ping`})),Date.now()-f.value>6e4&&(N(!0),o.value?.close()))},3e4),e?(C.onReconnect(),T(`Back online`,`success`,2500)):E()},t.onmessage=e=>{try{let t=JSON.parse(e.data);t.type===`packet`?b.addRealtimePacket(t.data):t.type===`stats`?(t.data?.packet_stats&&b.updateRealtimeStats({packet_stats:t.data.packet_stats}),t.data?.system_stats&&x.updateRealtimeStats(t.data.system_stats)):t.type===`packet_stats`?b.updateRealtimeStats(t.data):t.type===`system_stats`?x.updateRealtimeStats(t.data):(t.type===`pong`||t.type===`ping`)&&(f.value=Date.now(),t.type===`ping`&&o.value?.readyState===WebSocket.OPEN&&o.value.send(JSON.stringify({type:`pong`})))}catch(e){console.error(`[WebSocket] Parse error:`,e)}},t.onerror=()=>{u.value=d.value>0?`reconnecting`:`closed`},t.onclose=e=>{let t=o.value;if(N(),t===o.value&&(o.value=null),h.value||g.value){u.value=`closed`;return}if(e.code===1008||e.code===4001||e.code===4003){S.handleAuthFailure(`expired`);return}C.noteDisconnect(),P()}}function I(e=`lifecycle`){if(g.value=!0,D(),u.value=`closed`,e===`offline`?(_.value=!0,T(`Connection lost`,`error`,4e3)):e===`hidden`?(_.value=!0,E()):e===`logout`&&(_.value=!1,E()),o.value){let e=o.value;o.value=null,N(!0),e.close()}}function L(){h.value=!1,g.value=!1}function R(e={}){h.value=e.preventReconnect??h.value,e.silent||E(),I(e.preventReconnect?`logout`:`lifecycle`),d.value=0}return{isConnected:w,connectionState:u,reconnectAttempts:d,snackbar:v,connect:F,disconnect:R,pause:I,allowReconnect:L,hideSnackbar:E,resyncData:M}});export{u as t};
@@ -0,0 +1 @@
import{t as e}from"./websocket-CZs2lKC6.js";export{e as useWebSocketStore};
+12 -11
View File
@@ -8,20 +8,21 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-zOQrFmWP.js"></script>
<script type="module" crossorigin src="/assets/index-Cd7zc3iG.js"></script>
<link rel="modulepreload" crossorigin href="/assets/chunk-DECur_0Z.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-core.esm-bundler-CINEgm0a.js">
<link rel="modulepreload" crossorigin href="/assets/api-CTDWH2Cm.js">
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-D-_sbJKW.js">
<link rel="modulepreload" crossorigin href="/assets/api-CTBjApNX.js">
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-C08u_PTE.js">
<link rel="modulepreload" crossorigin href="/assets/runtime-dom.esm-bundler-B3VeUO8l.js">
<link rel="modulepreload" crossorigin href="/assets/Spinner-CMJUE3iy.js">
<link rel="modulepreload" crossorigin href="/assets/useTheme-vbCn9P26.js">
<link rel="modulepreload" crossorigin href="/assets/packets-CkIDwy78.js">
<link rel="modulepreload" crossorigin href="/assets/system-AMlobbOe.js">
<link rel="modulepreload" crossorigin href="/assets/dataService-BL9935kq.js">
<link rel="modulepreload" crossorigin href="/assets/websocket-Dq6ODStZ.js">
<link rel="modulepreload" crossorigin href="/assets/constants-C3rXUIAq.js">
<link rel="stylesheet" crossorigin href="/assets/index-CB8Hp2dm.css">
<link rel="modulepreload" crossorigin href="/assets/Spinner-D-jFxDsV.js">
<link rel="modulepreload" crossorigin href="/assets/useTheme-BtGkqRpn.js">
<link rel="modulepreload" crossorigin href="/assets/packets-CHXdenlB.js">
<link rel="modulepreload" crossorigin href="/assets/system-CYhvNqj-.js">
<link rel="modulepreload" crossorigin href="/assets/formatters-vw234I-c.js">
<link rel="modulepreload" crossorigin href="/assets/dataService-T3gtqUro.js">
<link rel="modulepreload" crossorigin href="/assets/websocket-CZs2lKC6.js">
<link rel="modulepreload" crossorigin href="/assets/constants-Hl8RMxy-.js">
<link rel="stylesheet" crossorigin href="/assets/index-DNuHAVQy.css">
</head>
<body>
<div id="app"></div>
+14 -1
View File
@@ -3450,7 +3450,10 @@ paths:
answer given and an empty string is a real answer meaning the neighbour
serves unscoped traffic only. `status`/`queried_at` describe the most
recent query, which may have failed after a good answer, so `responded_at`
is what says how fresh `scopes` is.
is what says how fresh `scopes` is. `served` carries this node's own scopes
in the same comma-separated form — the very string it sends when a neighbour
asks it the same question — so a client can tell which of a neighbour's
scopes it already shares.
security:
- BearerAuth: []
- ApiKeyAuth: []
@@ -3471,6 +3474,16 @@ paths:
type: string
count:
type: integer
served:
type: object
required: [scopes]
properties:
scopes:
type: string
description: >
This node's own advertised scopes, comma-separated, `*`
first when it floods unscoped. Empty when they cannot be
read.
data:
type: object
additionalProperties:
+40
View File
@@ -2128,3 +2128,43 @@ def test_purging_the_advert_table_takes_the_scopes_with_it(tmp_path):
handler.purge_table("adverts")
assert handler.get_neighbor_scopes() == {}
def test_neighbor_scopes_endpoint_reports_our_own_served_scopes(monkeypatch):
"""The comparison a client draws must be against what we tell neighbours.
Same formatter as the anon-regions responder, so "we serve this too" cannot
drift from the answer a neighbour would get by asking us.
"""
store = _FakeScopeStore(scopes={})
api = _api_with_publisher(monkeypatch, None, method="GET", storage=store)
api.daemon_instance.login_helper = SimpleNamespace(_format_region_names=lambda: "*,DEN,BOU")
out = api.neighbor_scopes()
assert out["success"] is True
assert out["served"]["scopes"] == "*,DEN,BOU"
def test_served_scopes_degrade_to_empty_without_a_login_helper(monkeypatch):
"""An older or half-started daemon must not fail the whole request."""
api = _api_with_publisher(monkeypatch, None, method="GET", storage=_FakeScopeStore())
api.daemon_instance.login_helper = None
out = api.neighbor_scopes()
assert out["success"] is True
assert out["served"]["scopes"] == ""
def test_served_scopes_survive_a_raising_formatter(monkeypatch):
def _boom():
raise RuntimeError("transport keys unreadable")
api = _api_with_publisher(monkeypatch, None, method="GET", storage=_FakeScopeStore())
api.daemon_instance.login_helper = SimpleNamespace(_format_region_names=_boom)
out = api.neighbor_scopes()
assert out["success"] is True
assert out["served"]["scopes"] == ""