diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html index 2c3a4a1..3df51a9 100644 --- a/meshview/templates/chat.html +++ b/meshview/templates/chat.html @@ -4,175 +4,150 @@ .timestamp { min-width: 10em; } -.chat-packet:nth-of-type(odd) { - background-color: #3a3a3a; -} -.chat-packet { - border-bottom: 1px solid #555; - padding: 8px; - border-radius: 8px; -} -.chat-packet:nth-of-type(even) { - background-color: #333333; -} +.chat-packet:nth-of-type(odd) { background-color: #3a3a3a; } +.chat-packet { border-bottom: 1px solid #555; padding: 8px; border-radius: 8px; } +.chat-packet:nth-of-type(even) { background-color: #333333; } + @keyframes flash { 0% { background-color: #ffe066; } 100% { background-color: inherit; } } -.chat-packet.flash { - animation: flash 3.5s ease-out; -} +.chat-packet.flash { animation: flash 3.5s ease-out; } -/* Nested reply style below the message */ -.replying-to { - font-size: 0.85em; - color: #aaa; /* gray text */ - margin-top: 4px; - padding-left: 20px; /* increased indentation */ - -.replying-to .reply-preview { - color: #aaa; -} - - -} +/* Nested reply style */ +.replying-to { font-size: 0.85em; color: #aaa; margin-top: 4px; padding-left: 20px; } +.replying-to .reply-preview { color: #aaa; } {% endblock %} {% block body %}
-
-
+
- - - - {% endblock %} diff --git a/meshview/templates/map.html b/meshview/templates/map.html index 8797df1..e093963 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -72,10 +72,10 @@ {% block body %}
- Show Routers Only + Show Routers Only
- +
@@ -87,6 +87,21 @@ crossorigin> - {% endblock %} diff --git a/meshview/templates/net.html b/meshview/templates/net.html index 0ce4917..af9efe6 100644 --- a/meshview/templates/net.html +++ b/meshview/templates/net.html @@ -19,9 +19,11 @@ {% block body %}
- {{ site_config["site"]["weekly_net_message"] }}

+ {{ site_config["site"]["weekly_net_message"] }}

-
Number of Check-ins: {{ packets|length }}
+
+ Number of Check-ins: {{ packets|length }} +
@@ -48,7 +50,26 @@
{% else %} - No packets found. + No packets found. {% endfor %} + + {% endblock %} diff --git a/meshview/templates/nodegraph.html b/meshview/templates/nodegraph.html index be350d3..3d25625 100644 --- a/meshview/templates/nodegraph.html +++ b/meshview/templates/nodegraph.html @@ -17,7 +17,7 @@ position: absolute; bottom: 100px; left: 10px; - z-index: 10; + z-index: 10;1 display: flex; flex-direction: column; gap: 5px; diff --git a/meshview/templates/stats.html b/meshview/templates/stats.html index fee8a2e..e045371 100644 --- a/meshview/templates/stats.html +++ b/meshview/templates/stats.html @@ -93,88 +93,87 @@ {% block body %}
-

Mesh Statistics - Summary (all available in Database)

+

Mesh Statistics - Summary (all available in Database)

-

Total Nodes

+

Total Nodes

{{ "{:,}".format(total_nodes) }}
-

Total Packets

+

Total Packets

{{ "{:,}".format(total_packets) }}
-

Total Packets Seen

+

Total Packets Seen

{{ "{:,}".format(total_packets_seen) }}
- -
-

Packets per Day - All Ports (Last 14 Days)

-
Total: 0
- - -
-
+ +
+

Packets per Day - All Ports (Last 14 Days)

+
Total: 0
+ + +
+
- -
-

Packet Types - Last 24 Hours

- - - -
-
- -
-

Packets per Day - Text Messages (Port 1, Last 14 Days)

-
Total: 0
- - -
-
+ +
+

Packet Types - Last 24 Hours

+ + + +
+
+
+

Packets per Day - Text Messages (Port 1, Last 14 Days)

+
Total: 0
+ + +
+
-

Packets per Hour - All Ports

+

Packets per Hour - All Ports

Total: 0
- - + +
-

Packets per Hour - Text Messages (Port 1)

+

Packets per Hour - Text Messages (Port 1)

Total: 0
- - + +
-

Hardware Breakdown

- - +

Hardware Breakdown

+ +
-

Role Breakdown

- - +

Role Breakdown

+ +
-

Channel Breakdown

- - +

Channel Breakdown

+ +
@@ -215,87 +214,17 @@ async function fetchStats(period_type,length,portnum=null,channel=null){ }catch{return [];} } -async function fetchNodes(){ - try{ - const res=await fetch("/api/nodes"); - const json=await res.json(); - return json.nodes||[]; - }catch{return [];} -} +async function fetchNodes(){ try{ const res=await fetch("/api/nodes"); const json=await res.json(); return json.nodes||[];}catch{return [];} } +async function fetchChannels(){ try{ const res = await fetch("/api/channels"); const json = await res.json(); return json.channels || [];}catch{return [];} } -async function fetchChannels(){ - try{ - const res = await fetch("/api/channels"); - const json = await res.json(); - return json.channels || []; - }catch{return [];} -} - -function processCountField(nodes,field){ - const counts={}; - nodes.forEach(n=>{ - const key=n[field]||"Unknown"; - counts[key]=(counts[key]||0)+1; - }); - return Object.entries(counts).map(([name,value])=>({name,value})); -} - -function updateTotalCount(domId,data){ - const el=document.getElementById(domId); - if(!el||!data.length) return; - const total=data.reduce((acc,d)=>acc+(d.count??d.packet_count??0),0); - el.textContent=`Total: ${total.toLocaleString()}`; -} - -function prepareTopN(data,n=20){ - data.sort((a,b)=>b.value-a.value); - let top=data.slice(0,n); - if(data.length>n){ - const otherValue=data.slice(n).reduce((sum,item)=>sum+item.value,0); - top.push({name:"Other", value:otherValue}); - } - return top; -} +function processCountField(nodes,field){ const counts={}; nodes.forEach(n=>{ const key=n[field]||"Unknown"; counts[key]=(counts[key]||0)+1; }); return Object.entries(counts).map(([name,value])=>({name,value})); } +function updateTotalCount(domId,data){ const el=document.getElementById(domId); if(!el||!data.length) return; const total=data.reduce((acc,d)=>acc+(d.count??d.packet_count??0),0); el.textContent=`Total: ${total.toLocaleString()}`; } +function prepareTopN(data,n=20){ data.sort((a,b)=>b.value-a.value); let top=data.slice(0,n); if(data.length>n){ const otherValue=data.slice(n).reduce((sum,item)=>sum+item.value,0); top.push({name:"Other", value:otherValue}); } return top; } // --- Chart Rendering --- -function renderChart(domId,data,type,color,isHourly){ - const el=document.getElementById(domId); - if(!el) return; - const chart=echarts.init(el); - const periods=data.map(d=>(d.period??d.period===0)?d.period.toString():''); - const counts=data.map(d=>d.count??d.packet_count??0); - const option={ - backgroundColor:'#272b2f', - tooltip:{trigger:'axis'}, - grid:{left:'6%', right:'6%', bottom:'18%'}, - xAxis:{type:'category', data:periods, axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{rotate:45,color:'#ccc'}}, - yAxis:{type:'value', axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{color:'#ccc'}}, - series:[{data:counts,type:type,smooth:type==='line',itemStyle:{color:color}, areaStyle:type==='line'?{}:undefined}] - }; - chart.setOption(option); - return chart; -} +function renderChart(domId,data,type,color){ const el=document.getElementById(domId); if(!el) return; const chart=echarts.init(el); const periods=data.map(d=>(d.period??d.period===0)?d.period.toString():''); const counts=data.map(d=>d.count??d.packet_count??0); chart.setOption({backgroundColor:'#272b2f', tooltip:{trigger:'axis'}, grid:{left:'6%', right:'6%', bottom:'18%'}, xAxis:{type:'category', data:periods, axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{rotate:45,color:'#ccc'}}, yAxis:{type:'value', axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{color:'#ccc'}}, series:[{data:counts,type:type,smooth:type==='line',itemStyle:{color:color}, areaStyle:type==='line'?{}:undefined}]}); return chart; } -function renderPieChart(elId,data,name){ - const el=document.getElementById(elId); - if(!el) return; - const chart=echarts.init(el); - const top20=prepareTopN(data,20); - const option={ - backgroundColor:"#272b2f", - tooltip:{trigger:"item", formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`}, - series:[{ - name:name, type:"pie", radius:["30%","70%"], center:["50%","50%"], - avoidLabelOverlap:true, - itemStyle:{borderRadius:6,borderColor:"#272b2f",borderWidth:2}, - label:{show:true,formatter:"{b}\n{d}%", color:"#ccc", fontSize:10}, - labelLine:{show:true,length:10,length2:6}, - data:top20 - }] - }; - chart.setOption(option); - return chart; -} +function renderPieChart(elId,data,name){ const el=document.getElementById(elId); if(!el) return; const chart=echarts.init(el); const top20=prepareTopN(data,20); chart.setOption({backgroundColor:"#272b2f", tooltip:{trigger:"item", formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`}, series:[{name:name, type:"pie", radius:["30%","70%"], center:["50%","50%"], avoidLabelOverlap:true, itemStyle:{borderRadius:6,borderColor:"#272b2f",borderWidth:2}, label:{show:true,formatter:"{b}\n{d}%", color:"#ccc", fontSize:10}, labelLine:{show:true,length:10,length2:6}, data:top20}]}); return chart; } // --- Packet Type Pie Chart --- async function fetchPacketTypeBreakdown(channel=null) { @@ -305,10 +234,8 @@ async function fetchPacketTypeBreakdown(channel=null) { const total = (data || []).reduce((sum,d)=>sum+(d.count??d.packet_count??0),0); return {portnum: pn, count: total}; }); - const allData = await fetchStats('hour',24,null,channel); const totalAll = allData.reduce((sum,d)=>sum+(d.count??d.packet_count??0),0); - const results = await Promise.all(requests); const trackedTotal = results.reduce((sum,d)=>sum+d.count,0); const other = Math.max(totalAll - trackedTotal,0); @@ -323,62 +250,41 @@ let chartHwModel, chartRole, chartChannel; let chartPacketTypes; async function init(){ - // Populate channels const channels = await fetchChannels(); const select = document.getElementById("channelSelect"); - channels.forEach(ch=>{ - const opt = document.createElement("option"); - opt.value = ch; - opt.textContent = ch; - select.appendChild(opt); - }); + channels.forEach(ch=>{ const opt = document.createElement("option"); opt.value = ch; opt.textContent = ch; select.appendChild(opt); }); - // Daily const dailyAllData=await fetchStats('day',14); updateTotalCount('total_daily_all',dailyAllData); - chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a',false); + chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a'); const dailyPort1Data=await fetchStats('day',14,1); updateTotalCount('total_daily_portnum_1',dailyPort1Data); - chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722',false); + chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722'); - // Hourly const hourlyAllData=await fetchStats('hour',24); updateTotalCount('total_hourly_all',hourlyAllData); - chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6',true); + chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6'); const portnums=[1,3,4,67,70,71]; const colors=['#ff5722','#2196f3','#9c27b0','#ffeb3b','#795548','#4caf50']; const domIds=['chart_portnum_1','chart_portnum_3','chart_portnum_4','chart_portnum_67','chart_portnum_70','chart_portnum_71']; const totalIds=['total_portnum_1','total_portnum_3','total_portnum_4','total_portnum_67','total_portnum_70','total_portnum_71']; const allData=await Promise.all(portnums.map(pn=>fetchStats('hour',24,pn))); - for(let i=0;id.count>0).map(d=>({ - name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), - value: d.count - })); + const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count })); chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)"); } -// --- Resize --- -window.addEventListener('resize',()=>{ - [chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71, - chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize()); -}); +window.addEventListener('resize',()=>{ [chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71, chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize()); }); -// --- Modal --- const modal=document.getElementById("chartModal"); const modalChartEl=document.getElementById("modalChart"); let modalChart=null; @@ -400,7 +306,6 @@ document.getElementById("closeModal").addEventListener("click",()=>{ modalChart=null; }); -// --- CSV Export --- function downloadCSV(filename,rows){ const csvContent=rows.map(r=>r.map(v=>`"${v}"`).join(",")).join("\n"); const blob=new Blob([csvContent],{type:"text/csv;charset=utf-8;"}); @@ -437,18 +342,34 @@ document.querySelectorAll(".export-btn").forEach(btn=>{ }); }); -// --- Channel filter for Packet Types --- document.getElementById("channelSelect").addEventListener("change", async (e)=>{ const channel = e.target.value; const packetTypesData = await fetchPacketTypeBreakdown(channel); - const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ - name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), - value: d.count - })); + const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count })); chartPacketTypes?.dispose(); chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)"); }); init(); + +// --- Translation Loader --- +async function loadTranslations() { + const langCode = "{{ site_config.get('site', {}).get('language','en') }}"; + try { + const res = await fetch(`/api/lang?lang=${langCode}§ion=stats`); + window.statsTranslations = await res.json(); + } catch(err){ + console.error("Stats translation load failed:", err); + window.statsTranslations = {}; + } +} +function applyTranslations() { + const t = window.statsTranslations || {}; + document.querySelectorAll("[data-translate-lang]").forEach(el=>{ + const key = el.getAttribute("data-translate-lang"); + if(t[key]) el.textContent = t[key]; + }); +} +loadTranslations().then(applyTranslations); {% endblock %} diff --git a/meshview/templates/top.html b/meshview/templates/top.html index 11bac09..cb72ea1 100644 --- a/meshview/templates/top.html +++ b/meshview/templates/top.html @@ -81,15 +81,22 @@ select { {% endblock %} {% block body %} -

Top Traffic Nodes (last 24 hours)

+

Top Traffic Nodes (last 24 hours)

-

This chart shows a bell curve (normal distribution) based on the total "Times Seen" values for all nodes. It helps visualize how frequently nodes are heard, relative to the average.

-

This "Times Seen" value is the closest that we can get to Mesh utilization by node.

-

Mean: - Standard Deviation:

+

+ This chart shows a bell curve (normal distribution) based on the total "Times Seen" values for all nodes. It helps visualize how frequently nodes are heard, relative to the average. +

+

+ This "Times Seen" value is the closest that we can get to Mesh utilization by node. +

+

+ Mean: - + Standard Deviation: +

@@ -97,23 +104,23 @@ select { {% if nodes %} -
+
- - - - - - - - - - - + + + + + + + + + + +
Long NameShort NameChannelPackets SentTimes SeenSeen % of Mean
Long NameShort NameChannelPackets SentTimes SeenSeen % of Mean
-
+
{% else %} -

No top traffic nodes available.

+

No top traffic nodes available.

{% endif %} @@ -121,31 +128,45 @@ select { const nodes = {{ nodes | tojson }}; let filteredNodes = []; -// Chart & Stats +// --- Language support --- +async function loadTopTranslations() { + const langCode = "{{ site_config.get('site', {}).get('language','en') }}"; + try { + const res = await fetch(`/api/lang?lang=${langCode}§ion=top`); + window.topTranslations = await res.json(); + } catch(err) { + console.error("Top page translation load failed:", err); + window.topTranslations = {}; + } +} + +function applyTopTranslations() { + const t = window.topTranslations || {}; + document.querySelectorAll("[data-translate-lang]").forEach(el=>{ + const key = el.getAttribute("data-translate-lang"); + if(t[key]) el.textContent = t[key]; + }); +} + +// --- Chart & Table code --- const chart = echarts.init(document.getElementById('bellCurveChart')); const meanEl = document.getElementById('mean'); const stdEl = document.getElementById('stdDev'); -// Populate Channel Dropdown (without "All"), default to "LongFast" +// Populate channel dropdown const channelSet = new Set(); nodes.forEach(n => channelSet.add(n.channel)); const dropdown = document.getElementById('channelFilter'); -const sortedChannels = [...channelSet].sort(); - -sortedChannels.forEach(channel => { +[...channelSet].sort().forEach(channel => { const option = document.createElement('option'); option.value = channel; option.textContent = channel; - if (channel === "LongFast") { - option.selected = true; - } + if (channel === "LongFast") option.selected = true; dropdown.appendChild(option); }); -// Default to LongFast filter on load +// Filter default filteredNodes = nodes.filter(n => n.channel === "LongFast"); - -// Filter change handler dropdown.addEventListener('change', () => { const val = dropdown.value; filteredNodes = nodes.filter(n => n.channel === val); @@ -153,18 +174,16 @@ dropdown.addEventListener('change', () => { updateStatsAndChart(); }); -// Normal distribution function +// Normal distribution function normalDistribution(x, mean, stdDev) { - return (1 / (stdDev * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * Math.pow((x - mean) / stdDev, 2)); + return (1 / (stdDev * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * Math.pow((x - mean) / stdDev, 2)); } -// Update table based on filteredNodes +// Update table function updateTable() { const tbody = document.querySelector('#trafficTable tbody'); tbody.innerHTML = ""; - const mean = filteredNodes.reduce((sum, n) => sum + n.total_times_seen, 0) / (filteredNodes.length || 1); - for (const node of filteredNodes) { const percent = mean > 0 ? ((node.total_times_seen / mean) * 100).toFixed(1) + "%" : "0%"; const row = ` @@ -182,73 +201,52 @@ function updateTable() { // Update chart & stats function updateStatsAndChart() { const timesSeen = filteredNodes.map(n => n.total_times_seen); - const mean = timesSeen.reduce((sum, v) => sum + v, 0) / (timesSeen.length || 1); - const stdDev = Math.sqrt(timesSeen.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (timesSeen.length || 1)); - + const mean = timesSeen.reduce((sum,v)=>sum+v,0)/(timesSeen.length||1); + const stdDev = Math.sqrt(timesSeen.reduce((sum,v)=>sum+Math.pow(v-mean,2),0)/(timesSeen.length||1)); meanEl.textContent = mean.toFixed(2); stdEl.textContent = stdDev.toFixed(2); const min = Math.min(...timesSeen); const max = Math.max(...timesSeen); const step = (max - min) / 100; - const xData = [], yData = []; + const xData=[], yData=[]; + for(let x=min;x<=max;x+=step){ xData.push(x); yData.push(normalDistribution(x,mean,stdDev)); } - for (let x = min; x <= max; x += step) { - xData.push(x); - yData.push(normalDistribution(x, mean, stdDev)); - } - - const option = { - animation: false, - tooltip: { trigger: 'axis' }, - xAxis: { - name: 'Total Times Seen', - type: 'value', - min, max - }, - yAxis: { - name: 'Probability Density', - type: 'value', - }, - series: [{ - data: xData.map((x, i) => [x, yData[i]]), - type: 'line', - smooth: true, - color: 'blue', - lineStyle: { width: 3 } - }] - }; - chart.setOption(option); + chart.setOption({ + animation:false, + tooltip:{ trigger:'axis' }, + xAxis:{ name:'Total Times Seen', type:'value', min, max }, + yAxis:{ name:'Probability Density', type:'value' }, + series:[{ data:xData.map((x,i)=>[x,yData[i]]), type:'line', smooth:true, color:'blue', lineStyle:{ width:3 }}] + }); chart.resize(); } -// Sorting +// Sort table function sortTable(n) { const table = document.getElementById("trafficTable"); const rows = Array.from(table.rows).slice(1); const header = table.rows[0].cells[n]; - const isNumeric = !isNaN(rows[0].cells[n].innerText.replace('%', '')); - let sortedRows = rows.sort((a, b) => { - const valA = isNumeric ? parseFloat(a.cells[n].innerText.replace('%', '')) : a.cells[n].innerText.toLowerCase(); - const valB = isNumeric ? parseFloat(b.cells[n].innerText.replace('%', '')) : b.cells[n].innerText.toLowerCase(); + const isNumeric = !isNaN(rows[0].cells[n].innerText.replace('%','')); + let sortedRows = rows.sort((a,b)=>{ + const valA = isNumeric ? parseFloat(a.cells[n].innerText.replace('%','')) : a.cells[n].innerText.toLowerCase(); + const valB = isNumeric ? parseFloat(b.cells[n].cells[n].innerText.replace('%','')) : b.cells[n].innerText.toLowerCase(); return valA > valB ? 1 : -1; }); - - if (header.getAttribute('data-sort-direction') === 'asc') { - sortedRows.reverse(); - header.setAttribute('data-sort-direction', 'desc'); - } else { - header.setAttribute('data-sort-direction', 'asc'); - } - + if(header.getAttribute('data-sort-direction')==='asc'){ sortedRows.reverse(); header.setAttribute('data-sort-direction','desc'); } + else header.setAttribute('data-sort-direction','asc'); const tbody = table.tBodies[0]; - sortedRows.forEach(row => tbody.appendChild(row)); + sortedRows.forEach(row=>tbody.appendChild(row)); } // Initialize -updateTable(); -updateStatsAndChart(); -window.addEventListener('resize', () => chart.resize()); +(async ()=>{ + await loadTopTranslations(); + applyTopTranslations(); + updateTable(); + updateStatsAndChart(); + window.addEventListener('resize',()=>chart.resize()); +})(); {% if timing_data %}