refactor: comprehensive simplification and functional programming improvements
- help_live/index.ex: 2,642→173 lines, 15 section modules + sidebar extracted MikroTik now real section, dead if false removed - agent_channel.ex: 2,506→1,751 lines, 3 helper modules extracted (heartbeat/subscriptions/job_builder), decode_and_process/4 eliminates 8 repeated handle_in patterns, guard-based size checks - antenna_catalog.ex: 1,174→47 lines, 107 specs → priv/antennas/catalog.json - topology.ex: unbounded query → batched loading (100/batch), all guard errors fixed, zero if/case/cond conditionals - proto: decoder_macros.ex + field_specs.ex infrastructure for macro-generated protobuf decoders
This commit is contained in:
parent
5b8f83671e
commit
9d4a5f6d81
26 changed files with 5577 additions and 4040 deletions
371
lib/towerops/proto/decoder_macros.ex
Normal file
371
lib/towerops/proto/decoder_macros.ex
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
defmodule Towerops.Proto.DecoderMacros do
|
||||
@moduledoc """
|
||||
Macros for generating protobuf message decoders from declarative field specs.
|
||||
|
||||
Uses pattern-matched function heads for dispatch -- no `case`, `cond`, or `if`
|
||||
in the generated code.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generate a struct-based decoder.
|
||||
|
||||
## Options
|
||||
* `:validate` -- optional validation function atom (e.g. `:validate_heartbeat`)
|
||||
* `:finalize` -- optional finalization specification
|
||||
- `nil` (default) -- no finalization, return accumulator as-is
|
||||
- `{:reverse, fields}` -- reverse the specified list fields
|
||||
* `:defaults` -- keyword list of field default value overrides
|
||||
|
||||
## Field types
|
||||
`:string`, `:uint`, `:int64`, `:double`, `:bool`,
|
||||
`{:enum, conv_fn}`, `{:message, decode_fn}`,
|
||||
`{:message, decode_fn, validate_fn}`,
|
||||
`{:repeated_message, decode_fn}`,
|
||||
`{:repeated_message, decode_fn, validate_fn}`,
|
||||
`{:repeated, :string}`, `{:map, :string, :string}`,
|
||||
`{:oneof, decode_fn, tag}`
|
||||
"""
|
||||
defmacro defdecoder(mod, fun, fields, opts \\ []) do
|
||||
# Evaluate opts at compile time (keyword list literal)
|
||||
validate = Keyword.get(opts, :validate)
|
||||
finalize = Keyword.get(opts, :finalize)
|
||||
overrides_map = opts |> Keyword.get(:defaults, []) |> Map.new()
|
||||
|
||||
# Destructure fields AST -- each entry is {:{}, _, [num, name, type]}
|
||||
defaults = build_defaults_ast(fields, overrides_map)
|
||||
fields_fn = :"#{fun}_fields"
|
||||
field_fn = :"#{fun}_field"
|
||||
validate_fn = :"#{fun}_apply_validate"
|
||||
|
||||
field_handlers =
|
||||
for field_ast <- fields do
|
||||
{:{}, _, [field_num, field_name, type_spec]} = field_ast
|
||||
build_field_handler_ast(fun, field_num, field_name, type_spec)
|
||||
end
|
||||
|
||||
finalize_clause = build_finalize_clause_ast(finalize)
|
||||
validate_clause = build_validate_clause_ast(validate_fn, validate)
|
||||
|
||||
quote location: :keep do
|
||||
def unquote(fun)(data) when is_binary(data) do
|
||||
acc = struct(unquote(mod), unquote(defaults))
|
||||
|
||||
with {:ok, result} <- unquote(fields_fn)(data, acc) do
|
||||
unquote(validate_fn)(result)
|
||||
end
|
||||
end
|
||||
|
||||
defp unquote(fields_fn)(<<>>, acc), do: {:ok, unquote(finalize_clause)}
|
||||
|
||||
defp unquote(fields_fn)(data, acc) do
|
||||
unquote(field_fn)(data, acc, Wire.decode_tag(data))
|
||||
end
|
||||
|
||||
unquote(field_handlers)
|
||||
|
||||
defp unquote(field_fn)(_data, acc, {:ok, {_fn, wt, rest}}) do
|
||||
with {:ok, rest2} <- Wire.skip_field(wt, rest) do
|
||||
unquote(fields_fn)(rest2, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp unquote(field_fn)(_data, _acc, {:error, _} = error), do: error
|
||||
|
||||
unquote(validate_clause)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate a decoder that accumulates repeated sub-messages into a list,
|
||||
then wraps in a struct.
|
||||
|
||||
## Options
|
||||
* `:validate_item` -- optional item validation function
|
||||
* `:max` -- optional maximum list size (creates batch-too-large check)
|
||||
"""
|
||||
defmacro deflistdecoder(fun, wrapper_mod, field_name, decode_fn, opts \\ []) do
|
||||
max_size = Keyword.get(opts, :max)
|
||||
validate_item = Keyword.get(opts, :validate_item)
|
||||
fields_fn = :"#{fun}_fields"
|
||||
field_fn = :"#{fun}_field"
|
||||
|
||||
finalize_clause = build_list_finalize_ast(fun, wrapper_mod, field_name, max_size)
|
||||
|
||||
item_handler =
|
||||
if validate_item do
|
||||
quote do
|
||||
defp unquote(field_fn)(_data, acc, {:ok, {1, wt, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, item} <- unquote(decode_fn)(msg_data),
|
||||
{:ok, item} <- unquote(validate_item)(item) do
|
||||
unquote(fields_fn)(rest2, [item | acc])
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
quote do
|
||||
defp unquote(field_fn)(_data, acc, {:ok, {1, wt, rest}}) do
|
||||
with {:ok, {msg_data, rest2}} <- Wire.decode_bytes(rest),
|
||||
{:ok, item} <- unquote(decode_fn)(msg_data) do
|
||||
unquote(fields_fn)(rest2, [item | acc])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quote location: :keep do
|
||||
def unquote(fun)(data) when is_binary(data) do
|
||||
with {:ok, items} <- unquote(fields_fn)(data, []) do
|
||||
unquote(:"#{fun}_finalize")(items)
|
||||
end
|
||||
end
|
||||
|
||||
defp unquote(fields_fn)(<<>>, acc), do: {:ok, acc}
|
||||
|
||||
defp unquote(fields_fn)(data, acc) do
|
||||
unquote(field_fn)(data, acc, Wire.decode_tag(data))
|
||||
end
|
||||
|
||||
unquote(item_handler)
|
||||
|
||||
defp unquote(field_fn)(_data, acc, {:ok, {_fn, wt, rest}}) do
|
||||
with {:ok, rest2} <- Wire.skip_field(wt, rest) do
|
||||
unquote(fields_fn)(rest2, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp unquote(field_fn)(_data, _acc, {:error, _} = error), do: error
|
||||
|
||||
unquote(finalize_clause)
|
||||
end
|
||||
end
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Compile-time helpers (work with AST values)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
defp build_defaults_ast(fields_ast, overrides_map) do
|
||||
for field_ast <- fields_ast do
|
||||
{:{}, _, [_num, name, type]} = field_ast
|
||||
{name, Map.get(overrides_map, name, default_for_type_ast(type))}
|
||||
end
|
||||
end
|
||||
|
||||
defp default_for_type_ast(:string), do: ""
|
||||
defp default_for_type_ast(:uint), do: 0
|
||||
defp default_for_type_ast(:int64), do: 0
|
||||
defp default_for_type_ast(:double), do: 0.0
|
||||
defp default_for_type_ast(:bool), do: false
|
||||
# Tuple type specs arrive as AST tuples {:{}, meta, [tag | args]}
|
||||
defp default_for_type_ast({:{}, _, [:enum, _]}), do: nil
|
||||
defp default_for_type_ast({:{}, _, [:message, _]}), do: nil
|
||||
defp default_for_type_ast({:{}, _, [:message, _, _]}), do: nil
|
||||
defp default_for_type_ast({:{}, _, [:repeated_message, _]}), do: []
|
||||
defp default_for_type_ast({:{}, _, [:repeated_message, _, _]}), do: []
|
||||
defp default_for_type_ast({:{}, _, [:repeated, :string]}), do: []
|
||||
defp default_for_type_ast({:{}, _, [:map, :string, :string]}), do: %{}
|
||||
defp default_for_type_ast({:{}, _, [:oneof, _, _]}), do: nil
|
||||
|
||||
defp build_finalize_clause_ast(nil), do: quote(do: acc)
|
||||
|
||||
defp build_finalize_clause_ast({:reverse, fields}) do
|
||||
reverses =
|
||||
Enum.map(fields, fn field ->
|
||||
quote do
|
||||
{unquote(field), Enum.reverse(acc.unquote(field))}
|
||||
end
|
||||
end)
|
||||
|
||||
quote do
|
||||
%{acc | unquote_splicing(reverses)}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_validate_clause_ast(vfn, nil) do
|
||||
quote do
|
||||
defp unquote(vfn)(result), do: {:ok, result}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_validate_clause_ast(vfn, validate) do
|
||||
quote do
|
||||
defp unquote(vfn)(result), do: unquote(validate)(result)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_list_finalize_ast(fun, wrapper_mod, field_name, nil) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_finalize")(items) do
|
||||
{:ok, %unquote(wrapper_mod){unquote(field_name) => Enum.reverse(items)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp build_list_finalize_ast(fun, wrapper_mod, field_name, max_size) do
|
||||
quote do
|
||||
defp unquote(:"#{fun}_finalize")(items) do
|
||||
reversed = Enum.reverse(items)
|
||||
unquote(:"#{fun}_check_size")(reversed)
|
||||
end
|
||||
|
||||
defp unquote(:"#{fun}_check_size")(result) when length(result) <= unquote(max_size) do
|
||||
{:ok, %unquote(wrapper_mod){unquote(field_name) => result}}
|
||||
end
|
||||
|
||||
defp unquote(:"#{fun}_check_size")(_result) do
|
||||
{:error, {:batch_too_large, "Batch exceeds #{unquote(max_size)} metrics"}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Generate a field handler clause for a single field
|
||||
defp build_field_handler_ast(fun, field_num, field_name, type_spec) do
|
||||
fields_fn = :"#{fun}_fields"
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
# Tuple type specs arrive as {:{}, meta, [tag | args]}
|
||||
match?({:{}, _, [:enum, _]}, type_spec) ->
|
||||
{:{}, _, [_, conv_fn]} = type_spec
|
||||
|
||||
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
|
||||
|
||||
match?({:{}, _, [:message, _]}, type_spec) and
|
||||
not match?({:{}, _, [:message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_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) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => msg})
|
||||
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
|
||||
end
|
||||
|
||||
match?({:{}, _, [:repeated_message, _]}, type_spec) and
|
||||
not match?({:{}, _, [:repeated_message, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_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) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => [msg | acc.unquote(field_name)]})
|
||||
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
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
match?({:{}, _, [:oneof, _, _]}, type_spec) ->
|
||||
{:{}, _, [_, decode_fn, tag]} = 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) do
|
||||
unquote(fields_fn)(rest2, %{acc | unquote(field_name) => {unquote(tag), msg}})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true ->
|
||||
raise "Unknown field type spec: #{inspect(type_spec)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
226
lib/towerops/proto/field_specs.ex
Normal file
226
lib/towerops/proto/field_specs.ex
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
defmodule Towerops.Proto.FieldSpecs do
|
||||
@moduledoc """
|
||||
Declarative field specifications for all protobuf message decoders.
|
||||
|
||||
This module documents every message type and its wire format fields.
|
||||
Each spec tuple: `{struct_module, decode_function_name, fields_list, opts_keyword}`
|
||||
|
||||
Field types: `:string`, `:uint`, `:int64`, `:double`, `:bool`,
|
||||
`{:enum, conv_fn}`, `{:message, decode_fn}`, `{:oneof, decode_fn, tag}`,
|
||||
`{:repeated, :string}`, `{:map, :string, :string}`,
|
||||
`{:repeated_message, decode_fn}`, `{:repeated_message, decode_fn, validate_fn}`.
|
||||
|
||||
Options: `validate`, `finalize`, `defaults`.
|
||||
"""
|
||||
|
||||
alias Towerops.Proto.Types
|
||||
|
||||
@doc "Return all decoder specifications."
|
||||
def all do
|
||||
[
|
||||
# ── Agent → Server messages ──────────────────────
|
||||
|
||||
{Types.AgentHeartbeat, :decode_agent_heartbeat,
|
||||
[
|
||||
{1, :version, :string},
|
||||
{2, :hostname, :string},
|
||||
{3, :uptime_seconds, :uint},
|
||||
{4, :ip_address, :string},
|
||||
{5, :arch, :string}
|
||||
], validate: :validate_heartbeat},
|
||||
{Types.HeartbeatMetadata, :decode_heartbeat_metadata,
|
||||
[{1, :version, :string}, {2, :hostname, :string}, {3, :uptime_seconds, :uint}]},
|
||||
{Types.HeartbeatResponse, :decode_heartbeat_response, [{1, :status, :string}]},
|
||||
{Types.SnmpResult, :decode_snmp_result,
|
||||
[
|
||||
{1, :device_id, :string},
|
||||
{2, :job_type, {:enum, &Types.job_type_from_int/1}},
|
||||
{3, :oid_values, {:map, :string, :string}},
|
||||
{4, :timestamp, :int64},
|
||||
{5, :job_id, :string}
|
||||
], validate: :validate_snmp_result, defaults: [job_type: :discover]},
|
||||
{Types.AgentError, :decode_agent_error,
|
||||
[{1, :device_id, :string}, {2, :job_id, :string}, {3, :message, :string}, {4, :timestamp, :int64}],
|
||||
validate: :validate_agent_error},
|
||||
{Types.CredentialTestResult, :decode_credential_test_result,
|
||||
[
|
||||
{1, :test_id, :string},
|
||||
{2, :success, :bool},
|
||||
{3, :error_message, :string},
|
||||
{4, :system_description, :string},
|
||||
{5, :timestamp, :int64}
|
||||
], validate: :validate_credential_test_result},
|
||||
{Types.MikrotikResult, :decode_mikrotik_result,
|
||||
[
|
||||
{1, :device_id, :string},
|
||||
{2, :job_id, :string},
|
||||
{3, :sentences, {:repeated_message, :decode_mikrotik_sentence}},
|
||||
{4, :error, :string},
|
||||
{5, :timestamp, :int64}
|
||||
], validate: :validate_mikrotik_result, finalize: {:reverse, [:sentences]}},
|
||||
{Types.MonitoringCheck, :decode_monitoring_check,
|
||||
[{1, :device_id, :string}, {2, :status, :string}, {3, :response_time_ms, :double}, {4, :timestamp, :int64}],
|
||||
validate: :validate_monitoring_check},
|
||||
{Types.LldpTopologyResult, :decode_lldp_topology_result,
|
||||
[
|
||||
{1, :device_id, :string},
|
||||
{2, :job_id, :string},
|
||||
{3, :local_system_name, :string},
|
||||
{4, :neighbors, {:repeated_message, :decode_lldp_neighbor}},
|
||||
{5, :timestamp, :int64}
|
||||
], validate: :validate_lldp_topology_result, finalize: {:reverse, [:neighbors]}},
|
||||
{Types.CheckResult, :decode_check_result,
|
||||
[
|
||||
{1, :check_id, :string},
|
||||
{2, :status, :uint},
|
||||
{3, :output, :string},
|
||||
{4, :response_time_ms, :double},
|
||||
{5, :timestamp, :int64}
|
||||
], validate: :validate_check_result},
|
||||
{Types.Sensor, :decode_sensor,
|
||||
[
|
||||
{1, :id, :string},
|
||||
{2, :sensor_type, :string},
|
||||
{3, :oid, :string},
|
||||
{4, :divisor, :double},
|
||||
{5, :unit, :string},
|
||||
{6, :metadata, {:map, :string, :string}}
|
||||
]},
|
||||
|
||||
# ── Server → Agent messages ──────────────────────
|
||||
|
||||
{Types.AgentJob, :decode_agent_job,
|
||||
[
|
||||
{1, :job_id, :string},
|
||||
{2, :job_type, {:enum, &Types.job_type_from_int/1}},
|
||||
{3, :device_id, :string},
|
||||
{4, :snmp_device, {:message, :decode_snmp_device, :validate_snmp_device}},
|
||||
{5, :queries, {:repeated_message, :decode_snmp_query, :validate_snmp_query}},
|
||||
{6, :mikrotik_device, {:message, :decode_mikrotik_device, :validate_mikrotik_device}},
|
||||
{7, :mikrotik_commands, {:repeated_message, :decode_mikrotik_command, :validate_mikrotik_command}}
|
||||
], defaults: [job_type: :discover], finalize: {:reverse, [:queries, :mikrotik_commands]}},
|
||||
{Types.AgentConfig, :decode_agent_config,
|
||||
[
|
||||
{1, :version, :string},
|
||||
{2, :poll_interval_seconds, :uint},
|
||||
{3, :devices, {:repeated_message, :decode_device}},
|
||||
{4, :checks, {:repeated_message, :decode_check, :validate_check}}
|
||||
], finalize: {:reverse, [:devices, :checks]}},
|
||||
|
||||
# ── Nested sub-messages ──────────────────────────
|
||||
|
||||
{Types.Device, :decode_device,
|
||||
[
|
||||
{1, :id, :string},
|
||||
{2, :name, :string},
|
||||
{3, :ip_address, :string},
|
||||
{4, :snmp, {:message, :decode_snmp_config}},
|
||||
{5, :poll_interval_seconds, :uint},
|
||||
{6, :sensors, {:repeated_message, :decode_sensor, :validate_sensor}},
|
||||
{7, :interfaces, {:repeated_message, :decode_interface, :validate_interface}},
|
||||
{8, :monitoring_enabled, :bool},
|
||||
{9, :check_interval_seconds, :uint}
|
||||
], finalize: {:reverse, [:sensors, :interfaces]}},
|
||||
{Types.SnmpConfig, :decode_snmp_config,
|
||||
[
|
||||
{1, :enabled, :bool},
|
||||
{2, :version, :string},
|
||||
{3, :community, :string},
|
||||
{4, :port, :uint},
|
||||
{5, :transport, :string}
|
||||
]},
|
||||
{Types.SensorReading, :decode_sensor_reading,
|
||||
[{1, :sensor_id, :string}, {2, :value, :double}, {3, :status, :string}, {4, :timestamp, :int64}]},
|
||||
{Types.InterfaceStat, :decode_interface_stat,
|
||||
[
|
||||
{1, :interface_id, :string},
|
||||
{2, :if_in_octets, :int64},
|
||||
{3, :if_out_octets, :int64},
|
||||
{4, :if_in_errors, :int64},
|
||||
{5, :if_out_errors, :int64},
|
||||
{6, :if_in_discards, :int64},
|
||||
{7, :if_out_discards, :int64},
|
||||
{8, :timestamp, :int64}
|
||||
]},
|
||||
{Types.NeighborDiscovery, :decode_neighbor_discovery,
|
||||
[
|
||||
{1, :interface_id, :string},
|
||||
{2, :protocol, :string},
|
||||
{3, :remote_chassis_id, :string},
|
||||
{4, :remote_system_name, :string},
|
||||
{5, :remote_system_description, :string},
|
||||
{6, :remote_platform, :string},
|
||||
{7, :remote_port_id, :string},
|
||||
{8, :remote_port_description, :string},
|
||||
{9, :remote_address, :string},
|
||||
{10, :remote_capabilities, {:repeated, :string}},
|
||||
{11, :timestamp, :int64}
|
||||
]},
|
||||
{Types.Interface, :decode_interface, [{1, :id, :string}, {2, :if_index, :uint}, {3, :if_name, :string}]},
|
||||
{Types.Check, :decode_check,
|
||||
[
|
||||
{1, :id, :string},
|
||||
{2, :check_type, :string},
|
||||
{3, :interval_seconds, :uint},
|
||||
{4, :timeout_ms, :uint},
|
||||
{5, :config, {:oneof, :decode_http_check_config, :http}},
|
||||
{6, :config, {:oneof, :decode_tcp_check_config, :tcp}},
|
||||
{7, :config, {:oneof, :decode_dns_check_config, :dns}},
|
||||
{8, :config, {:oneof, :decode_ssl_check_config, :ssl}}
|
||||
], defaults: [config: :no_config]},
|
||||
{Types.HttpCheckConfig, :decode_http_check_config,
|
||||
[
|
||||
{1, :url, :string},
|
||||
{2, :method, :string},
|
||||
{3, :expected_status, :uint},
|
||||
{4, :verify_ssl, :bool},
|
||||
{5, :headers, {:map, :string, :string}},
|
||||
{6, :body, :string},
|
||||
{7, :regex, :string},
|
||||
{8, :follow_redirects, :bool}
|
||||
]},
|
||||
{Types.TcpCheckConfig, :decode_tcp_check_config,
|
||||
[{1, :host, :string}, {2, :port, :uint}, {3, :send, :string}, {4, :expect, :string}]},
|
||||
{Types.DnsCheckConfig, :decode_dns_check_config,
|
||||
[{1, :hostname, :string}, {2, :server, :string}, {3, :record_type, :string}, {4, :expected, :string}]},
|
||||
{Types.SslCheckConfig, :decode_ssl_check_config,
|
||||
[{1, :host, :string}, {2, :port, :uint}, {3, :warning_days, :uint}]},
|
||||
{Types.SnmpDevice, :decode_snmp_device,
|
||||
[
|
||||
{1, :ip, :string},
|
||||
{2, :community, :string},
|
||||
{3, :version, :string},
|
||||
{4, :port, :uint},
|
||||
{5, :v3_security_level, :string},
|
||||
{6, :v3_username, :string},
|
||||
{7, :v3_auth_protocol, :string},
|
||||
{8, :v3_auth_password, :string},
|
||||
{9, :v3_priv_protocol, :string},
|
||||
{10, :v3_priv_password, :string},
|
||||
{11, :transport, :string}
|
||||
], validate: :validate_snmp_device},
|
||||
{Types.SnmpQuery, :decode_snmp_query,
|
||||
[{1, :query_type, {:enum, &Types.query_type_from_int/1}}, {2, :oids, {:repeated, :string}}],
|
||||
validate: :validate_snmp_query, defaults: [query_type: :get], finalize: {:reverse, [:oids]}},
|
||||
{Types.MikrotikDevice, :decode_mikrotik_device,
|
||||
[
|
||||
{1, :ip, :string},
|
||||
{2, :port, :uint},
|
||||
{3, :username, :string},
|
||||
{4, :password, :string},
|
||||
{5, :use_ssl, :bool},
|
||||
{6, :ssh_port, :uint}
|
||||
], validate: :validate_mikrotik_device},
|
||||
{Types.MikrotikCommand, :decode_mikrotik_command, [{1, :command, :string}, {2, :args, {:map, :string, :string}}],
|
||||
validate: :validate_mikrotik_command},
|
||||
{Types.LldpNeighbor, :decode_lldp_neighbor,
|
||||
[
|
||||
{1, :neighbor_name, :string},
|
||||
{2, :local_port, :string},
|
||||
{3, :remote_port, :string},
|
||||
{4, :remote_port_id, :string},
|
||||
{5, :management_addresses, {:repeated, :string}}
|
||||
], finalize: {:reverse, [:management_addresses]}}
|
||||
]
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
75
lib/towerops_web/channels/agent_channel/heartbeat.ex
Normal file
75
lib/towerops_web/channels/agent_channel/heartbeat.ex
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
defmodule ToweropsWeb.AgentChannel.Heartbeat do
|
||||
@moduledoc """
|
||||
Handles heartbeat processing for agent channels.
|
||||
|
||||
Receives a decoded `AgentHeartbeat` protobuf struct and the current socket,
|
||||
and returns `{:noreply, socket}` with updated heartbeat tracking.
|
||||
|
||||
## Rate-limiting
|
||||
|
||||
Database writes are throttled to at most one per 30 seconds to prevent
|
||||
flooding. In-memory heartbeat tracking (`last_heartbeat_at`) is always
|
||||
updated on every heartbeat.
|
||||
"""
|
||||
|
||||
alias Towerops.Agent.AgentHeartbeat
|
||||
alias Towerops.Agents
|
||||
alias ToweropsWeb.RemoteIp
|
||||
|
||||
@doc """
|
||||
Processes a decoded heartbeat message from an agent.
|
||||
|
||||
Updates the `last_seen_at` timestamp in the database (rate-limited),
|
||||
broadcasts a health event for real-time UI, and updates in-memory
|
||||
heartbeat tracking on the socket.
|
||||
"""
|
||||
@spec process(AgentHeartbeat.t(), Phoenix.Socket.t()) :: {:noreply, Phoenix.Socket.t()}
|
||||
def process(heartbeat, socket) do
|
||||
now = DateTime.utc_now()
|
||||
do_process(heartbeat, socket, now, socket.assigns[:last_heartbeat_db_update])
|
||||
end
|
||||
|
||||
# No previous DB update — always persist to database
|
||||
defp do_process(heartbeat, socket, now, nil) do
|
||||
{:noreply, persist_heartbeat(heartbeat, socket, now)}
|
||||
end
|
||||
|
||||
# Previous DB update exists — only persist if stale (>30s)
|
||||
defp do_process(heartbeat, socket, now, last_db_update) do
|
||||
socket = do_process_with_staleness(heartbeat, socket, now, last_db_update)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp do_process_with_staleness(heartbeat, socket, now, last_db_update) do
|
||||
do_process_stale(DateTime.diff(now, last_db_update) > 30, heartbeat, socket, now)
|
||||
end
|
||||
|
||||
defp do_process_stale(true, heartbeat, socket, now), do: persist_heartbeat(heartbeat, socket, now)
|
||||
defp do_process_stale(false, _heartbeat, socket, now), do: Phoenix.Socket.assign(socket, :last_heartbeat_at, now)
|
||||
|
||||
defp persist_heartbeat(heartbeat, socket, now) do
|
||||
metadata = %{
|
||||
"version" => heartbeat.version,
|
||||
"uptime_seconds" => heartbeat.uptime_seconds,
|
||||
"arch" => heartbeat.arch
|
||||
}
|
||||
|
||||
_ =
|
||||
Agents.update_agent_token_heartbeat(
|
||||
socket.assigns.agent_token_id,
|
||||
RemoteIp.from_socket(socket),
|
||||
metadata
|
||||
)
|
||||
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"agents:health",
|
||||
{:agent_heartbeat, socket.assigns.agent_token_id, socket.assigns.organization_id}
|
||||
)
|
||||
|
||||
socket
|
||||
|> Phoenix.Socket.assign(:last_heartbeat_at, now)
|
||||
|> Phoenix.Socket.assign(:last_heartbeat_db_update, now)
|
||||
end
|
||||
end
|
||||
589
lib/towerops_web/channels/agent_channel/job_builder.ex
Normal file
589
lib/towerops_web/channels/agent_channel/job_builder.ex
Normal file
|
|
@ -0,0 +1,589 @@
|
|||
defmodule ToweropsWeb.AgentChannel.JobBuilder do
|
||||
@moduledoc """
|
||||
Builds protobuf job messages for agent channels.
|
||||
|
||||
Provides functions to construct `AgentJob` and `CheckList` protobuf messages
|
||||
from device and check data, handling SNMP credential resolution, discovery
|
||||
vs. polling job selection, and MikroTik backup job construction.
|
||||
"""
|
||||
|
||||
alias Towerops.Agent.AgentJob
|
||||
alias Towerops.Agent.Check, as: CheckProto
|
||||
alias Towerops.Agent.CheckList
|
||||
alias Towerops.Agent.DnsCheckConfig
|
||||
alias Towerops.Agent.HttpCheckConfig
|
||||
alias Towerops.Agent.MikrotikCommand
|
||||
alias Towerops.Agent.MikrotikDevice
|
||||
alias Towerops.Agent.SnmpDevice
|
||||
alias Towerops.Agent.SnmpQuery
|
||||
alias Towerops.Agent.SslCheckConfig
|
||||
alias Towerops.Agent.TcpCheckConfig
|
||||
alias Towerops.Agents
|
||||
alias Towerops.Devices
|
||||
alias Towerops.Monitoring
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Builds all jobs for an agent based on assigned polling targets.
|
||||
"""
|
||||
@spec build_jobs_for_agent(Ecto.UUID.t()) :: [AgentJob.t()]
|
||||
def build_jobs_for_agent(agent_token_id) do
|
||||
agent_token_id
|
||||
|> Agents.list_agent_polling_targets()
|
||||
|> Enum.flat_map(&build_jobs_for_device/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds all jobs for a single device (SNMP discovery/polling, MikroTik, ping).
|
||||
"""
|
||||
@spec build_jobs_for_device(map()) :: [AgentJob.t()]
|
||||
def build_jobs_for_device(device) do
|
||||
[]
|
||||
|> maybe_put_snmp_job(device)
|
||||
|> maybe_put_mikrotik_job(device)
|
||||
|> maybe_put_ping_job(device)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp maybe_put_snmp_job(jobs, %{snmp_enabled: true} = device) do
|
||||
[snmp_job_for_device(device) | jobs]
|
||||
end
|
||||
|
||||
defp maybe_put_snmp_job(jobs, _device), do: jobs
|
||||
|
||||
defp snmp_job_for_device(device) do
|
||||
do_snmp_job(needs_discovery?(device), device)
|
||||
end
|
||||
|
||||
defp do_snmp_job(true, device), do: build_discovery_job(device)
|
||||
defp do_snmp_job(false, device), do: build_polling_job(device)
|
||||
|
||||
defp maybe_put_mikrotik_job(jobs, %{snmp_enabled: true} = device) do
|
||||
[mikrotik_conditional_job(device) | jobs]
|
||||
end
|
||||
|
||||
defp maybe_put_mikrotik_job(jobs, _device), do: jobs
|
||||
|
||||
defp mikrotik_conditional_job(device) do
|
||||
do_mikrotik_job(needs_discovery?(device), mikrotik_device?(device), device)
|
||||
end
|
||||
|
||||
defp do_mikrotik_job(true, true, device), do: build_mikrotik_job(device)
|
||||
defp do_mikrotik_job(_, _, _device), do: nil
|
||||
|
||||
defp maybe_put_ping_job(jobs, %{monitoring_enabled: true} = device) do
|
||||
[build_ping_job(device) | jobs]
|
||||
end
|
||||
|
||||
defp maybe_put_ping_job(jobs, _device), do: jobs
|
||||
|
||||
@doc """
|
||||
Determines if a device needs SNMP discovery.
|
||||
Returns `true` if no SNMP device, no previous discovery, or last discovery >24h ago.
|
||||
"""
|
||||
@spec needs_discovery?(map()) :: boolean()
|
||||
def needs_discovery?(device) do
|
||||
is_nil(device.snmp_device) or
|
||||
is_nil(device.last_discovery_at) or
|
||||
max(DateTime.diff(DateTime.utc_now(), device.last_discovery_at, :hour), 0) > 24
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a device is a MikroTik device based on SNMP discovery data.
|
||||
"""
|
||||
@spec mikrotik_device?(map()) :: boolean()
|
||||
def mikrotik_device?(device) do
|
||||
device.snmp_device &&
|
||||
(String.contains?(device.snmp_device.manufacturer || "", "MikroTik") ||
|
||||
String.contains?(device.snmp_device.sys_descr || "", "RouterOS"))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolves SNMP credentials for a device.
|
||||
|
||||
For SNMPv3, delegates to `Devices.get_snmpv3_config/1` for credential cascade.
|
||||
For v1/v2c, returns community string and version.
|
||||
"""
|
||||
@spec resolve_snmp_credentials(map()) :: map()
|
||||
def resolve_snmp_credentials(device) do
|
||||
do_resolve_snmp_credentials(device.snmp_version, device)
|
||||
end
|
||||
|
||||
defp do_resolve_snmp_credentials("3", device), do: Devices.get_snmpv3_config(device)
|
||||
|
||||
defp do_resolve_snmp_credentials(_version, device) do
|
||||
%{
|
||||
community: Devices.resolve_snmp_community(device),
|
||||
version: device.snmp_version
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if SNMP credentials are present (not nil/empty).
|
||||
"""
|
||||
@spec credentials_present?(map()) :: boolean()
|
||||
def credentials_present?(%{community: community}) when is_binary(community), do: community != ""
|
||||
def credentials_present?(%{username: username}) when is_binary(username), do: username != ""
|
||||
def credentials_present?(_), do: false
|
||||
|
||||
@doc """
|
||||
Builds an `SnmpDevice` protobuf message with appropriate credentials for the given
|
||||
device and SNMP config.
|
||||
"""
|
||||
@spec build_snmp_device_message(map(), map()) :: SnmpDevice.t()
|
||||
def build_snmp_device_message(device, snmp_config) do
|
||||
do_build_snmp_device_message(device.snmp_version, device, snmp_config)
|
||||
end
|
||||
|
||||
defp do_build_snmp_device_message("3", device, snmp_config) do
|
||||
log_v3_device_build(device, snmp_config)
|
||||
|
||||
%SnmpDevice{
|
||||
ip: device.ip_address,
|
||||
version: device.snmp_version,
|
||||
port: device.snmp_port || 161,
|
||||
community: "",
|
||||
v3_security_level: snmp_config.security_level || "",
|
||||
v3_username: snmp_config.username || "",
|
||||
v3_auth_protocol: snmp_config.auth_protocol || "",
|
||||
v3_auth_password: snmp_config.auth_password || "",
|
||||
v3_priv_protocol: snmp_config.priv_protocol || "",
|
||||
v3_priv_password: snmp_config.priv_password || ""
|
||||
}
|
||||
end
|
||||
|
||||
defp do_build_snmp_device_message(_version, device, snmp_config) do
|
||||
community = snmp_config.community || ""
|
||||
version = effective_snmp_version(device.snmp_version)
|
||||
|
||||
%SnmpDevice{
|
||||
ip: device.ip_address,
|
||||
version: version,
|
||||
port: device.snmp_port || 161,
|
||||
community: community
|
||||
}
|
||||
end
|
||||
|
||||
defp effective_snmp_version("1"), do: "1"
|
||||
defp effective_snmp_version(_), do: "2c"
|
||||
|
||||
defp log_v3_device_build(device, snmp_config) do
|
||||
auth_password_present =
|
||||
is_binary(snmp_config.auth_password) and snmp_config.auth_password != ""
|
||||
|
||||
priv_password_present =
|
||||
is_binary(snmp_config.priv_password) and snmp_config.priv_password != ""
|
||||
|
||||
Logger.info(
|
||||
"Building SNMPv3 device message",
|
||||
device_id: device.id,
|
||||
device_name: device.name,
|
||||
device_ip: device.ip_address,
|
||||
security_level: snmp_config.security_level || "",
|
||||
username: snmp_config.username || "",
|
||||
auth_protocol: snmp_config.auth_protocol || "",
|
||||
auth_password_present: auth_password_present,
|
||||
priv_protocol: snmp_config.priv_protocol || "",
|
||||
priv_password_present: priv_password_present
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a discovery job for a device.
|
||||
"""
|
||||
@spec build_discovery_job(map()) :: AgentJob.t()
|
||||
def build_discovery_job(device) do
|
||||
snmp_credentials = resolve_snmp_credentials(device)
|
||||
|
||||
%AgentJob{
|
||||
job_id: "discover:#{device.id}",
|
||||
job_type: :DISCOVER,
|
||||
device_id: device.id,
|
||||
snmp_device: build_snmp_device_message(device, snmp_credentials),
|
||||
queries: build_discovery_queries()
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a polling job for a device.
|
||||
"""
|
||||
@spec build_polling_job(map()) :: AgentJob.t()
|
||||
def build_polling_job(device) do
|
||||
snmp_credentials = resolve_snmp_credentials(device)
|
||||
|
||||
%AgentJob{
|
||||
job_id: "poll:#{device.id}",
|
||||
job_type: :POLL,
|
||||
device_id: device.id,
|
||||
snmp_device: build_snmp_device_message(device, snmp_credentials),
|
||||
queries: build_polling_queries(device)
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a ping job for a device.
|
||||
"""
|
||||
@spec build_ping_job(map()) :: AgentJob.t()
|
||||
def build_ping_job(device) do
|
||||
%AgentJob{
|
||||
job_id: "ping:#{device.id}",
|
||||
job_type: :PING,
|
||||
device_id: device.id,
|
||||
snmp_device: %SnmpDevice{
|
||||
ip: to_string(device.ip_address),
|
||||
port: 0,
|
||||
version: "",
|
||||
community: ""
|
||||
},
|
||||
queries: []
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a live polling job with only requested sensor OIDs.
|
||||
"""
|
||||
@spec build_live_polling_job(map(), [String.t()], String.t()) :: AgentJob.t()
|
||||
def build_live_polling_job(device, sensor_oids, reply_topic) do
|
||||
snmp_credentials = resolve_snmp_credentials(device)
|
||||
|
||||
%AgentJob{
|
||||
job_id: "live_poll:#{device.id}:#{reply_topic}",
|
||||
job_type: :POLL,
|
||||
device_id: device.id,
|
||||
snmp_device: build_snmp_device_message(device, snmp_credentials),
|
||||
queries: [
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: sensor_oids
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds the list of SNMP queries for device discovery.
|
||||
"""
|
||||
@spec build_discovery_queries() :: [SnmpQuery.t()]
|
||||
def build_discovery_queries do
|
||||
[
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.1.1.0",
|
||||
"1.3.6.1.2.1.1.2.0",
|
||||
"1.3.6.1.2.1.1.3.0",
|
||||
"1.3.6.1.2.1.1.4.0",
|
||||
"1.3.6.1.2.1.1.5.0",
|
||||
"1.3.6.1.2.1.1.6.0"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{query_type: :WALK, oids: ["1.3.6.1.2.1.2.2.1"]},
|
||||
%SnmpQuery{query_type: :WALK, oids: ["1.3.6.1.2.1.31.1.1.1"]},
|
||||
%SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.99.1.1.1",
|
||||
"1.3.6.1.2.1.47.1.1.1.1.2",
|
||||
"1.3.6.1.2.1.47.1.1.1.1.7",
|
||||
"1.3.6.1.2.1.47.1.1.1.1.5"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{query_type: :WALK, oids: ["1.3.6.1.2.1.131.1.1.1.1"]},
|
||||
%SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.25.3.3",
|
||||
"1.3.6.1.2.1.25.2.3",
|
||||
"1.3.6.1.2.1.25.3.2"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: [
|
||||
"1.3.6.1.4.1.2021.11.9.0",
|
||||
"1.3.6.1.4.1.2021.11.10.0",
|
||||
"1.3.6.1.4.1.2021.11.11.0"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.4.1.9.9.109",
|
||||
"1.3.6.1.4.1.9.9.13",
|
||||
"1.3.6.1.4.1.9.9.23",
|
||||
"1.3.6.1.4.1.9.9.618",
|
||||
"1.3.6.1.4.1.14988",
|
||||
"1.3.6.1.4.1.41112",
|
||||
"1.3.6.1.4.1.2636",
|
||||
"1.3.6.1.4.1.25506",
|
||||
"1.3.6.1.4.1.12356",
|
||||
"1.3.6.1.4.1.17713"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.0.8802.1.1.2.1.4.1.1",
|
||||
"1.3.6.1.4.1.9.9.23"
|
||||
]
|
||||
},
|
||||
%SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.4.20",
|
||||
"1.3.6.1.2.1.4.34"
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds the list of SNMP queries for regular polling of a device.
|
||||
"""
|
||||
@spec build_polling_queries(map()) :: [SnmpQuery.t()]
|
||||
def build_polling_queries(device) do
|
||||
sensor_oids = Enum.map(device.snmp_device.sensors, & &1.sensor_oid)
|
||||
|
||||
interface_oids =
|
||||
Enum.flat_map(device.snmp_device.interfaces, fn iface ->
|
||||
idx = iface.if_index
|
||||
|
||||
[
|
||||
"1.3.6.1.2.1.31.1.1.1.6.#{idx}",
|
||||
"1.3.6.1.2.1.31.1.1.1.10.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.10.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.16.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.14.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.20.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.13.#{idx}",
|
||||
"1.3.6.1.2.1.2.2.1.19.#{idx}"
|
||||
]
|
||||
end)
|
||||
|
||||
base_queries = [
|
||||
%SnmpQuery{
|
||||
query_type: :GET,
|
||||
oids: sensor_oids ++ interface_oids
|
||||
}
|
||||
]
|
||||
|
||||
neighbor_query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.0.8802.1.1.2.1.4.1.1",
|
||||
"1.3.6.1.4.1.9.9.23"
|
||||
]
|
||||
}
|
||||
|
||||
arp_query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.4.22",
|
||||
"1.3.6.1.2.1.4.35"
|
||||
]
|
||||
}
|
||||
|
||||
mac_query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.17.4.3"
|
||||
]
|
||||
}
|
||||
|
||||
ip_query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.4.20",
|
||||
"1.3.6.1.2.1.4.34"
|
||||
]
|
||||
}
|
||||
|
||||
host_resources_query = %SnmpQuery{
|
||||
query_type: :WALK,
|
||||
oids: [
|
||||
"1.3.6.1.2.1.25.3.3",
|
||||
"1.3.6.1.2.1.25.2.3"
|
||||
]
|
||||
}
|
||||
|
||||
base_queries ++ [neighbor_query, arp_query, mac_query, ip_query, host_resources_query]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a MikroTik job for a device (only when enabled and has credentials).
|
||||
Returns `nil` if MikroTik is not configured.
|
||||
"""
|
||||
@spec build_mikrotik_job(map()) :: AgentJob.t() | nil
|
||||
def build_mikrotik_job(device) do
|
||||
do_build_mikrotik_job(Devices.get_mikrotik_config(device), device)
|
||||
end
|
||||
|
||||
defp do_build_mikrotik_job(%{enabled: true, username: username}, device) when is_binary(username) do
|
||||
config = Devices.get_mikrotik_config(device)
|
||||
|
||||
%AgentJob{
|
||||
job_id: "mikrotik:#{device.id}",
|
||||
job_type: :MIKROTIK,
|
||||
device_id: device.id,
|
||||
mikrotik_device: %MikrotikDevice{
|
||||
ip: device.ip_address,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
port: config.port,
|
||||
use_ssl: config.use_ssl
|
||||
},
|
||||
mikrotik_commands: build_mikrotik_commands()
|
||||
}
|
||||
end
|
||||
|
||||
defp do_build_mikrotik_job(_config, _device), do: nil
|
||||
|
||||
@doc """
|
||||
Builds the list of MikroTik commands for device interrogation.
|
||||
"""
|
||||
@spec build_mikrotik_commands() :: [MikrotikCommand.t()]
|
||||
def build_mikrotik_commands do
|
||||
[
|
||||
%MikrotikCommand{
|
||||
command: "/system/identity/print",
|
||||
args: %{}
|
||||
},
|
||||
%MikrotikCommand{
|
||||
command: "/system/resource/print",
|
||||
args: %{}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a MikroTik backup job for a device.
|
||||
"""
|
||||
@spec build_backup_job(map(), String.t()) :: AgentJob.t()
|
||||
def build_backup_job(device, job_id) do
|
||||
mikrotik_config = Devices.get_mikrotik_config(device)
|
||||
backup_filename = "towerops-backup-#{DateTime.to_unix(DateTime.utc_now())}"
|
||||
chunk_size = 32_768
|
||||
num_chunks = 10
|
||||
|
||||
read_commands =
|
||||
for i <- 0..(num_chunks - 1) do
|
||||
%MikrotikCommand{
|
||||
command: "/file/read",
|
||||
args: %{
|
||||
"file" => "#{backup_filename}.rsc",
|
||||
"offset" => to_string(i * chunk_size),
|
||||
"chunk-size" => to_string(chunk_size)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
%AgentJob{
|
||||
job_id: job_id,
|
||||
job_type: :MIKROTIK,
|
||||
device_id: device.id,
|
||||
mikrotik_device: %MikrotikDevice{
|
||||
ip: device.ip_address || "",
|
||||
username: mikrotik_config.username || "",
|
||||
password: mikrotik_config.password || "",
|
||||
port: mikrotik_config.port || 8729,
|
||||
ssh_port: mikrotik_config.ssh_port || 22,
|
||||
use_ssl: mikrotik_config.use_ssl || false
|
||||
},
|
||||
mikrotik_commands:
|
||||
[
|
||||
%MikrotikCommand{
|
||||
command: "/export",
|
||||
args: %{"file" => backup_filename, "compact" => "true"}
|
||||
}
|
||||
] ++
|
||||
read_commands ++
|
||||
[
|
||||
%MikrotikCommand{
|
||||
command: "/file/remove",
|
||||
args: %{"numbers" => "#{backup_filename}.rsc"}
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a `CheckProto` protobuf message from a monitoring check.
|
||||
"""
|
||||
@spec build_check_protobuf(map()) :: CheckProto.t()
|
||||
def build_check_protobuf(check) do
|
||||
config = check.config || %{}
|
||||
|
||||
base_fields = [
|
||||
id: check.id,
|
||||
check_type: check.check_type,
|
||||
interval_seconds: check.interval_seconds,
|
||||
timeout_ms: check.timeout_ms
|
||||
]
|
||||
|
||||
struct!(CheckProto, base_fields ++ check_type_config(check.check_type, config))
|
||||
end
|
||||
|
||||
defp check_type_config("http", config) do
|
||||
[
|
||||
http: %HttpCheckConfig{
|
||||
url: config["url"] || "",
|
||||
method: config["method"] || "GET",
|
||||
expected_status: config["expected_status"] || 200,
|
||||
verify_ssl: config["verify_ssl"] != false,
|
||||
regex: config["regex"] || "",
|
||||
follow_redirects: config["follow_redirects"] != false
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp check_type_config("tcp", config) do
|
||||
[
|
||||
tcp: %TcpCheckConfig{
|
||||
host: config["host"] || "",
|
||||
port: config["port"] || 0,
|
||||
send: config["send"] || "",
|
||||
expect: config["expect"] || ""
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp check_type_config("dns", config) do
|
||||
[
|
||||
dns: %DnsCheckConfig{
|
||||
hostname: config["hostname"] || "",
|
||||
server: config["server"] || "",
|
||||
record_type: config["record_type"] || "A",
|
||||
expected: config["expected"] || ""
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp check_type_config("ssl", config) do
|
||||
[
|
||||
ssl: %SslCheckConfig{
|
||||
host: config["host"] || "",
|
||||
port: config["port"] || 443,
|
||||
warning_days: config["warning_days"] || 30
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
defp check_type_config(_, _config), do: []
|
||||
|
||||
@doc """
|
||||
Lists enabled checks for an agent.
|
||||
"""
|
||||
@spec list_checks_for_agent(Ecto.UUID.t()) :: [map()]
|
||||
def list_checks_for_agent(agent_token_id) do
|
||||
Monitoring.list_checks_for_agent(agent_token_id, enabled: true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a `CheckList` protobuf message from a list of checks.
|
||||
"""
|
||||
@spec build_check_list([map()]) :: CheckList.t()
|
||||
def build_check_list(checks) do
|
||||
%CheckList{checks: Enum.map(checks, &build_check_protobuf/1)}
|
||||
end
|
||||
end
|
||||
92
lib/towerops_web/channels/agent_channel/subscriptions.ex
Normal file
92
lib/towerops_web/channels/agent_channel/subscriptions.ex
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
defmodule ToweropsWeb.AgentChannel.Subscriptions do
|
||||
@moduledoc """
|
||||
PubSub subscription management for agent channels.
|
||||
|
||||
Handles all PubSub topic subscriptions and event broadcasting for agent
|
||||
lifecycle events (connection, disconnection, heartbeat).
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Subscribes the current process to all PubSub topics relevant to the given
|
||||
agent token.
|
||||
|
||||
Topics subscribed:
|
||||
- `agent:{id}:assignments` — device assignment changes
|
||||
- `agent:{id}:discovery` — discovery requests
|
||||
- `agent:{id}:backup` — backup requests
|
||||
- `agent:{id}:credential_test` — credential test requests
|
||||
- `agent:{id}:live_poll` — live poll requests
|
||||
- `checks:agent:{id}` — check changes
|
||||
- `agent:{id}:lifecycle` — token lifecycle events
|
||||
- `agent:{id}:latency_probe` — latency probes (cloud pollers only)
|
||||
"""
|
||||
@spec subscribe_all(map()) :: :ok
|
||||
def subscribe_all(agent_token) do
|
||||
id = agent_token.id
|
||||
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:assignments")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:discovery")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:backup")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:credential_test")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:live_poll")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "checks:agent:#{id}")
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{id}:lifecycle")
|
||||
|
||||
subscribe_latency_if_cloud_poller(agent_token)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
# Cloud pollers additionally subscribe to latency probe requests
|
||||
defp subscribe_latency_if_cloud_poller(%{is_cloud_poller: true} = agent_token) do
|
||||
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:latency_probe")
|
||||
:ok
|
||||
end
|
||||
|
||||
defp subscribe_latency_if_cloud_poller(_agent_token), do: :ok
|
||||
|
||||
@doc """
|
||||
Broadcasts an agent connection event to the `agents:health` topic.
|
||||
"""
|
||||
@spec broadcast_connection(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok
|
||||
def broadcast_connection(agent_token_id, organization_id) do
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"agents:health",
|
||||
{:agent_connected, agent_token_id, organization_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts an agent disconnection event to the `agents:health` topic.
|
||||
"""
|
||||
@spec broadcast_disconnection(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok
|
||||
def broadcast_disconnection(agent_token_id, organization_id) do
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"agents:health",
|
||||
{:agent_disconnected, agent_token_id, organization_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Broadcasts an agent heartbeat event to the `agents:health` topic.
|
||||
"""
|
||||
@spec broadcast_heartbeat(Ecto.UUID.t(), Ecto.UUID.t()) :: :ok
|
||||
def broadcast_heartbeat(agent_token_id, organization_id) do
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"agents:health",
|
||||
{:agent_heartbeat, agent_token_id, organization_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
99
lib/towerops_web/live/help_live/sections/about.ex
Normal file
99
lib/towerops_web/live/help_live/sections/about.ex
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.About do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">About Towerops</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Towerops is a modern network monitoring and management platform designed to provide
|
||||
comprehensive visibility into your network infrastructure. It came out of a need to simplify
|
||||
network monitoring and alerting while running a wireless ISP.
|
||||
</p>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Our promise to you:
|
||||
<ul class="list-disc list-inside">
|
||||
<li>No random marketing emails or sign up for our newsletter popups.</li>
|
||||
<li>No trackers EVER, no external libraries used.</li>
|
||||
<li>No ads or sponsored content.</li>
|
||||
<li>No data collection or sharing.</li>
|
||||
<li>
|
||||
Our remote agent is
|
||||
<a href="https://codeberg.org/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.
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-4">
|
||||
Think of Towerops as combining the best of LibreNMS, Icinga2/Nagios, and PagerDuty into
|
||||
a single, unified platform. You get deep network device monitoring with SNMP auto-discovery,
|
||||
flexible health checks and alerting, and intelligent incident management—all with a modern
|
||||
interface that makes complex monitoring workflows simple and accessible.
|
||||
</p>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-4">
|
||||
Towerops is not and has no plans to be a replacement for a full WISP/network billing system.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Network Monitoring
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Like LibreNMS, discover and monitor network devices via SNMP with support for
|
||||
multi-vendor equipment, interface statistics, and topology mapping.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Health Checks & Alerting
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Like Icinga2/Nagios, configure flexible monitoring checks with customizable thresholds
|
||||
and alert conditions for any metric or device state.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Incident Management
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Like PagerDuty, manage incidents with intelligent alerting, escalation policies,
|
||||
and notification routing to keep your team informed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Ready to get started? Check out the
|
||||
<.link
|
||||
patch={~p"/help?section=getting-started"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200"
|
||||
>
|
||||
Getting Started
|
||||
</.link>
|
||||
guide to begin monitoring your network infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
227
lib/towerops_web/live/help_live/sections/agents.ex
Normal file
227
lib/towerops_web/live/help_live/sections/agents.ex
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Agents do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Remote Pollers</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Remote pollers are lightweight Docker containers that run on your network to monitor devices
|
||||
that aren't accessible from the public internet — behind firewalls, on private networks, or
|
||||
at remote locations. They use minimal resources (1 CPU core, 512 MB RAM) and auto-update
|
||||
themselves via Watchtower. Just deploy a
|
||||
<.code>docker-compose.yml</.code>
|
||||
and you're done.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Requirements</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Remote pollers can run on any system that supports Docker Compose, including:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside">
|
||||
<li>Linux servers (Ubuntu, Debian, CentOS, RHEL, etc.)</li>
|
||||
<li>Windows Server with Docker Desktop</li>
|
||||
<li>macOS with Docker Desktop</li>
|
||||
<li>Raspberry Pi or other ARM-based systems</li>
|
||||
<li>Virtual machines (VMware, Hyper-V, Proxmox, etc.)</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong class="text-gray-900 dark:text-white">Minimum requirements:</strong>
|
||||
1 CPU core, 512 MB RAM, and network access to your devices
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Getting Started</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Create an Agent Token
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to the
|
||||
<.code>Agents</.code>
|
||||
page in Towerops and create a new agent token. This token authenticates your remote poller with the Towerops platform.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Create a docker-compose.yml
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
When you create a new agent on the
|
||||
<.code>Agents</.code>
|
||||
page, Towerops will show you a ready-to-use
|
||||
<.code>docker-compose.yml</.code>
|
||||
with your agent token pre-filled. It looks like this:
|
||||
</p>
|
||||
<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
|
||||
container_name: towerops-agent
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TOWEROPS_API_URL=https://app.towerops.net/
|
||||
- TOWEROPS_AGENT_TOKEN=your-token-here
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
- "com.centurylinklabs.watchtower.scope=towerops"
|
||||
|
||||
watchtower:
|
||||
image: containrrr/watchtower:latest
|
||||
container_name: towerops-watchtower
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- WATCHTOWER_POLL_INTERVAL=43200
|
||||
- WATCHTOWER_LABEL_ENABLE=true
|
||||
- WATCHTOWER_SCOPE=towerops
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock]) %></code></pre>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-3">
|
||||
The agent is lightweight — it uses minimal CPU and memory. Watchtower is included
|
||||
to automatically update the agent every 12 hours, so you never have to think about it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">Deploy It</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Copy the
|
||||
<.code>docker-compose.yml</.code>
|
||||
to your server and run:
|
||||
</p>
|
||||
<div class="mt-3 p-4 bg-black rounded-lg not-prose">
|
||||
<pre class="text-sm text-white font-mono"><code>docker compose up -d</code></pre>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-3">
|
||||
That's it. The agent will connect to Towerops within seconds and start polling your local devices.
|
||||
You'll see it come online on the Agents page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Assign Devices to Remote Poller
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
You can assign devices to your remote poller in several ways:
|
||||
</p>
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-2">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Per Organization:</strong>
|
||||
Set a default agent for all devices in your organization
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Per Site:</strong>
|
||||
Set a default agent for all devices at a specific site
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Per Device:</strong>
|
||||
Select the remote poller when creating or editing individual devices
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-3">
|
||||
Devices assigned to a remote poller will be monitored from your local network instead
|
||||
of from the cloud.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-shield-check"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Security Note
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Remote pollers use secure WebSocket connections to communicate with Towerops.
|
||||
Your agent token is encrypted and should be kept confidential. The remote poller
|
||||
only needs outbound HTTPS access (port 443) to connect to Towerops - no inbound
|
||||
ports need to be opened on your firewall.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Managing Remote Pollers
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
You can view the status of your remote pollers on the Agents page. The page shows:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>Last connection time</li>
|
||||
<li>Number of devices assigned to each poller</li>
|
||||
<li>Connection status (online/offline)</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-yellow-900 dark:text-yellow-300 mb-1">
|
||||
Important
|
||||
</h4>
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-300">
|
||||
If a remote poller goes offline, devices assigned to it will not be monitored until
|
||||
the poller comes back online. Consider setting up monitoring alerts for your remote
|
||||
pollers to be notified if they disconnect.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
59
lib/towerops_web/live/help_live/sections/api_tokens.ex
Normal file
59
lib/towerops_web/live/help_live/sections/api_tokens.ex
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.ApiTokens do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">API Tokens</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
API tokens allow external applications and scripts to authenticate with the Towerops REST and GraphQL APIs. Each token is scoped to an organization and can be managed from your user settings.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Creating a Token</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>Go to <strong>User Settings → API Tokens</strong></li>
|
||||
<li>Click <strong>"Create API Token"</strong></li>
|
||||
<li>Give the token a descriptive name (e.g., "Grafana Integration")</li>
|
||||
<li>Copy the token immediately — it will only be shown once</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Using a Token</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Include the token in the
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
Authorization
|
||||
</code>
|
||||
header:
|
||||
</p>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code>Authorization: Bearer your-api-token-here</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Security Best Practices
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
Create separate tokens for each integration — don't reuse tokens across services
|
||||
</li>
|
||||
<li>Revoke tokens you no longer need</li>
|
||||
<li>Never commit tokens to version control</li>
|
||||
<li>Store tokens in environment variables or a secrets manager</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>See also:</strong>
|
||||
<.link navigate={~p"/help?section=rest-api"} class="underline">REST API</.link>
|
||||
and
|
||||
<.link navigate={~p"/help?section=graphql-api"} class="underline">
|
||||
GraphQL API
|
||||
</.link>
|
||||
for endpoint documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
79
lib/towerops_web/live/help_live/sections/cloud_pollers.ex
Normal file
79
lib/towerops_web/live/help_live/sections/cloud_pollers.ex
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.CloudPollers do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Cloud Pollers</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
If you have publicly reachable devices, they will be automatically polled from one of our cloud pollers unless overridden by your settings.
|
||||
Our cloud infrastructure monitors your devices without requiring any additional setup or agent installation.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Active Cloud Pollers
|
||||
</h3>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800/75">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Location
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
IPv4 Address
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
IPv6 Address
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
|
||||
DFW (Dallas-Fort Worth)
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-600 dark:text-gray-400">
|
||||
144.202.64.79
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-600 dark:text-gray-400">
|
||||
2001:19f0:6401:0af2:5400:05ff:fee7:1bfb
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Firewall Configuration
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
If your devices are behind a firewall, make sure to allow SNMP traffic (UDP port 161) and ICMP (for ping monitoring) from these IP addresses.
|
||||
For devices not accessible from the public internet, consider using a
|
||||
<.link
|
||||
patch={~p"/help?section=agents"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200"
|
||||
>
|
||||
Remote Poller
|
||||
</.link>
|
||||
instead.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
127
lib/towerops_web/live/help_live/sections/getting_started.ex
Normal file
127
lib/towerops_web/live/help_live/sections/getting_started.ex
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.GettingStarted do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Getting Started with Towerops
|
||||
</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Towerops is a network monitoring platform that helps you keep track of your network devices,
|
||||
monitor their health, and get alerts when issues occur.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Quick Start Guide
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Create a Site (Optional)
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Sites represent physical or logical locations where your network devices are located.
|
||||
If you manage multiple locations, start by creating a site for your office, data center,
|
||||
or any location where you have equipment.
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mt-2">
|
||||
Navigate to
|
||||
<.code>Sites</.code>
|
||||
→
|
||||
<.code>Add Site</.code>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
||||
Sites are disabled by default and can be enabled later in
|
||||
<.link
|
||||
patch={~p"/help?section=settings"}
|
||||
class="underline hover:text-gray-700 dark:hover:text-gray-300"
|
||||
>
|
||||
Organization Settings
|
||||
</.link>
|
||||
→ General tab.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Add Your First Device
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Add network devices like routers, switches, access points, or servers. You'll need the device's
|
||||
IP address or hostname, and SNMP community string (for SNMP-enabled devices).
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mt-2">
|
||||
Navigate to
|
||||
<.code>Devices</.code>
|
||||
→
|
||||
<.code>Add Device</.code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
View Device Metrics
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Click on any device to view real-time metrics including CPU usage, memory, temperature,
|
||||
interface traffic, and more. Explore the different tabs to see network topology, port status,
|
||||
and historical performance data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-light-bulb"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">Pro Tip</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
For devices behind firewalls or in remote locations, you can deploy a remote poller
|
||||
to monitor devices locally without exposing them to the internet. See the
|
||||
<.link
|
||||
patch={~p"/help?section=agents"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200"
|
||||
>
|
||||
Remote Pollers
|
||||
</.link>
|
||||
section for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
75
lib/towerops_web/live/help_live/sections/graphql_api.ex
Normal file
75
lib/towerops_web/live/help_live/sections/graphql_api.ex
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.GraphqlApi do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">GraphQL API</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
The GraphQL API lets you query exactly the data you need in a single request. The endpoint is
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
POST /api/graphql
|
||||
</code>
|
||||
and uses the same API token authentication as the REST API.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Why GraphQL?</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>Fetch related data in one query</strong>
|
||||
— Get devices with their sites, alerts, and latest metrics in a single request
|
||||
</li>
|
||||
<li><strong>No over-fetching</strong> — Request only the fields you need</li>
|
||||
<li>
|
||||
<strong>Introspection</strong>
|
||||
— The schema is self-documenting; use any GraphQL client to explore available types and fields
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Quick Example</h3>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw("curl -X POST https://app.towerops.net/api/graphql \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"query\": \"{ devices { id name ipAddress status } }\"}'") %></code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Available Queries</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>devices</strong> — List devices with filtering and nested site/alert data
|
||||
</li>
|
||||
<li>
|
||||
<strong>device(id)</strong>
|
||||
— Single device with full detail including sensors and interfaces
|
||||
</li>
|
||||
<li><strong>sites</strong> — Sites with nested device lists</li>
|
||||
<li>
|
||||
<strong>alerts</strong> — Active and historical alerts with device context
|
||||
</li>
|
||||
<li><strong>agents</strong> — Remote poller agents and their assignments</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Mutations</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>createDevice</strong> / <strong>updateDevice</strong> — Manage devices
|
||||
</li>
|
||||
<li>
|
||||
<strong>acknowledgeAlert</strong>
|
||||
/ <strong>resolveAlert</strong>
|
||||
— Alert lifecycle management
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Full reference:</strong>
|
||||
<a href="/docs/graphql" class="underline font-medium">
|
||||
GraphQL API Documentation →
|
||||
</a>
|
||||
— complete schema reference with query examples and variable usage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
323
lib/towerops_web/live/help_live/sections/graphs.ex
Normal file
323
lib/towerops_web/live/help_live/sections/graphs.ex
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Graphs do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Graphs & Live Polling</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Towerops provides powerful visualization capabilities for all monitored metrics. Every sensor,
|
||||
interface, and metric can be viewed as a time-series graph with multiple time ranges and a
|
||||
real-time live polling mode for instant feedback.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Accessing Graphs
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Graphs are available throughout Towerops wherever metrics are displayed:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Device Overview:</strong>
|
||||
Click any metric tile (CPU, Memory, Temperature, etc.) to view its graph
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Sensors Tab:</strong>
|
||||
Click the graph icon next to any sensor reading
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Interfaces Tab:</strong>
|
||||
Click the graph icon next to any interface to view traffic graphs
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Storage Tab:</strong>
|
||||
Click any storage volume to view usage over time
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">Time Ranges</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
All graphs support multiple time ranges for analyzing trends at different scales:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">1 Hour</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Recent activity with high detail</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">6 Hours</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Half-day trends and patterns</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">12 Hours</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Business day overview</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
24 Hours (Default)
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Full day of activity</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">7 Days</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Weekly trends and patterns</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">30 Days</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Monthly overview and capacity planning
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Live Polling Mode
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Live mode provides real-time sensor monitoring with data updating every second. This is perfect for:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>Testing configuration changes and seeing immediate effects</li>
|
||||
<li>Monitoring system load during maintenance or upgrades</li>
|
||||
<li>Watching temperature changes during thermal testing</li>
|
||||
<li>Observing traffic patterns during load testing</li>
|
||||
<li>Real-time troubleshooting of performance issues</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-signal"
|
||||
class="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-green-900 dark:text-green-300 mb-1">
|
||||
How Live Mode Works
|
||||
</h4>
|
||||
<ul class="text-sm text-green-800 dark:text-green-300 space-y-1 list-disc list-inside">
|
||||
<li>Polls sensors directly via SNMP every 1 second</li>
|
||||
<li>Displays a rolling 5-minute window (300 data points)</li>
|
||||
<li>Updates chart in real-time as new data arrives</li>
|
||||
<li>Automatically stops when you switch to another time range</li>
|
||||
<li>Works with remote pollers - polling happens on the agent</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">Using Live Mode</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Open Any Graph
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to any device and click on a metric graph (CPU, Memory, Temperature, Traffic, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Click the "Live" Button
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The Live button has a distinctive green gradient style and will pulse when active
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Watch Real-Time Updates
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The graph will start updating every second with fresh data. A pulsing green indicator
|
||||
shows that live polling is active.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Switch Back to Historical Data
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Click any other time range button to stop live polling and view historical data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Supported Metrics in Live Mode
|
||||
</h3>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-cpu-chip" class="h-4 w-4 text-blue-500" /> CPU / Processors
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Real-time CPU load and utilization</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-circle-stack" class="h-4 w-4 text-blue-500" /> Memory Usage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">RAM utilization percentage</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-fire" class="h-4 w-4 text-orange-500" /> Temperature
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Device and component temperatures</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-bolt" class="h-4 w-4 text-yellow-500" /> Voltage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Power supply voltages</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-server-stack" class="h-4 w-4 text-purple-500" /> Storage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Disk usage and capacity</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-hashtag" class="h-4 w-4 text-green-500" /> Custom Metrics
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">Sessions, connections, and counts</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Note About Traffic Graphs
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Live mode is currently only available for sensor metrics (CPU, memory, temperature, etc.).
|
||||
Interface traffic graphs use historical data only, as traffic calculations require
|
||||
comparing multiple SNMP polls over time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Tips for Using Graphs
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Multiple Sensors on One Graph
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
When viewing aggregate metrics (like "Temperature" for all sensors), the graph
|
||||
automatically displays all sensors of that type with different colors for easy comparison.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Max and Min Values
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Historical graphs (non-live) display the maximum and minimum values for the selected
|
||||
time range at the bottom of the chart for quick reference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Traffic Graph Direction
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Interface traffic graphs show outbound traffic as positive values (above zero) and
|
||||
inbound traffic as negative values (below zero) for easy visualization of bidirectional flow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Automatic Unit Scaling
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Traffic graphs automatically scale units (bps, Kbps, Mbps, Gbps) based on the data
|
||||
range for optimal readability. Hover over data points to see exact values.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-light-bulb"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-yellow-900 dark:text-yellow-300 mb-1">
|
||||
Performance Tip
|
||||
</h4>
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-300">
|
||||
Live polling makes direct SNMP requests every second. While this provides instant
|
||||
feedback, keeping many live graphs open simultaneously may impact device performance.
|
||||
Use live mode for troubleshooting and testing, then switch back to historical ranges
|
||||
for routine monitoring.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
14
lib/towerops_web/live/help_live/sections/helpers.ex
Normal file
14
lib/towerops_web/live/help_live/sections/helpers.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Helpers do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
def code(assigns) do
|
||||
~H"""
|
||||
<span class="font-mono bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">
|
||||
{render_slot(@inner_block)}
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
end
|
||||
61
lib/towerops_web/live/help_live/sections/insights.ex
Normal file
61
lib/towerops_web/live/help_live/sections/insights.ex
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Insights do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Network Insights</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Network Insights provides proactive observations about your network health, gathered automatically from all connected data sources.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">How It Works</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Towerops continuously analyzes data from SNMP polling, Preseem, Gaiia, and other integrations to surface actionable findings. Insights are categorized by source, urgency, and type so you can focus on what matters.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Insight Types</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>Reconciliation Findings</strong>
|
||||
— Devices that exist in Towerops but not in Gaiia (or vice versa), data mismatches between systems
|
||||
</li>
|
||||
<li>
|
||||
<strong>Performance Anomalies</strong>
|
||||
— Devices with unusual metric patterns detected by Preseem or SNMP baselines
|
||||
</li>
|
||||
<li>
|
||||
<strong>Configuration Drift</strong>
|
||||
— Detected changes to device configurations that may affect performance
|
||||
</li>
|
||||
<li>
|
||||
<strong>Capacity Warnings</strong>
|
||||
— Access points or links approaching utilization thresholds
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Filtering & Management
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Use the filter bar to narrow insights by status (active/dismissed), source (Preseem, Gaiia, SNMP, system), and urgency level. Each insight shows affected devices as clickable links so you can drill directly into the device detail page.
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Dismiss insights you've reviewed to keep the active list clean. Bulk actions let you select multiple insights at once.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Tip:</strong>
|
||||
Insights run on a nightly schedule. Connect your Gaiia and Preseem integrations in
|
||||
<.link navigate={~p"/help?section=integrations"} class="underline">
|
||||
Organization Settings
|
||||
</.link>
|
||||
to get the most comprehensive insights.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
338
lib/towerops_web/live/help_live/sections/integrations.ex
Normal file
338
lib/towerops_web/live/help_live/sections/integrations.ex
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Integrations do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Integrations</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Integrations allow you to connect Towerops with third-party services to enrich your
|
||||
monitoring data, sync subscriber information, and streamline your workflow. Configure
|
||||
integrations from
|
||||
<.code>Organization Settings</.code>
|
||||
→
|
||||
<.code>Integrations</.code>
|
||||
tab.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Preseem</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
href="https://preseem.com"
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
Preseem
|
||||
</a>
|
||||
is a Quality of Experience (QoE) monitoring platform designed for WISPs and broadband providers.
|
||||
The Preseem integration syncs subscriber and access point QoE data into Towerops, giving you
|
||||
a unified view of network health alongside device monitoring.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">What It Syncs</h4>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside">
|
||||
<li>Subscriber QoE scores and metrics</li>
|
||||
<li>Access point performance data</li>
|
||||
<li>Latency, jitter, and packet loss statistics</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">Configuration</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Get Your Preseem API Key
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Log in to your Preseem dashboard and generate an API key from your account settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Add the Integration
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Organization Settings</.code>
|
||||
→
|
||||
<.code>Integrations</.code>
|
||||
tab.
|
||||
Enter your Preseem API key and configure the sync interval.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Test the Connection
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Use the <strong class="text-gray-900 dark:text-white">Test Connection</strong> button
|
||||
to verify your API key is valid and Towerops can reach the Preseem API.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Sync Schedule
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Preseem data is synced automatically on a periodic schedule based on your configured
|
||||
sync interval. You can also trigger a manual sync at any time from the integration settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">Gaiia</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
href="https://gaiia.com"
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
Gaiia
|
||||
</a>
|
||||
is a billing and subscriber management platform. The Gaiia integration syncs subscriber data,
|
||||
service plans, and entity mappings into Towerops, enabling you to correlate network issues with
|
||||
specific customers and services.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">What It Syncs</h4>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside">
|
||||
<li>Subscriber accounts and contact information</li>
|
||||
<li>Service plans and subscription status</li>
|
||||
<li>Entity mappings between Gaiia and Towerops devices</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">Configuration</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Configure Gaiia Credentials
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Organization Settings</.code>
|
||||
→
|
||||
<.code>Integrations</.code>
|
||||
tab
|
||||
and enter your Gaiia API credentials.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Set Up Webhooks
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Configure a webhook in your Gaiia account pointing to the webhook URL provided
|
||||
in the integration settings. Webhooks enable real-time updates when subscriber
|
||||
data changes in Gaiia.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Entity Mapping & Reconciliation
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
After the initial sync, review the entity mapping to ensure Gaiia subscribers are
|
||||
correctly matched to Towerops devices. Use the reconciliation tools to resolve
|
||||
any mismatches.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Sync Schedule
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Gaiia data is synced through two mechanisms: automatic periodic syncs pull the full
|
||||
dataset on a schedule, while real-time webhooks push individual changes as they happen
|
||||
in Gaiia. This ensures your data stays current without excessive API calls.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">PagerDuty</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
<a
|
||||
href="https://www.pagerduty.com"
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
PagerDuty
|
||||
</a>
|
||||
is an incident management and on-call alerting platform. The PagerDuty integration provides
|
||||
2-way alert sync — when a device goes down in Towerops, a PagerDuty incident is automatically
|
||||
triggered. Acknowledging or resolving the alert in Towerops updates the PagerDuty incident as well.
|
||||
</p>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">How It Works</h4>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside">
|
||||
<li>Device goes down → PagerDuty incident triggered (critical severity)</li>
|
||||
<li>Alert acknowledged in Towerops → PagerDuty incident acknowledged</li>
|
||||
<li>Device recovers / alert resolved → PagerDuty incident resolved</li>
|
||||
</ul>
|
||||
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mt-4 mb-2">Configuration</h4>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Create an Events API v2 Integration in PagerDuty
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
In PagerDuty, go to
|
||||
<.code>Services</.code>
|
||||
→ select your service →
|
||||
<.code>Integrations</.code>
|
||||
tab →
|
||||
<.code>Add Integration</.code>
|
||||
→ choose <.code>Events API v2</.code>. Copy the
|
||||
<.code>Integration Key</.code>
|
||||
(routing key).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Configure in Towerops
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Organization Settings</.code>
|
||||
→
|
||||
<.code>Integrations</.code>
|
||||
tab → click
|
||||
<.code>Configure</.code>
|
||||
on PagerDuty. Paste your integration key and test the connection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Event-Driven
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Unlike other integrations, PagerDuty is event-driven — there is no periodic sync.
|
||||
Alerts are sent to PagerDuty in real time as they occur. No polling or sync interval is needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
General Integration Features
|
||||
</h3>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Test Connection</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Every integration includes a Test Connection button that validates your credentials
|
||||
and confirms Towerops can communicate with the external service before saving.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Sync Status</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
The Integrations tab shows the last sync time, sync status, and any errors for
|
||||
each configured integration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
334
lib/towerops_web/live/help_live/sections/mikrotik.ex
Normal file
334
lib/towerops_web/live/help_live/sections/mikrotik.ex
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Mikrotik do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">MikroTik Configuration</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Towerops supports read-only monitoring of MikroTik RouterOS devices via SSH.
|
||||
In addition to SNMP polling, SSH access allows Towerops to retrieve detailed system information
|
||||
and create configuration backups of your MikroTik devices for disaster recovery and audit purposes.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Read-Only Access
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Towerops currently operates in read-only mode for MikroTik devices. Configuration changes,
|
||||
reboots, and other administrative actions are not supported. This ensures your device
|
||||
configurations remain unchanged by monitoring operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-server"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Cloud or Agent-Based Connections
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Both SNMP polling and SSH connections to MikroTik devices can be performed from either
|
||||
Towerops cloud infrastructure or your remote poller (agent). For publicly accessible devices,
|
||||
cloud polling is the simplest option. For devices on private networks or when you prefer
|
||||
to keep SSH credentials on your local network, deploy a remote poller. The device assignment
|
||||
determines which poller handles both SNMP and SSH operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Security Best Practices
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
For security purposes, it is strongly recommended to create a dedicated read-only user account
|
||||
for Towerops rather than using an administrator account. This follows the principle of least
|
||||
privilege and minimizes potential security risks. Even though Towerops only performs read-only
|
||||
operations, limiting the account permissions provides defense in depth.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Creating a Read-Only User
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Connect to your MikroTik device via SSH or terminal and execute the following commands to
|
||||
create a read-only user named <.code>towerops</.code>:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2 font-medium">
|
||||
Step 1: Create a new user group with read-only permissions
|
||||
</p>
|
||||
<div class="p-4 bg-black rounded-lg not-prose">
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="text-sm text-white font-mono break-all flex-1">
|
||||
/user group add name=readonly policy=ssh,read,test,api
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={JS.dispatch("phx:copy", to: "#step1-command")}
|
||||
class="flex-shrink-0 p-1 text-blue-400 hover:text-blue-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<.icon name="hero-clipboard-document" class="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type="hidden"
|
||||
id="step1-command"
|
||||
value="/user group add name=readonly policy=ssh,read,test,api"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2 font-medium">
|
||||
Step 2: Create the monitoring user with a strong password
|
||||
</p>
|
||||
<div class="p-4 bg-black rounded-lg not-prose">
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="text-sm text-white font-mono break-all flex-1">
|
||||
/user add name=towerops password={if @generated_password,
|
||||
do: @generated_password,
|
||||
else: "YOUR_STRONG_PASSWORD"} group=readonly
|
||||
</code>
|
||||
<%= if @generated_password do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={JS.dispatch("phx:copy", to: "#generated-password-value")}
|
||||
class="flex-shrink-0 p-1 text-blue-400 hover:text-blue-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<.icon name="hero-clipboard-document" class="h-4 w-4" />
|
||||
</button>
|
||||
<input type="hidden" id="generated-password-value" value={@generated_password} />
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex items-start gap-3">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="generate_password"
|
||||
disabled={@password_generating}
|
||||
>
|
||||
<%= if @password_generating do %>
|
||||
<.icon name="hero-arrow-path" class="h-4 w-4 mr-1 animate-spin" /> Generating...
|
||||
<% else %>
|
||||
<.icon name="hero-sparkles" class="h-4 w-4 mr-1" /> Regenerate Random Password
|
||||
<% end %>
|
||||
</.button>
|
||||
<%= if @generated_password do %>
|
||||
<p class="text-xs text-yellow-600 dark:text-yellow-400 font-medium mt-1.5">
|
||||
⚠️ This truly random password from random.org will only be shown once!
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2 font-medium">
|
||||
Step 3: Verify the user was created successfully
|
||||
</p>
|
||||
<div class="p-4 bg-black rounded-lg not-prose">
|
||||
<div class="flex items-start gap-2">
|
||||
<code class="text-sm text-white font-mono break-all flex-1">
|
||||
/user print detail where name=towerops
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={JS.dispatch("phx:copy", to: "#step3-command")}
|
||||
class="flex-shrink-0 p-1 text-blue-400 hover:text-blue-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<.icon name="hero-clipboard-document" class="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
type="hidden"
|
||||
id="step3-command"
|
||||
value="/user print detail where name=towerops"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Permissions Explained
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The
|
||||
<.code>readonly</.code>
|
||||
group includes the following permissions. Note that while these
|
||||
permissions are granted to the user account, Towerops only performs read operations and does
|
||||
not make any configuration changes:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
|
||||
<thead class="bg-gray-50 dark:bg-gray-800/75">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Permission
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
|
||||
ssh
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Allow SSH access to the device
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
|
||||
read
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Allow viewing configuration and status (read-only)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
|
||||
test
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Allow executing diagnostic commands (ping, traceroute, etc.)
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-900 dark:text-white">
|
||||
api
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Allow API access for automated monitoring
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Important Notes
|
||||
</h4>
|
||||
<ul class="text-sm text-blue-800 dark:text-blue-300 space-y-1 list-disc list-inside">
|
||||
<li>The read-only user cannot modify device configuration</li>
|
||||
<li>No write, reboot, or sensitive permissions are granted</li>
|
||||
<li>Use a strong, unique password for the monitoring account</li>
|
||||
<li>Consider restricting SSH access by source IP if possible</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Configuring in Towerops
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
After creating the read-only user, configure SSH credentials in Towerops:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Organization-Level Configuration
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Organization Settings</.code>
|
||||
→
|
||||
<.code>MikroTik tab</.code>
|
||||
to set default
|
||||
SSH credentials for all MikroTik devices in your organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Site-Level Configuration
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Sites</.code>
|
||||
→ select a site →
|
||||
<.code>Edit</.code>
|
||||
to override
|
||||
credentials for all devices at a specific location.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Device-Level Configuration
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
When editing a MikroTik device, you can specify unique SSH credentials that override
|
||||
organization and site defaults.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-shield-check"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-yellow-900 dark:text-yellow-300 mb-1">
|
||||
Security Recommendations
|
||||
</h4>
|
||||
<ul class="text-sm text-yellow-800 dark:text-yellow-300 space-y-1 list-disc list-inside">
|
||||
<li>Always use SSL/TLS for SSH connections (API-SSL on port 8729)</li>
|
||||
<li>Store credentials at the organization or site level when possible</li>
|
||||
<li>Rotate passwords periodically following your security policies</li>
|
||||
<li>Monitor access logs for unauthorized SSH connection attempts</li>
|
||||
<li>
|
||||
Consider using SSH keys instead of passwords (if supported by your setup)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
50
lib/towerops_web/live/help_live/sections/network_map.ex
Normal file
50
lib/towerops_web/live/help_live/sections/network_map.ex
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.NetworkMap do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Network Map</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
The Network Map provides a geographic view of your sites on an interactive map. Sites with latitude and longitude coordinates are displayed as markers that you can click to view details.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Adding Location Data
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Each site can have an address and/or latitude/longitude coordinates. You can set these in two ways:
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>Manual entry</strong> — Enter latitude and longitude directly in the site edit form
|
||||
</li>
|
||||
<li>
|
||||
<strong>Geocoding</strong>
|
||||
— Enter a street address and click "Geocode" to automatically look up the coordinates using Google Maps
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Geocoding Setup</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Address-to-coordinate conversion requires a Google Maps Geocoding API key. Your administrator sets this as the
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
GOOGLE_MAPS_API_KEY
|
||||
</code>
|
||||
environment variable on the server. The map display itself uses OpenStreetMap and requires no API key.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Map Features</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>Interactive pan and zoom with OpenStreetMap tiles</li>
|
||||
<li>
|
||||
Click any site marker to see site name, device count, and a link to the site detail page
|
||||
</li>
|
||||
<li>Auto-fits the map to show all your sites</li>
|
||||
<li>Summary stats showing total sites, mapped sites, and total devices</li>
|
||||
</ul>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
15
lib/towerops_web/live/help_live/sections/not_found.ex
Normal file
15
lib/towerops_web/live/help_live/sections/not_found.ex
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.NotFound do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Section Not Found</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The requested help section could not be found.
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
64
lib/towerops_web/live/help_live/sections/rest_api.ex
Normal file
64
lib/towerops_web/live/help_live/sections/rest_api.ex
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.RestApi do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">REST API</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
The Towerops REST API provides programmatic access to your monitoring data. All endpoints are under
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
/api/v1/
|
||||
</code>
|
||||
and require a valid API token.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Available Endpoints
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>Devices</strong> — List, view, create, and update monitored devices
|
||||
</li>
|
||||
<li>
|
||||
<strong>Alerts</strong> — Query active and historical alerts, acknowledge, and resolve
|
||||
</li>
|
||||
<li><strong>Sites</strong> — Manage sites and their device assignments</li>
|
||||
<li><strong>Agents</strong> — List remote pollers and their status</li>
|
||||
<li>
|
||||
<strong>Check Results</strong> — Query monitoring check results and metrics
|
||||
</li>
|
||||
<li>
|
||||
<strong>Organization Settings</strong> — Read and update SNMP/MikroTik configuration
|
||||
</li>
|
||||
<li><strong>Members & Invitations</strong> — Manage organization membership</li>
|
||||
<li><strong>Integrations</strong> — Configure Preseem, Gaiia, and PagerDuty</li>
|
||||
<li>
|
||||
<strong>Activity Feed</strong> — Recent events and changes across the organization
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Quick Example</h3>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw("curl -H \"Authorization: Bearer YOUR_TOKEN\" \\\n https://app.towerops.net/api/v1/devices") %></code></pre>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
All responses use
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
{raw("{\"data\": ...}")}
|
||||
</code>
|
||||
format. Results are scoped to the organization associated with your API token.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Full reference:</strong>
|
||||
<a href="/docs/api" class="underline font-medium">REST API Documentation →</a>
|
||||
— complete endpoint reference with request/response examples.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
152
lib/towerops_web/live/help_live/sections/settings.ex
Normal file
152
lib/towerops_web/live/help_live/sections/settings.ex
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Settings do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Organization Settings</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Organization Settings is the central hub for configuring your Towerops organization. Access it
|
||||
from the main navigation under <.code>Organization Settings</.code>. The settings page uses a
|
||||
tabbed interface to organize different configuration areas.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">General Tab</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The General tab lets you manage basic organization properties:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Organization Name</strong>
|
||||
— The display name for your organization
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Default Organization</strong>
|
||||
— Set this as the default org when logging in
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Sites Toggle</strong>
|
||||
— Enable or disable site-based device organization (see the
|
||||
<.link
|
||||
patch={~p"/help?section=sites"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
Sites
|
||||
</.link>
|
||||
section for details)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">SNMP Tab</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Configure organization-wide SNMP defaults used for device polling:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">SNMP Version</strong>
|
||||
— Choose between v1, v2c, or v3
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Community String</strong>
|
||||
— The SNMP community string for v1/v2c
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">SNMPv3 Credentials</strong>
|
||||
— Username, auth protocol, auth passphrase, privacy protocol, and privacy passphrase
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Port</strong> — SNMP port (default: 161)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Credential Hierarchy
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
SNMP credentials follow a hierarchy: <strong>Device > Site > Organization</strong>.
|
||||
Device-level settings take the highest priority, followed by site-level, then
|
||||
organization-level defaults. This lets you set sensible defaults while overriding
|
||||
for specific locations or devices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-4">
|
||||
Use the <strong class="text-gray-900 dark:text-white">Force Apply</strong> button to push
|
||||
the organization's SNMP settings to all devices, overriding any device or site-level
|
||||
customizations.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">MikroTik Tab</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Configure organization-wide SSH credentials for MikroTik device monitoring. This tab is
|
||||
only accessible to superusers. See the
|
||||
<.link
|
||||
patch={~p"/help?section=mikrotik"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
MikroTik
|
||||
</.link>
|
||||
section for detailed setup instructions.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Agents Tab</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Manage remote poller (agent) assignments for your organization:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Default Agent</strong>
|
||||
— Set a default remote poller for all devices in the organization
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Assignment Breakdown</strong>
|
||||
— View how devices are distributed across pollers (cloud vs. remote agents)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-4">
|
||||
Use the <strong class="text-gray-900 dark:text-white">Force Apply</strong> button to push
|
||||
the default agent assignment to all devices, overriding any device or site-level agent
|
||||
selections.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Integrations Tab
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Connect Towerops with third-party services. See the
|
||||
<.link
|
||||
patch={~p"/help?section=integrations"}
|
||||
class="underline hover:text-blue-900 dark:hover:text-blue-200 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
Integrations
|
||||
</.link>
|
||||
section for full details on available integrations and how to configure them.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
221
lib/towerops_web/live/help_live/sections/sites.ex
Normal file
221
lib/towerops_web/live/help_live/sections/sites.ex
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sections.Sites do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
import ToweropsWeb.HelpLive.Sections.Helpers
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Sites</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Sites are an optional organizational feature that lets you group devices by physical or logical
|
||||
locations such as offices, datacenters, customer locations, or network zones. Sites are disabled
|
||||
by default to keep things simple for single-location deployments.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
When to Enable Sites
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Consider enabling sites if you:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Manage multiple physical locations</strong>
|
||||
- Offices, datacenters, retail stores, etc.
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Want to group credentials by location</strong>
|
||||
- Different SNMP communities or credentials per site
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Need location-based monitoring</strong>
|
||||
- Assign different remote pollers to different sites
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Organize by customer or department</strong>
|
||||
- Logical groupings for multi-tenant or large networks
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
When to Skip Sites
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
You can skip enabling sites if you:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Have a single location</strong>
|
||||
- All devices in one office or datacenter
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Want a simpler setup</strong>
|
||||
- Devices belong directly to your organization without extra grouping
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Don't need location-based credentials</strong>
|
||||
- Organization-level credentials work for all devices
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">Note</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Sites are optional and disabled by default. You can start without sites and enable them
|
||||
later if your needs change. When sites are disabled, all devices belong directly to
|
||||
your organization.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">How Sites Work</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
When sites are enabled, devices can be organized into sites, and credentials (SNMP communities,
|
||||
usernames, passwords) can be set at three levels:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
1. Organization Level
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Default credentials that apply to all devices across all sites in your organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
2. Site Level (when sites enabled)
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Override organization defaults for all devices at a specific site. Useful when different
|
||||
locations have different network configurations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">3. Device Level</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Override organization or site credentials for a specific device. Useful for devices with
|
||||
unique configurations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">Enabling Sites</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Navigate to Organization Settings
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Go to
|
||||
<.code>Settings</.code>
|
||||
→
|
||||
<.code>Organization</.code>
|
||||
and scroll to the "Site Organization" section.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Enable the "Use Sites" Toggle
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Check the box for "Use sites to organize devices" and save your settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Create Your First Site
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to
|
||||
<.code>Sites</.code>
|
||||
→
|
||||
<.code>Add Site</.code>
|
||||
to create your first site. Give it a descriptive name like "Main Office" or "Dallas Datacenter".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Assign Devices to Sites
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
When creating or editing devices, you can now assign them to specific sites. The site
|
||||
selector will appear in the device form.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-exclamation-triangle"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-yellow-900 dark:text-yellow-300 mb-1">
|
||||
Disabling Sites
|
||||
</h4>
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-300">
|
||||
If you disable sites after enabling them, all devices will be removed from their sites
|
||||
and assigned directly to the organization. Site assignments will be lost, but devices
|
||||
will remain in your organization and continue to be monitored.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
54
lib/towerops_web/live/help_live/sidebar.ex
Normal file
54
lib/towerops_web/live/help_live/sidebar.ex
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
defmodule ToweropsWeb.HelpLive.Sidebar do
|
||||
@moduledoc false
|
||||
use ToweropsWeb, :html
|
||||
|
||||
attr :active_section, :string, required: true
|
||||
|
||||
def sidebar(assigns) do
|
||||
~H"""
|
||||
<div class="lg:col-span-1">
|
||||
<nav class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 sticky top-4">
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
|
||||
Sections
|
||||
</h3>
|
||||
<ul class="space-y-1">
|
||||
<li :for={{section_key, label} <- sections()}>
|
||||
<.link
|
||||
patch={~p"/help?section=#{section_key}"}
|
||||
class={[
|
||||
"block px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
active_class(section_key, @active_section)
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp sections do
|
||||
[
|
||||
{"about", "About Towerops"},
|
||||
{"getting-started", "Getting Started"},
|
||||
{"settings", "Organization Settings"},
|
||||
{"sites", "Sites"},
|
||||
{"cloud-pollers", "Cloud Pollers"},
|
||||
{"agents", "Remote Pollers"},
|
||||
{"integrations", "Integrations"},
|
||||
{"graphs", "Graphs & Live Polling"},
|
||||
{"insights", "Network Insights"},
|
||||
{"network-map", "Network Map"},
|
||||
{"api-tokens", "API Tokens"},
|
||||
{"rest-api", "REST API"},
|
||||
{"graphql-api", "GraphQL API"},
|
||||
{"mikrotik", "MikroTik"}
|
||||
]
|
||||
end
|
||||
|
||||
defp active_class(key, key), do: "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
|
||||
|
||||
defp active_class(_, _), do: "text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
end
|
||||
1072
priv/antennas/catalog.json
Normal file
1072
priv/antennas/catalog.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue