Applies the 11 recommendations from the codebase optimization review. Module decomposition: - Extract Towerops.Topology.InferenceEngine: pulls device-role inference (capability rules, vendor matching, evidence merging) out of the topology context. Topology now delegates merge_confidence/2, infer_role_from_capabilities/1, and infer_device_role/1. - Extract Towerops.Accounts.TOTP: TOTP secret/QR generation, multi-device verification, recovery code lifecycle, and sudo MFA logic. Accounts delegates the public TOTP API for backwards compatibility. - Extract Towerops.Accounts.Sessions: browser session create/list/touch/ revoke/anonymize/expire. Accounts delegates the public session API. - Extract Towerops.Snmp.Queries: time-series read paths (sensor readings, interface stats, latest-per-id batches). - Extract Towerops.Snmp.Monitoring: time-series write paths (sensor/interface/processor reading inserts, batch inserts via Repo.insert_all). Snmp.ex shrinks from 2985 to 2742 lines. DRY refactoring: - Add per-schema Query modules (Devices.DeviceQuery, Alerts.AlertQuery) with composable for_organization/for_site/with_status/etc filters; refactor list_site_devices, count_organization_devices, count_site_devices, count_site_devices_down, count_organization_alerts to compose them. - Introduce create_discovered_checks/6 helper in Snmp; collapses 4 nearly identical Enum.reduce blocks for sensors/interfaces/processors/storage. - Unify MAC and ARP evidence collectors behind collect_lookup_evidence/3 (lldp/cdp were already factored). Pattern matching: - Replace cond block in infer_role_from_capabilities/1 with a declarative @role_rules table consulted via Enum.find_value (Elixir 1.19 forbids `in` with runtime lists in guards, so multi-clause functions weren't an option). - Replace inline `if since` in get_sensor_readings/2 with a maybe_filter_since pipe helper. Performance: - Replace Enum.each + Repo.insert per evidence row in upsert_link with a single Repo.insert_all batch (truncates :observed_at to seconds). Human-centric: - Decompose build_device_lookup/1 into fetch_lookup_devices, map_ips_to_ids, map_names_to_ids, map_macs_to_ids. - Adopt Towerops.Result.map/2 in Snmp.Client.do_get_multiple_sequential (kept narrow; `with` is preferred over Result.and_then for chained operations and is already used widely). CLAUDE.md updates: - Replace stale main→staging / production-branch deployment notes with the current flow: PRs deploy to Dokku staging, push to main runs the ExUnit gate then builds an image; argocd-image-updater rolls production. - Refresh data-model diagram and references from "Equipment" to "Device" (post equipment→devices rename migration). - Correct stale architecture facts: Hammer → in-tree Towerops.RateLimit, Rust NIF → C NIF (libnetsnmp), gitlab-registry → forgejo-registry, expand the Oban queue and cron lists, list actual implemented Ecto types. All 10,228 tests pass.
51 lines
1.5 KiB
Elixir
51 lines
1.5 KiB
Elixir
defmodule Towerops.Devices.DeviceQuery do
|
|
@moduledoc """
|
|
Composable query fragments for `Towerops.Devices.Device`.
|
|
|
|
Lets context modules build queries by piping filters together instead of
|
|
repeating `where: e.organization_id == ^...` clauses inline.
|
|
|
|
## Example
|
|
|
|
DeviceQuery.base()
|
|
|> DeviceQuery.for_organization(org_id)
|
|
|> DeviceQuery.with_status(:down)
|
|
|> Repo.all()
|
|
"""
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Devices.Device
|
|
|
|
@doc "Starting point for a Device query."
|
|
def base, do: Device
|
|
|
|
@doc "Filter to a single organization."
|
|
def for_organization(query \\ base(), organization_id) do
|
|
where(query, [d], d.organization_id == ^organization_id)
|
|
end
|
|
|
|
@doc "Filter to multiple organizations."
|
|
def for_organizations(query \\ base(), organization_ids) when is_list(organization_ids) do
|
|
where(query, [d], d.organization_id in ^organization_ids)
|
|
end
|
|
|
|
@doc "Filter to a single site."
|
|
def for_site(query \\ base(), site_id) do
|
|
where(query, [d], d.site_id == ^site_id)
|
|
end
|
|
|
|
@doc "Filter to multiple sites."
|
|
def for_sites(query \\ base(), site_ids) when is_list(site_ids) do
|
|
where(query, [d], d.site_id in ^site_ids)
|
|
end
|
|
|
|
@doc "Filter by status (e.g. :up, :down, :unknown)."
|
|
def with_status(query \\ base(), status) do
|
|
where(query, [d], d.status == ^status)
|
|
end
|
|
|
|
@doc "Default display ordering: explicit display_order then name."
|
|
def order_by_display(query \\ base()) do
|
|
order_by(query, [d], asc: d.display_order, asc: d.name)
|
|
end
|
|
end
|