mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-08-06 08:53:20 +02:00
Map: activity time filters
This commit is contained in:
+28
-3
@@ -263,7 +263,13 @@ async def get_node_traffic(node_id: int):
|
||||
return []
|
||||
|
||||
|
||||
async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
||||
async def get_nodes(
|
||||
role=None,
|
||||
channel=None,
|
||||
hw_model=None,
|
||||
days_active=None,
|
||||
active_within: timedelta | None = None,
|
||||
):
|
||||
"""
|
||||
Fetches nodes from the database based on optional filtering criteria.
|
||||
|
||||
@@ -271,6 +277,8 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
||||
role (str, optional): The role of the node (converted to uppercase for consistency).
|
||||
channel (str, optional): The communication channel associated with the node.
|
||||
hw_model (str, optional): The hardware model of the node.
|
||||
days_active (int, optional): Legacy support for filtering by a number of days.
|
||||
active_within (timedelta, optional): Filter nodes seen within the provided window.
|
||||
|
||||
Returns:
|
||||
list: A list of Node objects that match the given criteria.
|
||||
@@ -290,8 +298,12 @@ async def get_nodes(role=None, channel=None, hw_model=None, days_active=None):
|
||||
if hw_model is not None:
|
||||
query = query.where(Node.hw_model == hw_model)
|
||||
|
||||
if days_active is not None:
|
||||
query = query.where(Node.last_update > datetime.now() - timedelta(days_active))
|
||||
window = active_within
|
||||
if window is None and days_active is not None:
|
||||
window = timedelta(days=days_active)
|
||||
|
||||
if window is not None:
|
||||
query = query.where(Node.last_update > datetime.now() - window)
|
||||
|
||||
# Exclude nodes where last_update is an empty string
|
||||
query = query.where(Node.last_update != "")
|
||||
@@ -360,6 +372,19 @@ async def get_packet_stats(
|
||||
}
|
||||
|
||||
|
||||
async def get_all_channels():
|
||||
async with database.async_session() as session:
|
||||
stmt = (
|
||||
select(Node.channel)
|
||||
.where(Node.channel.is_not(None))
|
||||
.where(Node.channel != "")
|
||||
.distinct()
|
||||
.order_by(Node.channel.asc())
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
return [row[0] for row in result]
|
||||
|
||||
|
||||
async def get_channels_in_period(period_type: str = "hour", length: int = 24):
|
||||
"""
|
||||
Returns a list of distinct channels used in packets over a given period.
|
||||
|
||||
+74
-11
@@ -23,10 +23,20 @@
|
||||
#filter-container {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.filter-checkbox {
|
||||
margin: 0 10px;
|
||||
}
|
||||
#activity-range {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#share-button {
|
||||
margin-left: 20px;
|
||||
padding: 5px 15px;
|
||||
@@ -72,6 +82,12 @@
|
||||
{% block body %}
|
||||
<div id="map" style="width: 100%; height: calc(100vh - 270px)"></div>
|
||||
<div id="filter-container">
|
||||
<label for="activity-range" id="activity-range-label">Active in:</label>
|
||||
<select id="activity-range">
|
||||
{% for value, label, _window in activity_filters %}
|
||||
<option value="{{ value }}" {% if value == selected_activity %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="checkbox" class="filter-checkbox" id="filter-routers-only"> <span id="filter-routers-label">Show Routers Only</span>
|
||||
</div>
|
||||
<div style="text-align: center; margin-top: 5px;">
|
||||
@@ -101,6 +117,18 @@ async function loadTranslations() {
|
||||
// Initialize map AFTER translations are loaded
|
||||
loadTranslations().then(() => {
|
||||
const t = window.mapTranslations || {};
|
||||
const activitySelect = document.getElementById("activity-range");
|
||||
const activityLabel = document.getElementById("activity-range-label");
|
||||
if (activityLabel) {
|
||||
activityLabel.textContent = t.active_within || "Active in:";
|
||||
}
|
||||
if (activitySelect) {
|
||||
activitySelect.addEventListener("change", () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("active", activitySelect.value);
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Map Setup ----
|
||||
var map = L.map('map');
|
||||
@@ -135,6 +163,8 @@ loadTranslations().then(() => {
|
||||
}{{ "," if not loop.last else "" }}
|
||||
{% endfor %}
|
||||
];
|
||||
const providedChannels = {{ all_channels | default([], true) | tojson }};
|
||||
const channelSet = new Set(providedChannels.filter(ch => ch));
|
||||
|
||||
const portMap = {1: "Text", 67: "Telemetry", 3: "Position", 70: "Traceroute", 4: "Node Info", 71: "Neighbour Info", 73: "Map Report"};
|
||||
|
||||
@@ -162,18 +192,24 @@ loadTranslations().then(() => {
|
||||
return color;
|
||||
}
|
||||
|
||||
function channelKey(channel) {
|
||||
if (typeof channel === 'string' && channel.trim().length > 0) {
|
||||
return channel;
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach(n => nodeMap.set(n.id, n));
|
||||
function isInvalidCoord(node) { return !node || !node.lat || !node.long || node.lat===0 || node.long===0 || Number.isNaN(node.lat) || Number.isNaN(node.long); }
|
||||
|
||||
// ---- Marker Plotting ----
|
||||
var bounds = L.latLngBounds();
|
||||
var channels = new Set();
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (!isInvalidCoord(node)) {
|
||||
let category = node.channel;
|
||||
channels.add(category);
|
||||
let category = channelKey(node.channel);
|
||||
channelSet.add(category);
|
||||
let color = hashToColor(category);
|
||||
|
||||
let popupContent = `<b><a href="/packet_list/${node.id}">${node.long_name}</a> (${node.short_name})</b><br>
|
||||
@@ -209,6 +245,8 @@ loadTranslations().then(() => {
|
||||
if (customView) map.setView([customView.lat,customView.lng],customView.zoom);
|
||||
else map.fitBounds(areaBounds);
|
||||
|
||||
const channelList = Array.from(channelSet).sort();
|
||||
|
||||
// ---- LocalStorage for Filter Preferences ----
|
||||
const FILTER_STORAGE_KEY = 'meshview_map_filters';
|
||||
|
||||
@@ -225,7 +263,7 @@ loadTranslations().then(() => {
|
||||
channels: {}
|
||||
};
|
||||
|
||||
channels.forEach(channel => {
|
||||
channelList.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
||||
let checkbox = document.getElementById(filterId);
|
||||
if (checkbox) {
|
||||
@@ -259,7 +297,7 @@ loadTranslations().then(() => {
|
||||
document.getElementById("filter-routers-only").checked = false;
|
||||
|
||||
// Reset all channel filters to checked (default)
|
||||
channels.forEach(channel => {
|
||||
channelList.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
||||
let checkbox = document.getElementById(filterId);
|
||||
if (checkbox) {
|
||||
@@ -286,7 +324,7 @@ loadTranslations().then(() => {
|
||||
filterLabel.textContent = t.show_routers_only || "Show Routers Only";
|
||||
|
||||
let filterContainer = document.getElementById("filter-container");
|
||||
channels.forEach(channel => {
|
||||
channelList.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g,'-').toLowerCase()}`;
|
||||
let color = hashToColor(channel);
|
||||
let label = document.createElement('label');
|
||||
@@ -302,7 +340,7 @@ loadTranslations().then(() => {
|
||||
document.getElementById("filter-routers-only").checked = savedFilters.routersOnly || false;
|
||||
|
||||
// Apply channel filters
|
||||
channels.forEach(channel => {
|
||||
channelList.forEach(channel => {
|
||||
let filterId = `filter-${channel.replace(/\s+/g, '-').toLowerCase()}`;
|
||||
let checkbox = document.getElementById(filterId);
|
||||
if (checkbox && savedFilters.channels.hasOwnProperty(channel)) {
|
||||
@@ -314,15 +352,19 @@ loadTranslations().then(() => {
|
||||
function updateMarkers() {
|
||||
let showRoutersOnly = document.getElementById("filter-routers-only").checked;
|
||||
nodes.forEach(node => {
|
||||
let category=node.channel;
|
||||
let category = channelKey(node.channel);
|
||||
let checkbox=document.getElementById(`filter-${category.replace(/\s+/g,'-').toLowerCase()}`);
|
||||
let shouldShow=checkbox.checked && (!showRoutersOnly || node.isRouter);
|
||||
let shouldShow=(!checkbox || checkbox.checked) && (!showRoutersOnly || node.isRouter);
|
||||
let marker=markerById[node.id];
|
||||
if(marker) marker.setStyle({fillOpacity:shouldShow?1:0});
|
||||
});
|
||||
|
||||
// Save filters to localStorage whenever they change
|
||||
saveFiltersToLocalStorage();
|
||||
|
||||
if (!document.hidden) {
|
||||
restartPacketFetcher();
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".filter-checkbox").forEach(input=>input.addEventListener("change",updateMarkers));
|
||||
@@ -338,7 +380,11 @@ loadTranslations().then(() => {
|
||||
const zoom = map.getZoom();
|
||||
const lat = center.lat.toFixed(6);
|
||||
const lng = center.lng.toFixed(6);
|
||||
const shareUrl = `${window.location.origin}/map?lat=${lat}&lng=${lng}&zoom=${zoom}`;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('lat', lat);
|
||||
url.searchParams.set('lng', lng);
|
||||
url.searchParams.set('zoom', zoom);
|
||||
const shareUrl = url.toString();
|
||||
navigator.clipboard.writeText(shareUrl).then(()=>{
|
||||
const orig = shareBtn.textContent;
|
||||
shareBtn.textContent = '✓ Link Copied!';
|
||||
@@ -474,8 +520,25 @@ loadTranslations().then(() => {
|
||||
}).catch(err=>console.error(err));
|
||||
}
|
||||
let packetInterval=null;
|
||||
function startPacketFetcher(){ if(mapInterval<=0) return; if(!packetInterval){ fetchLatestPacket(); packetInterval=setInterval(fetchNewPackets,mapInterval*1000); } }
|
||||
function startPacketFetcher(resetImportTime=true){
|
||||
if(mapInterval<=0) return;
|
||||
if(!packetInterval){
|
||||
if(resetImportTime || !lastImportTime){
|
||||
fetchLatestPacket();
|
||||
}
|
||||
packetInterval=setInterval(fetchNewPackets,mapInterval*1000);
|
||||
if(!resetImportTime && lastImportTime){
|
||||
fetchNewPackets();
|
||||
}
|
||||
}
|
||||
}
|
||||
function stopPacketFetcher(){ if(packetInterval){ clearInterval(packetInterval); packetInterval=null; } }
|
||||
function restartPacketFetcher(){
|
||||
if(mapInterval<=0) return;
|
||||
stopPacketFetcher();
|
||||
if(document.hidden) return;
|
||||
startPacketFetcher(false);
|
||||
}
|
||||
document.addEventListener("visibilitychange",function(){ if(document.hidden) stopPacketFetcher(); else startPacketFetcher(); });
|
||||
if(mapInterval>0) startPacketFetcher();
|
||||
});
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
<div id="mynetwork"></div>
|
||||
|
||||
<div class="search-container">
|
||||
<label for="activity-range" style="color:#333;">Active in:</label>
|
||||
<select id="activity-range">
|
||||
{% for value, label, _window in activity_filters %}
|
||||
<option value="{{ value }}" {% if value == selected_activity %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="channel-select" style="color:#333;">Channel:</label>
|
||||
<select id="channel-select" onchange="filterByChannel()"></select>
|
||||
<input type="text" id="node-search" placeholder="Search node...">
|
||||
@@ -123,6 +129,25 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const activityRangeSelect = document.getElementById('activity-range');
|
||||
const availableChannelsFromServer = {{ all_channels | default([], true) | tojson }};
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const channelParam = urlParams.get('channel');
|
||||
let selectedChannel = {{ selected_channel | default('', true) | tojson }};
|
||||
const hasChannelSelection = value => value !== null && value !== undefined && value !== '';
|
||||
if (activityRangeSelect) {
|
||||
activityRangeSelect.addEventListener('change', () => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('active', activityRangeSelect.value);
|
||||
if (hasChannelSelection(selectedChannel)) {
|
||||
url.searchParams.set('channel', selectedChannel);
|
||||
} else {
|
||||
url.searchParams.delete('channel');
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
}
|
||||
|
||||
const chart = echarts.init(document.getElementById('mynetwork'));
|
||||
|
||||
const colors = {
|
||||
@@ -204,28 +229,62 @@ const edges = [
|
||||
|
||||
let filteredNodes = [];
|
||||
let filteredEdges = [];
|
||||
let selectedChannel = 'LongFast';
|
||||
let lastSelectedNode = null;
|
||||
const nodeChannelSet = [...new Set(nodes.map(n => n.channel).filter(Boolean))];
|
||||
const channelOptions = Array.from(new Set([
|
||||
...availableChannelsFromServer.filter(Boolean),
|
||||
...nodeChannelSet,
|
||||
])).sort();
|
||||
|
||||
if (!selectedChannel || !channelOptions.includes(selectedChannel)) {
|
||||
if (channelParam && channelOptions.includes(channelParam)) {
|
||||
selectedChannel = channelParam;
|
||||
} else if (nodeChannelSet.length) {
|
||||
selectedChannel = nodeChannelSet[0];
|
||||
} else if (channelOptions.length) {
|
||||
selectedChannel = channelOptions[0];
|
||||
} else {
|
||||
selectedChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
function populateChannelDropdown() {
|
||||
const sel = document.getElementById('channel-select');
|
||||
const unique = [...new Set(nodes.map(n=>n.channel).filter(Boolean))].sort();
|
||||
unique.forEach(ch=>{
|
||||
channelOptions.forEach(ch=>{
|
||||
const opt = document.createElement('option');
|
||||
opt.value=ch; opt.text=ch;
|
||||
if(ch==='LongFast') opt.selected=true;
|
||||
if(selectedChannel && ch === selectedChannel) opt.selected=true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
selectedChannel = sel.value;
|
||||
filterByChannel();
|
||||
if (!selectedChannel && channelOptions.length) {
|
||||
selectedChannel = channelOptions[0];
|
||||
sel.value = selectedChannel;
|
||||
}
|
||||
filterByChannel(true);
|
||||
}
|
||||
|
||||
function filterByChannel() {
|
||||
selectedChannel = document.getElementById('channel-select').value;
|
||||
filteredNodes = nodes.filter(n=>n.channel===selectedChannel);
|
||||
function filterByChannel(isInitial=false) {
|
||||
const sel = document.getElementById('channel-select');
|
||||
if (sel) {
|
||||
selectedChannel = sel.value || selectedChannel;
|
||||
}
|
||||
if (hasChannelSelection(selectedChannel)) {
|
||||
filteredNodes = nodes.filter(n=>n.channel===selectedChannel);
|
||||
} else {
|
||||
filteredNodes = [...nodes];
|
||||
}
|
||||
const nodeSet = new Set(filteredNodes.map(n=>n.name));
|
||||
filteredEdges = edges.filter(e=>nodeSet.has(e.source) && nodeSet.has(e.target));
|
||||
lastSelectedNode=null;
|
||||
if (!isInitial) {
|
||||
const url = new URL(window.location.href);
|
||||
if (hasChannelSelection(selectedChannel)) {
|
||||
url.searchParams.set('channel', selectedChannel);
|
||||
} else {
|
||||
url.searchParams.delete('channel');
|
||||
}
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
updateChart();
|
||||
}
|
||||
|
||||
|
||||
+91
-24
@@ -33,6 +33,17 @@ SEQ_REGEX = re.compile(r"seq \d+")
|
||||
SOFTWARE_RELEASE = "2.0.7 ~ 09-17-25"
|
||||
CONFIG = config.CONFIG
|
||||
|
||||
ACTIVITY_FILTERS = [
|
||||
("1h", "Last 1 hour", timedelta(hours=1)),
|
||||
("8h", "Last 8 hours", timedelta(hours=8)),
|
||||
("1d", "Last 1 day", timedelta(days=1)),
|
||||
("3d", "Last 3 days", timedelta(days=3)),
|
||||
("7d", "Last 7 days", timedelta(days=7)),
|
||||
("total", "All time", None),
|
||||
]
|
||||
ACTIVITY_OPTIONS = {value: window for value, _label, window in ACTIVITY_FILTERS}
|
||||
DEFAULT_ACTIVITY_OPTION = "1d"
|
||||
|
||||
env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape())
|
||||
|
||||
# Start Database
|
||||
@@ -186,6 +197,17 @@ def format_timestamp(timestamp):
|
||||
env.filters["node_id_to_hex"] = node_id_to_hex
|
||||
env.filters["format_timestamp"] = format_timestamp
|
||||
|
||||
|
||||
def resolve_activity_window(
|
||||
raw_value: str | None, default_key: str = DEFAULT_ACTIVITY_OPTION
|
||||
):
|
||||
default_key = default_key if default_key in ACTIVITY_OPTIONS else DEFAULT_ACTIVITY_OPTION
|
||||
if raw_value:
|
||||
normalized = raw_value.strip().lower()
|
||||
if normalized in ACTIVITY_OPTIONS:
|
||||
return normalized, ACTIVITY_OPTIONS[normalized]
|
||||
return default_key, ACTIVITY_OPTIONS[default_key]
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
|
||||
|
||||
@@ -1181,7 +1203,16 @@ async def net(request):
|
||||
@routes.get("/map")
|
||||
async def map(request):
|
||||
try:
|
||||
nodes = await store.get_nodes(days_active=3)
|
||||
activity_param = request.query.get("active")
|
||||
if not activity_param:
|
||||
legacy_days = request.query.get("days_active")
|
||||
if legacy_days and legacy_days.isdigit():
|
||||
activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
|
||||
|
||||
selected_activity, activity_window = resolve_activity_window(activity_param)
|
||||
|
||||
nodes = await store.get_nodes(active_within=activity_window)
|
||||
all_channels = await store.get_all_channels()
|
||||
|
||||
# Filter out nodes with no latitude
|
||||
nodes = [node for node in nodes if node.last_lat is not None]
|
||||
@@ -1214,6 +1245,10 @@ async def map(request):
|
||||
text=template.render(
|
||||
nodes=nodes,
|
||||
custom_view=custom_view,
|
||||
activity_filters=ACTIVITY_FILTERS,
|
||||
selected_activity=selected_activity,
|
||||
default_activity=DEFAULT_ACTIVITY_OPTION,
|
||||
all_channels=all_channels,
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
),
|
||||
@@ -1352,22 +1387,40 @@ async def chat(request):
|
||||
# Assuming the route URL structure is /nodegraph
|
||||
@routes.get("/nodegraph")
|
||||
async def nodegraph(request):
|
||||
nodes = await store.get_nodes(days_active=3) # Fetch nodes for the given channel
|
||||
node_ids = set()
|
||||
activity_param = request.query.get("active")
|
||||
if not activity_param:
|
||||
legacy_days = request.query.get("days_active")
|
||||
if legacy_days and legacy_days.isdigit():
|
||||
activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
|
||||
|
||||
selected_activity, activity_window = resolve_activity_window(activity_param)
|
||||
|
||||
nodes = await store.get_nodes(active_within=activity_window)
|
||||
all_channels = await store.get_all_channels()
|
||||
channel_param = request.query.get("channel")
|
||||
node_channel_candidates = sorted({node.channel for node in nodes if node.channel})
|
||||
|
||||
if channel_param and channel_param in node_channel_candidates:
|
||||
selected_channel = channel_param
|
||||
elif channel_param and channel_param in all_channels:
|
||||
selected_channel = channel_param
|
||||
elif node_channel_candidates:
|
||||
selected_channel = node_channel_candidates[0]
|
||||
elif all_channels:
|
||||
selected_channel = all_channels[0]
|
||||
else:
|
||||
selected_channel = None
|
||||
|
||||
active_node_ids = {node.node_id for node in nodes}
|
||||
edges_map = defaultdict(
|
||||
lambda: {"weight": 0, "type": None}
|
||||
) # weight is based on the number of traceroutes and neighbor info packets
|
||||
used_nodes = set() # This will track nodes involved in edges (including traceroutes)
|
||||
since = datetime.timedelta(hours=48)
|
||||
traceroutes = []
|
||||
|
||||
# Fetch traceroutes
|
||||
async for tr in store.get_traceroutes(since):
|
||||
node_ids.add(tr.gateway_node_id)
|
||||
node_ids.add(tr.packet.from_node_id)
|
||||
node_ids.add(tr.packet.to_node_id)
|
||||
route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route)
|
||||
node_ids.update(route.route)
|
||||
|
||||
path = [tr.packet.from_node_id]
|
||||
path.extend(route.route)
|
||||
@@ -1383,18 +1436,12 @@ async def nodegraph(request):
|
||||
edge_pair = (path[i], path[i + 1])
|
||||
edges_map[edge_pair]["weight"] += 1
|
||||
edges_map[edge_pair]["type"] = "traceroute"
|
||||
used_nodes.add(path[i]) # Add all nodes in the traceroute path
|
||||
used_nodes.add(path[i + 1]) # Add all nodes in the traceroute path
|
||||
|
||||
# Fetch NeighborInfo packets
|
||||
for packet in await store.get_packets(portnum=PortNum.NEIGHBORINFO_APP, after=since):
|
||||
try:
|
||||
_, neighbor_info = decode_payload.decode(packet)
|
||||
node_ids.add(packet.from_node_id)
|
||||
used_nodes.add(packet.from_node_id)
|
||||
for node in neighbor_info.neighbors:
|
||||
node_ids.add(node.node_id)
|
||||
used_nodes.add(node.node_id)
|
||||
|
||||
edge_pair = (node.node_id, packet.from_node_id)
|
||||
edges_map[edge_pair]["weight"] += 1
|
||||
@@ -1403,7 +1450,16 @@ async def nodegraph(request):
|
||||
logger.error(f"Error decoding NeighborInfo packet: {e}")
|
||||
|
||||
# Convert edges_map to a list of dicts with colors
|
||||
max_weight = max(i['weight'] for i in edges_map.values()) if edges_map else 1
|
||||
filtered_edge_items = [
|
||||
((frm, to), info)
|
||||
for (frm, to), info in edges_map.items()
|
||||
if frm in active_node_ids and to in active_node_ids
|
||||
]
|
||||
max_weight = (
|
||||
max(info["weight"] for _, info in filtered_edge_items)
|
||||
if filtered_edge_items
|
||||
else 1
|
||||
)
|
||||
edges = [
|
||||
{
|
||||
"from": frm,
|
||||
@@ -1411,17 +1467,25 @@ async def nodegraph(request):
|
||||
"type": info["type"],
|
||||
"weight": max([info['weight'] / float(max_weight) * 10, 1]),
|
||||
}
|
||||
for (frm, to), info in edges_map.items()
|
||||
for (frm, to), info in filtered_edge_items
|
||||
]
|
||||
|
||||
# Filter nodes to only include those involved in edges (including traceroutes)
|
||||
nodes_with_edges = [node for node in nodes if node.node_id in used_nodes]
|
||||
connected_node_ids = {
|
||||
node_id for edge in edges for node_id in (edge["from"], edge["to"])
|
||||
}
|
||||
nodes_with_edges = [node for node in nodes if node.node_id in connected_node_ids]
|
||||
|
||||
template = env.get_template("nodegraph.html")
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
nodes=nodes_with_edges,
|
||||
edges=edges, # Pass edges with color info
|
||||
activity_filters=ACTIVITY_FILTERS,
|
||||
selected_activity=selected_activity,
|
||||
default_activity=DEFAULT_ACTIVITY_OPTION,
|
||||
all_channels=all_channels,
|
||||
selected_channel=selected_channel,
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
),
|
||||
@@ -1572,17 +1636,20 @@ async def api_nodes(request):
|
||||
role = request.query.get("role")
|
||||
channel = request.query.get("channel")
|
||||
hw_model = request.query.get("hw_model")
|
||||
days_active = request.query.get("days_active")
|
||||
activity_param = request.query.get("active")
|
||||
if not activity_param:
|
||||
legacy_days = request.query.get("days_active")
|
||||
if legacy_days and legacy_days.isdigit():
|
||||
activity_param = "total" if legacy_days == "0" else f"{legacy_days}d"
|
||||
|
||||
if days_active:
|
||||
try:
|
||||
days_active = int(days_active)
|
||||
except ValueError:
|
||||
days_active = None
|
||||
_, activity_window = resolve_activity_window(activity_param, default_key="total")
|
||||
|
||||
# Fetch nodes from database using your get_nodes function
|
||||
nodes = await store.get_nodes(
|
||||
role=role, channel=channel, hw_model=hw_model, days_active=days_active
|
||||
role=role,
|
||||
channel=channel,
|
||||
hw_model=hw_model,
|
||||
active_within=activity_window,
|
||||
)
|
||||
|
||||
# Prepare the JSON response
|
||||
|
||||
Reference in New Issue
Block a user