fix: resolve credo warnings — refactor decoder_macros complexity and remove unused assign indirection
Some checks failed
Build base image / Build and push base image (push) Failing after 2s
Production Deployment / Run ExUnit Tests (push) Failing after 58s
Production Deployment / Build and Push Docker Image (push) Has been skipped

- Refactored build_field_handler_ast to use pattern-matching function heads
  instead of a large cond block, reducing cyclomatic complexity from 30 to ≤9
- Extracted classify_type_spec and per-type AST builder helpers into separate
  private functions for maintainability
- Removed :previous_interface_stats initialization from show.ex (only
  polling.ex reads it); prev_stats now passed explicitly as a parameter
  to polling functions instead of reaching through socket.assigns
- Updated Polling.poll_sensors, poll_traffic, and poll_traffic_with_state
  to accept prev_stats as an explicit argument
This commit is contained in:
Graham McIntire 2026-07-25 09:44:56 -05:00
parent 012a8ece95
commit b5512f2579
19 changed files with 229 additions and 153 deletions

View file

@ -293,8 +293,8 @@ Towerops uses Forgejo CI/CD with two trigger paths (see `.forgejo/workflows/`):
- Push to `main` triggers `production.yaml`
- **Test gate:** ExUnit suite must pass before any image is built
- After tests pass:
- Builds Docker image from `k8s/Dockerfile``FROM codeberg.org/gmcintire/towerops-base:latest`
- Pushes to `codeberg.org/gmcintire/towerops` (tagged `main-<ts>-<sha>` and `production`)
- Builds Docker image from `k8s/Dockerfile``FROM git.mcintire.me/gmcintire/towerops-base:latest`
- Pushes to `git.mcintire.me/gmcintire/towerops` (tagged `main-<ts>-<sha>` and `production`)
- `argocd-image-updater` picks up the new tag (~2 min), writes it to the
ArgoCD Application's `spec.source.kustomize.images`, and ArgoCD rolls the
Deployment. No commit to `k8s/deployment.yaml`.
@ -306,7 +306,7 @@ ship to prod once tests pass.
The slow runtime apt installs (`gdal-bin`, `snmp`, `libsnmp40`, `locales`,
BEAM runtime libs) live in a prebuilt base image —
`codeberg.org/gmcintire/towerops-base:latest` — so day-to-day app builds
`git.mcintire.me/gmcintire/towerops-base:latest` — so day-to-day app builds
skip ~500 MB of GIS dep installation.
- **Source**: `k8s/Dockerfile.base`

View file

@ -136,7 +136,7 @@ config :towerops, :coverage_storage_dir, {:towerops, "priv/static/coverage"}
# Agent Docker Image
# Override this in runtime.exs or environment-specific config
config :towerops,
agent_docker_image: "codeberg.org/towerops-agent/towerops-agent:latest"
agent_docker_image: "git.mcintire.me/towerops-agent/towerops-agent:latest"
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.

View file

@ -7,7 +7,7 @@ use Bearer-token auth scoped to a single organization.
> Looking for an Ansible-native interface? The official Towerops Ansible
> collection wraps these endpoints with idempotent modules:
> <https://codeberg.org/towerops/ansible>.
> <https://git.mcintire.me/towerops/ansible>.
## Authentication

View file

@ -7,7 +7,7 @@
# This file is based on these images:
#
# - https://hub.docker.com/r/hexpm/elixir/tags - for the build image
# - codeberg.org/gmcintire/towerops-base - prebuilt runtime base (see Dockerfile.base)
# - git.mcintire.me/gmcintire/towerops-base - prebuilt runtime base (see Dockerfile.base)
# - Ex: docker.io/hexpm/elixir:1.20.2-erlang-29.0.3-debian-trixie-20260713-slim
#
ARG ELIXIR_VERSION=1.20.2
@ -20,7 +20,7 @@ ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION
# Built by .forgejo/workflows/build-base.yaml only when Dockerfile.base
# changes, so this Dockerfile pulls a ready-made layer instead of
# reinstalling gdal (~500 MB) on every push.
ARG BASE_IMAGE="codeberg.org/gmcintire/towerops-base:latest"
ARG BASE_IMAGE="git.mcintire.me/gmcintire/towerops-base:latest"
FROM ${BUILDER_IMAGE} AS builder
ENV MIX_OS_DEPS_COMPILE_PARTITION_COUNT=6

View file

@ -8,7 +8,7 @@
#
# Built by `.forgejo/workflows/build-base.yaml` only when this file or
# the workflow changes. The app `Dockerfile` does
# `FROM codeberg.org/gmcintire/towerops-base:latest` so everyday code
# `FROM git.mcintire.me/gmcintire/towerops-base:latest` so everyday code
# pushes skip the gdal + snmp apt install entirely.
#
# Bumping the Debian release: change the ARG below and push — the base

View file

@ -77,7 +77,7 @@ defmodule Towerops.Agents.ReleaseChecker do
end
defp fetch_and_cache do
url = "https://codeberg.org/api/v1/repos/#{@codeberg_repo}/releases/latest"
url = "https://git.mcintire.me/api/v1/repos/#{@codeberg_repo}/releases/latest"
case req_get(url, headers: [{"accept", "application/json"}], retry: false) do
{:ok, %Req.Response{status: 200, body: body}} ->

View file

@ -218,154 +218,227 @@ defmodule Towerops.Proto.DecoderMacros do
end
end
# Generate a field handler clause for a single field
defp build_field_handler_ast(fun, field_num, field_name, type_spec) do
# Generate a field handler clause for a single field (dispatch by type_spec shape)
defp build_field_handler_ast(fun, field_num, field_name, :string) do
fields_fn = :"#{fun}_fields"
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :string)
end
cond do
type_spec == :string ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
defp build_field_handler_ast(fun, field_num, field_name, :uint) do
fields_fn = :"#{fun}_fields"
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :uint)
end
defp build_field_handler_ast(fun, field_num, field_name, :int64) do
fields_fn = :"#{fun}_fields"
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :int64)
end
defp build_field_handler_ast(fun, field_num, field_name, :double) do
fields_fn = :"#{fun}_fields"
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :double)
end
defp build_field_handler_ast(fun, field_num, field_name, :bool) do
fields_fn = :"#{fun}_fields"
build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :bool)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:enum, conv_fn]}) do
fields_fn = :"#{fun}_fields"
build_enum_handler_ast(fun, field_num, field_name, fields_fn, conv_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:message, decode_fn]}) do
fields_fn = :"#{fun}_fields"
build_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:message, decode_fn, validate_fn]}) do
fields_fn = :"#{fun}_fields"
build_validated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated_message, decode_fn]}) do
fields_fn = :"#{fun}_fields"
build_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated_message, decode_fn, validate_fn]}) do
fields_fn = :"#{fun}_fields"
build_validated_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:repeated, :string]}) do
fields_fn = :"#{fun}_fields"
build_repeated_string_handler_ast(fun, field_num, field_name, fields_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:map, :string, :string]}) do
fields_fn = :"#{fun}_fields"
build_map_handler_ast(fun, field_num, field_name, fields_fn)
end
defp build_field_handler_ast(fun, field_num, field_name, {:{}, _, [:oneof, decode_fn, tag]}) do
fields_fn = :"#{fun}_fields"
build_oneof_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, tag)
end
defp build_field_handler_ast(_fun, _field_num, _field_name, type_spec) do
raise "Unknown field type spec: #{inspect(type_spec)}"
end
# -- Scalar helpers --
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :string) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
end
end
type_spec == :uint ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :uint) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
end
end
type_spec == :int64 ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {raw, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Wire.decode_int64_value(raw)})
end
end
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :int64) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {raw, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Wire.decode_int64_value(raw)})
end
end
end
end
type_spec == :double ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 1, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_double(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :double) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 1, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_double(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value})
end
end
end
end
type_spec == :bool ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value != 0})
end
end
defp build_scalar_handler_ast(fun, field_num, field_name, fields_fn, :bool) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => value != 0})
end
end
end
end
# Tuple type specs arrive as {:{}, meta, [tag | args]}
match?({:{}, _, [:enum, _]}, type_spec) ->
{:{}, _, [_, conv_fn]} = type_spec
# -- Enum handler --
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest),
{:ok, converted} <- unquote(conv_fn).(value) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => converted})
end
end
defp build_enum_handler_ast(fun, field_num, field_name, fields_fn, conv_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 0, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_varint(rest),
{:ok, converted} <- unquote(conv_fn).(value) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => converted})
end
end
end
end
match?({:{}, _, [:message, _]}, type_spec) and
not match?({:{}, _, [:message, _, _]}, type_spec) ->
{:{}, _, [_, decode_fn]} = type_spec
# -- Message handlers --
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
end
end
defp build_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
end
end
end
end
match?({:{}, _, [:message, _, _]}, type_spec) ->
{:{}, _, [_, decode_fn, validate_fn]} = type_spec
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data),
{:ok, msg} <- unquote(validate_fn)(msg) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
end
end
defp build_validated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data),
{:ok, msg} <- unquote(validate_fn)(msg) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
end
end
end
end
match?({:{}, _, [:repeated_message, _]}, type_spec) and
not match?({:{}, _, [:repeated_message, _, _]}, type_spec) ->
{:{}, _, [_, decode_fn]} = type_spec
# -- Repeated message handlers --
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
end
end
defp build_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
end
end
end
end
match?({:{}, _, [:repeated_message, _, _]}, type_spec) ->
{:{}, _, [_, decode_fn, validate_fn]} = type_spec
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data),
{:ok, msg} <- unquote(validate_fn)(msg) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
end
end
defp build_validated_repeated_message_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, validate_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data),
{:ok, msg} <- unquote(validate_fn)(msg) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
end
end
end
end
match?({:{}, _, [:repeated, :string]}, type_spec) ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [value | acc.unquote(field_name)]})
end
end
# -- Repeated string handler --
defp build_repeated_string_handler_ast(fun, field_num, field_name, fields_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {value, rest2}} <- Wire.decode_bytes(rest) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [value | acc.unquote(field_name)]})
end
end
end
end
match?({:{}, _, [:map, :string, :string]}, type_spec) ->
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {entry_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, {key, val}} <- decode_map_entry(entry_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Map.put(acc.unquote(field_name), key, val)})
end
end
# -- Map handler --
defp build_map_handler_ast(fun, field_num, field_name, fields_fn) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {entry_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, {key, val}} <- decode_map_entry(entry_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => Map.put(acc.unquote(field_name), key, val)})
end
end
end
end
match?({:{}, _, [:oneof, _, _]}, type_spec) ->
{:{}, _, [_, decode_fn, tag]} = type_spec
# -- Oneof handler --
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => {unquote(tag), msg}})
end
end
defp build_oneof_handler_ast(fun, field_num, field_name, fields_fn, decode_fn, tag) do
quote do
defp unquote(:"#{fun}_field")(_data, acc, {:ok, {unquote(field_num), 2, rest}}) do
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
{:ok, msg} <- unquote(decode_fn)(msg_data) do
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => {unquote(tag), msg}})
end
true ->
raise "Unknown field type spec: #{inspect(type_spec)}"
end
end
end
end

View file

@ -229,6 +229,7 @@ defmodule Towerops.Snmp.Discovery do
{:ok, transceivers} = with_timeout(client_opts, fn -> Base.discover_transceivers(client_opts) end, timeouts)
Logger.info("Discovering printer supplies...", device_id: device.id)
{:ok, printer_supplies} =
with_timeout(client_opts, fn -> Base.discover_printer_supplies(client_opts) end, timeouts)
@ -316,7 +317,8 @@ defmodule Towerops.Snmp.Discovery do
timeout: timeouts[:slow],
default: {:error, :interface_discovery_timeout}
) do
{:ok, _ifaces} = ok -> ok
{:ok, _ifaces} = ok ->
ok
{:error, :interface_discovery_timeout} ->
Logger.error("Interface discovery timed out - aborting to prevent data loss", device_id: device_id)
@ -334,7 +336,8 @@ defmodule Towerops.Snmp.Discovery do
timeout: timeouts[:slow],
default: {:error, :sensor_discovery_timeout}
) do
{:ok, _sensors} = ok -> ok
{:ok, _sensors} = ok ->
ok
{:error, :sensor_discovery_timeout} ->
Logger.error("Sensor discovery timed out - aborting to prevent data loss", device_id: device_id)
@ -1604,8 +1607,7 @@ defmodule Towerops.Snmp.Discovery do
@spec update_device_name_from_snmp(DeviceSchema.t(), system_info()) ::
{:ok, DeviceSchema.t()} | {:error, term()}
defp update_device_name_from_snmp(%{name: name} = device, _system_info)
when is_binary(name) and name != "" do
defp update_device_name_from_snmp(%{name: name} = device, _system_info) when is_binary(name) and name != "" do
Logger.info("Device name already set or no sysName, skipping update", device_id: device.id)
{:ok, device}
end

View file

@ -243,9 +243,9 @@
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Drive Towerops as code. The Ansible collection wraps every resource on this page with idempotent modules: <a
href="https://codeberg.org/towerops/ansible"
href="https://git.mcintire.me/towerops/ansible"
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
>codeberg.org/towerops/ansible</a>.
>git.mcintire.me/towerops/ansible</a>.
</p>
</div>
</section>
@ -3922,7 +3922,7 @@ curl -G https://towerops.net/api/v1/activity \\
and progresses through <code>computing</code>
to <code>ready</code>
or <code>failed</code>. The status field is present on every response so polling-based clients (Terraform, the <a
href="https://codeberg.org/towerops/ansible"
href="https://git.mcintire.me/towerops/ansible"
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
>Ansible collection</a>, scripts) can wait for a healthy resource. Outputs include a coloured PNG heatmap, a Float32 GeoTIFF raster, and a downloadable KMZ bundle for Google Earth.
</p>

View file

@ -326,9 +326,9 @@
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Prefer infrastructure-as-code? The Towerops Ansible collection covers every resource exposed here: <a
href="https://codeberg.org/towerops/ansible"
href="https://git.mcintire.me/towerops/ansible"
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
>codeberg.org/towerops/ansible</a>.
>git.mcintire.me/towerops/ansible</a>.
</p>
</div>
</section>

View file

@ -199,7 +199,7 @@
and starts collecting metrics. No firewall changes, no VPN, no open ports.
</p>
<div class="mt-6 rounded-lg bg-slate-900 dark:bg-black/50 dark:ring-1 dark:ring-white/10 p-4 font-mono text-sm text-green-400">
$ docker run -d codeberg.org/towerops-agent/towerops-agent
$ docker run -d git.mcintire.me/towerops-agent/towerops-agent
</div>
</div>
<!-- Step 2 -->

View file

@ -49,7 +49,7 @@ defmodule ToweropsWeb.AgentLive.Index do
Application.get_env(
:towerops,
:agent_docker_image,
"codeberg.org/towerops-agent/towerops-agent:latest"
"git.mcintire.me/towerops-agent/towerops-agent:latest"
)
{:ok,

View file

@ -31,9 +31,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
@doc """
Poll sensors for live mode, dispatching based on sensor type.
"""
def poll_sensors(assigns, client_opts, timestamp_ms) do
def poll_sensors(assigns, client_opts, timestamp_ms, prev_stats) do
if assigns.sensor_type == "traffic" do
poll_traffic(assigns, client_opts, timestamp_ms)
poll_traffic(assigns, client_opts, timestamp_ms, prev_stats)
else
poll_regular_sensors(assigns, client_opts, timestamp_ms)
end
@ -62,7 +62,7 @@ defmodule ToweropsWeb.GraphLive.Polling do
Poll traffic stats (used from non-state-updating path).
Requires two readings to calculate BPS; uses `previous_interface_stats` from assigns.
"""
def poll_traffic(assigns, client_opts, timestamp_ms) do
def poll_traffic(assigns, client_opts, timestamp_ms, prev_stats) do
device = Snmp.get_device_with_associations(assigns.device_id)
if is_nil(device) || Enum.empty?(device.interfaces) do
@ -70,9 +70,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
else
current_stats = poll_interface_stats_parallel(device.interfaces, client_opts)
if assigns.previous_interface_stats do
if prev_stats do
calculate_traffic_bps_from_stats(
assigns.previous_interface_stats,
prev_stats,
current_stats,
timestamp_ms
)
@ -86,7 +86,7 @@ defmodule ToweropsWeb.GraphLive.Polling do
Poll traffic with socket state update.
Returns `{data_points, updated_stats}` for the caller to assign back.
"""
def poll_traffic_with_state(assigns, client_opts, timestamp_ms) do
def poll_traffic_with_state(assigns, client_opts, timestamp_ms, prev_stats) do
device = Snmp.get_device_with_associations(assigns.device_id)
if is_nil(device) || Enum.empty?(device.interfaces) do
@ -95,9 +95,9 @@ defmodule ToweropsWeb.GraphLive.Polling do
current_stats = poll_interface_stats_parallel(device.interfaces, client_opts)
data_points =
if assigns.previous_interface_stats do
if prev_stats do
calculate_traffic_bps_from_stats(
assigns.previous_interface_stats,
prev_stats,
current_stats,
timestamp_ms
)

View file

@ -353,7 +353,6 @@ defmodule ToweropsWeb.GraphLive.Show do
|> assign(:show_zero_line, sensor_type == "traffic")
|> assign(:max_value, nil)
|> assign(:min_value, nil)
|> assign(:previous_interface_stats, nil)
end
defp get_title_suffix(assigns) do
@ -1070,10 +1069,12 @@ defmodule ToweropsWeb.GraphLive.Show do
client_opts = Polling.build_snmp_client_opts(device)
if socket.assigns.sensor_type == "traffic" do
{points, stats} = Polling.poll_traffic_with_state(socket.assigns, client_opts, timestamp_ms)
prev_stats = Map.get(socket.assigns, :previous_interface_stats)
{points, stats} = Polling.poll_traffic_with_state(socket.assigns, client_opts, timestamp_ms, prev_stats)
{points, stats}
else
points = Polling.poll_sensors(socket.assigns, client_opts, timestamp_ms)
prev_stats = Map.get(socket.assigns, :previous_interface_stats)
points = Polling.poll_sensors(socket.assigns, client_opts, timestamp_ms, prev_stats)
{points, nil}
end
end

View file

@ -23,7 +23,7 @@ defmodule ToweropsWeb.HelpLive.Sections.About do
<li>No data collection or sharing.</li>
<li>
Our remote agent is
<a href="https://codeberg.org/towerops-agent/towerops-agent">
<a href="https://git.mcintire.me/towerops-agent/towerops-agent">
fully open source
</a>
and ONLY collects monitoring jobs from the server and WILL NEVER allow us to access any part of your internal network.

View file

@ -81,7 +81,7 @@ defmodule ToweropsWeb.HelpLive.Sections.Agents do
<div class="mt-3 p-4 bg-black rounded-lg not-prose overflow-x-auto">
<pre class="text-xs text-green-400 font-mono"><code><%= raw(~S[services:
towerops-agent:
image: codeberg.org/towerops-agent/towerops-agent:latest
image: git.mcintire.me/towerops-agent/towerops-agent:latest
container_name: towerops-agent
restart: unless-stopped
environment:

View file

@ -347,7 +347,7 @@
-e TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
-e TOWEROPS_SERVER=<%= ToweropsWeb.Endpoint.url() %> \
--network host \
codeberg.org/towerops-agent/towerops-agent:latest</code></pre>
git.mcintire.me/towerops-agent/towerops-agent:latest</code></pre>
</div>
<div>

View file

@ -2,7 +2,7 @@ syntax = "proto3";
package towerops.agent;
option go_package = "codeberg.org/towerops-agent/towerops-agent/pb";
option go_package = "git.mcintire.me/towerops-agent/towerops-agent/pb";
// Configuration received from the API
message AgentConfig {

View file

@ -1980,7 +1980,7 @@ defmodule Towerops.AgentsTest do
Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:lifecycle")
url =
"https://codeberg.org/towerops-agent/towerops-agent/releases/download/v0.2.0/towerops-agent-linux-amd64"
"https://git.mcintire.me/towerops-agent/towerops-agent/releases/download/v0.2.0/towerops-agent-linux-amd64"
checksum = "abc123def456"