Files
l5y d2fe4f8223 web: add node opt-out marker and data retention policies (#793)
* web: add node opt-out marker and data retention policies

* web: address review comments

* web: address review comments
2026-05-20 21:02:59 +02:00

85 lines
3.2 KiB
Ruby

# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# frozen_string_literal: true
module PotatoMesh
module App
module Federation
# Count the number of nodes active since the supplied timestamp.
#
# Opted-out nodes (those whose +short_name+ or +long_name+ contains
# the {PotatoMesh::Config::NODE_OPT_OUT_MARKER}) are excluded so the
# federation node total never reflects nodes that have asked to stay
# hidden from API consumers.
#
# @param cutoff [Integer] unix timestamp in seconds.
# @param db [SQLite3::Database, nil] optional open handle to reuse.
# @return [Integer, nil] node count or nil when unavailable.
def active_node_count_since(cutoff, db: nil)
return nil unless cutoff
handle = db || open_database(readonly: true)
sql = "SELECT COUNT(*) FROM nodes WHERE last_heard >= ? AND #{opt_out_self_filter}"
count =
with_busy_retry do
handle.get_first_value(sql, [cutoff.to_i, *opt_out_marker_params])
end
Integer(count)
rescue SQLite3::Exception, ArgumentError => e
warn_log(
"Failed to count active nodes",
context: "instances.nodes_count",
error_class: e.class.name,
error_message: e.message,
)
nil
ensure
handle&.close unless db
end
# Count the number of nodes for a specific protocol active since the
# supplied timestamp. Opted-out nodes are excluded for the same
# reason as in {#active_node_count_since}.
#
# @param cutoff [Integer] unix timestamp in seconds.
# @param protocol [String] protocol name (e.g. "meshcore", "meshtastic").
# @param db [SQLite3::Database, nil] optional open handle to reuse.
# @return [Integer, nil] node count or nil when unavailable.
def active_node_count_since_for_protocol(cutoff, protocol, db: nil)
return nil unless cutoff && protocol
handle = db || open_database(readonly: true)
sql = "SELECT COUNT(*) FROM nodes WHERE last_heard >= ? AND protocol = ? AND #{opt_out_self_filter}"
count =
with_busy_retry do
handle.get_first_value(sql, [cutoff.to_i, protocol, *opt_out_marker_params])
end
Integer(count)
rescue SQLite3::Exception, ArgumentError => e
warn_log(
"Failed to count active nodes for protocol",
context: "instances.protocol_nodes_count",
protocol: protocol,
error_class: e.class.name,
error_message: e.message,
)
nil
ensure
handle&.close unless db
end
end
end
end