mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
* nodes: add charts to detail pages * nodes: improve charts on detail pages * fix ignored packet debug loggin * run rufo * address review comments
396 lines
14 KiB
Ruby
396 lines
14 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
|
|
# Shared view and controller helper methods. Each helper is documented with
|
|
# its intended consumers to ensure consistent behaviour across the Sinatra
|
|
# application.
|
|
module Helpers
|
|
# Fetch an application level constant exposed by {PotatoMesh::Application}.
|
|
#
|
|
# @param name [Symbol] constant identifier to retrieve.
|
|
# @return [Object] constant value stored on the application class.
|
|
def app_constant(name)
|
|
PotatoMesh::Application.const_get(name)
|
|
end
|
|
|
|
# Retrieve the configured Prometheus report identifiers as an array.
|
|
#
|
|
# @return [Array<String>] list of report IDs used on the metrics page.
|
|
def prom_report_ids
|
|
PotatoMesh::Config.prom_report_id_list
|
|
end
|
|
|
|
# Read a text configuration value with a fallback.
|
|
#
|
|
# @param key [String] environment variable key.
|
|
# @param default [String] fallback value when unset.
|
|
# @return [String] sanitised configuration string.
|
|
def fetch_config_string(key, default)
|
|
PotatoMesh::Config.fetch_string(key, default)
|
|
end
|
|
|
|
# Proxy for {PotatoMesh::Sanitizer.string_or_nil}.
|
|
#
|
|
# @param value [Object] value to sanitise.
|
|
# @return [String, nil] cleaned string or nil.
|
|
def string_or_nil(value)
|
|
PotatoMesh::Sanitizer.string_or_nil(value)
|
|
end
|
|
|
|
# Proxy for {PotatoMesh::Sanitizer.sanitize_instance_domain}.
|
|
#
|
|
# @param value [Object] candidate domain string.
|
|
# @param downcase [Boolean] whether to force lowercase normalisation.
|
|
# @return [String, nil] canonical domain or nil.
|
|
def sanitize_instance_domain(value, downcase: true)
|
|
PotatoMesh::Sanitizer.sanitize_instance_domain(value, downcase: downcase)
|
|
end
|
|
|
|
# Proxy for {PotatoMesh::Sanitizer.instance_domain_host}.
|
|
#
|
|
# @param domain [String] domain literal.
|
|
# @return [String, nil] host portion of the domain.
|
|
def instance_domain_host(domain)
|
|
PotatoMesh::Sanitizer.instance_domain_host(domain)
|
|
end
|
|
|
|
# Proxy for {PotatoMesh::Sanitizer.ip_from_domain}.
|
|
#
|
|
# @param domain [String] domain literal.
|
|
# @return [IPAddr, nil] parsed address object.
|
|
def ip_from_domain(domain)
|
|
PotatoMesh::Sanitizer.ip_from_domain(domain)
|
|
end
|
|
|
|
# Proxy for {PotatoMesh::Sanitizer.sanitized_string}.
|
|
#
|
|
# @param value [Object] arbitrary input.
|
|
# @return [String] trimmed string representation.
|
|
def sanitized_string(value)
|
|
PotatoMesh::Sanitizer.sanitized_string(value)
|
|
end
|
|
|
|
# Retrieve the site name presented to users.
|
|
#
|
|
# @return [String] sanitised site label.
|
|
def sanitized_site_name
|
|
PotatoMesh::Sanitizer.sanitized_site_name
|
|
end
|
|
|
|
# Retrieve the configured channel.
|
|
#
|
|
# @return [String] sanitised channel identifier.
|
|
def sanitized_channel
|
|
PotatoMesh::Sanitizer.sanitized_channel
|
|
end
|
|
|
|
# Retrieve the configured frequency descriptor.
|
|
#
|
|
# @return [String] sanitised frequency text.
|
|
def sanitized_frequency
|
|
PotatoMesh::Sanitizer.sanitized_frequency
|
|
end
|
|
|
|
# Build the configuration hash exposed to the frontend application.
|
|
#
|
|
# @return [Hash] JSON serialisable configuration payload.
|
|
def frontend_app_config
|
|
{
|
|
refreshIntervalSeconds: PotatoMesh::Config.refresh_interval_seconds,
|
|
refreshMs: PotatoMesh::Config.refresh_interval_seconds * 1000,
|
|
chatEnabled: !private_mode?,
|
|
channel: sanitized_channel,
|
|
frequency: sanitized_frequency,
|
|
contactLink: sanitized_contact_link,
|
|
contactLinkUrl: sanitized_contact_link_url,
|
|
mapCenter: {
|
|
lat: PotatoMesh::Config.map_center_lat,
|
|
lon: PotatoMesh::Config.map_center_lon,
|
|
},
|
|
maxDistanceKm: PotatoMesh::Config.max_distance_km,
|
|
tileFilters: PotatoMesh::Config.tile_filters,
|
|
instanceDomain: app_constant(:INSTANCE_DOMAIN),
|
|
instancesFeatureEnabled: federation_enabled? && !private_mode?,
|
|
}
|
|
end
|
|
|
|
# Retrieve the configured contact link or nil when unset.
|
|
#
|
|
# @return [String, nil] contact link identifier.
|
|
def sanitized_contact_link
|
|
PotatoMesh::Sanitizer.sanitized_contact_link
|
|
end
|
|
|
|
# Retrieve the hyperlink derived from the configured contact link.
|
|
#
|
|
# @return [String, nil] hyperlink pointing to the community chat.
|
|
def sanitized_contact_link_url
|
|
PotatoMesh::Sanitizer.sanitized_contact_link_url
|
|
end
|
|
|
|
# Retrieve the configured maximum node distance in kilometres.
|
|
#
|
|
# @return [Numeric, nil] maximum distance or nil if disabled.
|
|
def sanitized_max_distance_km
|
|
PotatoMesh::Sanitizer.sanitized_max_distance_km
|
|
end
|
|
|
|
# Format a kilometre value for human readable output.
|
|
#
|
|
# @param distance [Numeric] distance in kilometres.
|
|
# @return [String] formatted distance value.
|
|
def formatted_distance_km(distance)
|
|
PotatoMesh::Meta.formatted_distance_km(distance)
|
|
end
|
|
|
|
# Build the canonical node detail path for the supplied identifier.
|
|
#
|
|
# @param identifier [String, nil] node identifier in ``!xxxx`` notation.
|
|
# @return [String, nil] detail path including the canonical ``!`` prefix.
|
|
def node_detail_path(identifier)
|
|
ident = string_or_nil(identifier)
|
|
return nil unless ident && !ident.empty?
|
|
trimmed = ident.strip
|
|
return nil if trimmed.empty?
|
|
body = trimmed.start_with?("!") ? trimmed[1..-1] : trimmed
|
|
return nil unless body && !body.empty?
|
|
escaped = Rack::Utils.escape_path(body)
|
|
"/nodes/!#{escaped}"
|
|
end
|
|
|
|
# Render a linked long name pointing to the node detail page.
|
|
#
|
|
# @param long_name [String] display name for the node.
|
|
# @param identifier [String, nil] canonical node identifier.
|
|
# @param css_class [String, nil] optional CSS class applied to the anchor.
|
|
# @return [String] escaped HTML snippet.
|
|
def node_long_name_link(long_name, identifier, css_class: "node-long-link")
|
|
text = string_or_nil(long_name)
|
|
return "" unless text
|
|
href = node_detail_path(identifier)
|
|
escaped_text = Rack::Utils.escape_html(text)
|
|
return escaped_text unless href
|
|
class_attr = css_class ? %( class="#{css_class}") : ""
|
|
%(<a#{class_attr} href="#{href}" target="_blank" rel="noopener noreferrer">#{escaped_text}</a>)
|
|
end
|
|
|
|
# Generate the meta description used in SEO tags.
|
|
#
|
|
# @return [String] combined descriptive sentence.
|
|
def meta_description
|
|
PotatoMesh::Meta.description(private_mode: private_mode?)
|
|
end
|
|
|
|
# Generate the structured meta configuration for the UI.
|
|
#
|
|
# @return [Hash] frozen configuration metadata.
|
|
def meta_configuration
|
|
PotatoMesh::Meta.configuration(private_mode: private_mode?)
|
|
end
|
|
|
|
# Coerce an arbitrary value into an integer when possible.
|
|
#
|
|
# @param value [Object] user supplied value.
|
|
# @return [Integer, nil] parsed integer or nil when invalid.
|
|
def coerce_integer(value)
|
|
case value
|
|
when Integer
|
|
value
|
|
when Float
|
|
value.finite? ? value.to_i : nil
|
|
when Numeric
|
|
value.to_i
|
|
when String
|
|
trimmed = value.strip
|
|
return nil if trimmed.empty?
|
|
return trimmed.to_i(16) if trimmed.match?(/\A0[xX][0-9A-Fa-f]+\z/)
|
|
return trimmed.to_i(10) if trimmed.match?(/\A-?\d+\z/)
|
|
begin
|
|
float_val = Float(trimmed)
|
|
float_val.finite? ? float_val.to_i : nil
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Coerce an arbitrary value into a floating point number when possible.
|
|
#
|
|
# @param value [Object] user supplied value.
|
|
# @return [Float, nil] parsed float or nil when invalid.
|
|
def coerce_float(value)
|
|
case value
|
|
when Float
|
|
value.finite? ? value : nil
|
|
when Integer
|
|
value.to_f
|
|
when Numeric
|
|
value.to_f
|
|
when String
|
|
trimmed = value.strip
|
|
return nil if trimmed.empty?
|
|
begin
|
|
float_val = Float(trimmed)
|
|
float_val.finite? ? float_val : nil
|
|
rescue ArgumentError
|
|
nil
|
|
end
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Coerce an arbitrary value into a boolean according to common truthy
|
|
# conventions.
|
|
#
|
|
# @param value [Object] user supplied value.
|
|
# @return [Boolean, nil] boolean interpretation or nil when unknown.
|
|
def coerce_boolean(value)
|
|
case value
|
|
when true, false
|
|
value
|
|
when String
|
|
trimmed = value.strip.downcase
|
|
return true if %w[true 1 yes y].include?(trimmed)
|
|
return false if %w[false 0 no n].include?(trimmed)
|
|
nil
|
|
when Numeric
|
|
!value.to_i.zero?
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Normalise PEM encoded public key content into LF line endings.
|
|
#
|
|
# @param value [String, #to_s, nil] raw PEM content.
|
|
# @return [String, nil] cleaned PEM string or nil when blank.
|
|
def sanitize_public_key_pem(value)
|
|
return nil if value.nil?
|
|
|
|
pem = value.is_a?(String) ? value : value.to_s
|
|
pem = pem.gsub(/\r\n?/, "\n")
|
|
return nil if pem.strip.empty?
|
|
|
|
pem
|
|
end
|
|
|
|
# Recursively coerce hash keys to strings and normalise nested arrays.
|
|
#
|
|
# @param value [Object] JSON compatible value.
|
|
# @return [Object] structure with canonical string keys.
|
|
def normalize_json_value(value)
|
|
case value
|
|
when Hash
|
|
value.each_with_object({}) do |(key, val), memo|
|
|
memo[key.to_s] = normalize_json_value(val)
|
|
end
|
|
when Array
|
|
value.map { |element| normalize_json_value(element) }
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
# Parse JSON payloads or hashes into normalised hashes with string keys.
|
|
#
|
|
# @param value [Hash, String, nil] raw JSON object or string representation.
|
|
# @return [Hash, nil] canonicalised hash or nil when parsing fails.
|
|
def normalize_json_object(value)
|
|
case value
|
|
when Hash
|
|
normalize_json_value(value)
|
|
when String
|
|
trimmed = value.strip
|
|
return nil if trimmed.empty?
|
|
begin
|
|
parsed = JSON.parse(trimmed)
|
|
rescue JSON::ParserError
|
|
return nil
|
|
end
|
|
parsed.is_a?(Hash) ? normalize_json_value(parsed) : nil
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Emit a structured debug log entry tagged with the calling context.
|
|
#
|
|
# @param message [String] text to emit.
|
|
# @param context [String] logical source of the message.
|
|
# @param metadata [Hash] additional structured key/value data.
|
|
# @return [void]
|
|
def debug_log(message, context: "app", **metadata)
|
|
logger = PotatoMesh::Logging.logger_for(self)
|
|
PotatoMesh::Logging.log(logger, :debug, message, context: context, **metadata)
|
|
end
|
|
|
|
# Emit a structured warning log entry tagged with the calling context.
|
|
#
|
|
# @param message [String] text to emit.
|
|
# @param context [String] logical source of the message.
|
|
# @param metadata [Hash] additional structured key/value data.
|
|
# @return [void]
|
|
def warn_log(message, context: "app", **metadata)
|
|
logger = PotatoMesh::Logging.logger_for(self)
|
|
PotatoMesh::Logging.log(logger, :warn, message, context: context, **metadata)
|
|
end
|
|
|
|
# Indicate whether private mode has been requested.
|
|
#
|
|
# @return [Boolean] true when PRIVATE=1.
|
|
def private_mode?
|
|
PotatoMesh::Config.private_mode_enabled?
|
|
end
|
|
|
|
# Identify whether the Rack environment corresponds to the test suite.
|
|
#
|
|
# @return [Boolean] true when RACK_ENV is "test".
|
|
def test_environment?
|
|
ENV["RACK_ENV"] == "test"
|
|
end
|
|
|
|
# Determine whether the application is running in a production environment.
|
|
#
|
|
# @return [Boolean] true when APP_ENV or RACK_ENV resolves to "production".
|
|
def production_environment?
|
|
app_env = string_or_nil(ENV["APP_ENV"])&.downcase
|
|
rack_env = string_or_nil(ENV["RACK_ENV"])&.downcase
|
|
|
|
app_env == "production" || rack_env == "production"
|
|
end
|
|
|
|
# Determine whether federation features should be active.
|
|
#
|
|
# @return [Boolean] true when federation configuration allows it.
|
|
def federation_enabled?
|
|
PotatoMesh::Config.federation_enabled?
|
|
end
|
|
|
|
# Determine whether federation announcements should run asynchronously.
|
|
#
|
|
# @return [Boolean] true when announcements are enabled.
|
|
def federation_announcements_active?
|
|
federation_enabled? && !test_environment?
|
|
end
|
|
end
|
|
end
|
|
end
|