Fix/discovery sources empty #2650

Merged
mfreeman451 merged 34 commits from refs/pull/2650/head into staging 2026-01-11 19:21:05 +00:00
mfreeman451 commented 2026-01-11 18:38:26 +00:00 (Migrated from github.com)
Owner

Imported from GitHub pull request.

Original GitHub pull request: #2249
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/pull/2249
Original created: 2026-01-11T18:38:26Z
Original updated: 2026-01-11T19:21:07Z
Original head: carverauto/serviceradar:fix/discovery_sources_empty
Original base: staging
Original merged: 2026-01-11T19:21:05Z by @mfreeman451

User description

IMPORTANT: Please sign the Developer Certificate of Origin

Thank you for your contribution to ServiceRadar. Please note, when contributing, the developer must include
a DCO sign-off statement indicating the DCO acceptance in one commit message. Here
is an example DCO Signed-off-by line in a commit message:

Signed-off-by: J. Doe <j.doe@domain.com>

Describe your changes

Code checklist before requesting a review

  • I have signed the DCO?
  • The build completes without errors?
  • All tests are passing when running make test?

PR Type

Enhancement, Tests


Description

  • Comprehensive database schema migration with 30+ tables for tenant data including ocsf_devices, ocsf_agents, service_checks, polling_schedules, and integration_sources

  • New stateful alert rules engine with bucketed evaluation, state tracking, and fault tolerance for log and event-based alerting

  • Integration sources management LiveView with CRUD operations for Armis, SNMP, Syslog, Nmap, and Custom sources

  • Real-time cluster monitoring view with ERTS topology visibility, gateway/agent registration tracking, and Oban job queue monitoring

  • Infrastructure monitoring view with role-based access control and PubSub-based caching for cluster topology

  • Gateway process GenServer for agent coordination with health checks, job execution, and agent discovery

  • Enhanced agent detail view displaying live registry data, system information, and configured service checks

  • Redesigned job scheduler UI with sortable tables, filtering, pagination, and support for multiple job sources

  • Edge package creation refactored with Ash forms, automatic certificate generation, and tenant CA provisioning

  • Tenant registry test suite with comprehensive coverage for registry operations and process lifecycle management

  • Significant codebase migration from Go to Elixir with removal of legacy poller and discovery components


Diagram Walkthrough

flowchart LR
  DB["Database Schema<br/>30+ Tables"]
  AlertEngine["Stateful Alert<br/>Engine"]
  ClusterMon["Cluster<br/>Monitoring"]
  InfraMon["Infrastructure<br/>View"]
  GatewayProc["Gateway<br/>Process"]
  IntegSources["Integration<br/>Sources UI"]
  
  DB --> AlertEngine
  DB --> IntegSources
  ClusterMon --> InfraMon
  GatewayProc --> ClusterMon
  IntegSources --> GatewayProc
  AlertEngine --> ClusterMon

File Walkthrough

Relevant files
Database migration
1 files
20260107043446_initial_schema.exs
Initial tenant schema migration with comprehensive table definitions

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs

  • Adds comprehensive database migration for initial tenant schema with
    1416 lines of table definitions
  • Creates 30+ tables including ocsf_devices, ocsf_agents,
    service_checks, polling_schedules, integration_sources, and related
    entities
  • Establishes foreign key relationships, indexes, and unique constraints
    across all tables
  • Implements both up and down migration functions for schema creation
    and rollback
+1416/-0
Enhancement
8 files
index.ex
Integration sources LiveView with CRUD and modal management

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex

  • Implements LiveView module for managing integration sources (Armis,
    SNMP, Syslog, Nmap, Custom)
  • Provides create, read, update, delete operations with modal-based UI
    for integration source management
  • Includes query management, network blacklist configuration, and
    credential handling with JSON parsing
  • Implements filtering by source type and enabled status, with
    agent/partition assignment and sync statistics display
+1471/-0
index.ex
New cluster monitoring LiveView with real-time status tracking

web-ng/lib/serviceradar_web_ng_web/live/settings/cluster_live/index.ex

  • New LiveView module for monitoring distributed Horde cluster with
    real-time visibility into ERTS topology, gateways, agents, and Oban
    job queues
  • Implements PubSub subscriptions for cluster events, agent
    registrations, and gateway platform updates with automatic refresh
    scheduling
  • Provides comprehensive data loading and caching mechanisms for
    gateways and agents across cluster nodes using RPC calls
  • Includes helper functions for timestamp parsing, staleness detection,
    tenant filtering, and rendering health metrics and event logs
+1086/-0
index.ex
Refactor edge package creation with Ash forms and auto-cert generation

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex

  • Migrated from Ecto changesets to AshPhoenix.Form for form handling
    with automatic validation and error management
  • Added automatic certificate generation via create_with_tenant_cert
    with tenant CA auto-provisioning
  • Changed component type from "poller" to "gateway" as primary component
    type, added support for "sync" type
  • Enhanced success modal with Docker/systemd install commands,
    certificate details, and improved UX with loading states
  • Added tenant scoping to all package operations and improved form field
    handling with JSON config support
+546/-224
index.ex
New infrastructure monitoring view with role-based access control

web-ng/lib/serviceradar_web_ng_web/live/infrastructure_live/index.ex

  • New LiveView for displaying cluster infrastructure with tabs for
    nodes, gateways, and agents with platform admin visibility controls
  • Implements PubSub-based caching for gateways and agents with periodic
    refresh and staleness detection using wall-clock time
  • Provides role-based access control where non-admins see only their
    tenant's agents while admins see full cluster topology
  • Includes comprehensive data loading from cluster nodes via RPC with
    timeout handling and fallback mechanisms
+1029/-0
show.ex
Agent detail view with live registry and system info         

web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex

  • Refactored agent display to show both live registry data and database
    records with preference for live data
  • Added live agent status banner and gateway node system information
    (memory, processes, schedulers, uptime)
  • Replaced hardcoded agent type names with dynamic lookup and added
    service checks card displaying configured checks
  • Enhanced registration timeline display with relative time formatting
    and improved capability visualization
+501/-159
index.ex
Job scheduler list with filtering and auto-refresh             

web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex

  • Completely redesigned job scheduler UI from card-based layout to
    sortable table with pagination
  • Added support for multiple job sources (Cron jobs and AshOban
    triggers) with filtering and search capabilities
  • Implemented auto-refresh functionality with configurable intervals and
    manual trigger capability for jobs
  • Added role-based access control for viewing platform jobs, triggering
    jobs, and accessing Oban Web
+610/-212
stateful_alert_engine.ex
Stateful alert engine with bucketed evaluation                     

elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex

  • New GenServer implementing bucketed stateful alert evaluation for log
    and event rules with time-windowed thresholds
  • Supports rule matching on logs/events with configurable group-by
    fields, severity filtering, and body content matching
  • Manages alert lifecycle including firing, recovery, cooldown periods,
    and renotification with history tracking
  • Persists rule state snapshots to database and loads them on
    initialization for fault tolerance
+960/-0 
gateway_process.ex
Gateway process for agent coordination                                     

elixir/serviceradar_core/lib/serviceradar/edge/gateway_process.ex

  • New GenServer representing an agent gateway in the ERTS cluster for
    managing check execution and agent coordination
  • Provides APIs for synchronous/asynchronous job execution, health
    checks, and result retrieval with load balancing
  • Implements agent discovery with domain-based and partition-based
    selection, registry integration, and metrics tracking
  • Includes health check scheduling and graceful termination with
    registry cleanup
+466/-0 
Tests
1 files
tenant_registry_test.exs
Tenant registry module comprehensive test coverage             

elixir/serviceradar_core/test/serviceradar/cluster/tenant_registry_test.exs

  • Adds comprehensive test suite for TenantRegistry module with 310 lines
    covering 13 test groups
  • Tests registry creation, tenant isolation, gateway/agent registration,
    and process lifecycle management
  • Validates slug-to-UUID mapping, heartbeat updates, and dynamic
    supervisor child spawning
  • Includes setup/teardown with unique tenant IDs to prevent cross-test
    pollution
+310/-0 
Configuration changes
1 files
.gitkeep
Docker compose credentials directory placeholder                 

docker/compose/creds/.gitkeep

  • Creates placeholder file to ensure directory structure is preserved in
    version control
+1/-0     
Database schema
1 files
20260110054954_add_stateful_alert_rules.exs
Add stateful alert rules and state tracking tables             

elixir/serviceradar_core/priv/repo/tenant_migrations/20260110054954_add_stateful_alert_rules.exs

  • Creates stateful_alert_rules table with configuration for alert
    thresholds, time windows, and notification settings
  • Creates stateful_alert_rule_states table for tracking rule state per
    group with bucket-based counting and cooldown tracking
  • Adds unique indexes on tenant_id with name and on tenant_id with
    rule_id and group_key for data integrity
+85/-0   
Additional files
101 files
.bazelignore +4/-0     
.bazelrc +5/-0     
.env-sample +33/-0   
.env.example +38/-0   
main.yml +18/-0   
sbom-images.yml +1/-3     
web-lint.yml +0/-60   
AGENTS.md +177/-11
INSTALL.md +19/-15 
MODULE.bazel +22/-2   
Makefile +58/-56 
README-Docker.md +17/-2   
README.md +3/-3     
ROADMAP.md +1/-1     
BUILD.bazel +11/-38 
BUILD.bazel +12/-0   
mix_release.bzl +141/-49
BUILD.bazel +1/-0     
README.md +4/-4     
config.json +5/-6     
main.go +174/-74
build.rs +0/-1     
monitoring.proto +3/-26   
server.rs +2/-0     
BUILD.bazel +1/-1     
main.go +1/-1     
README.md +2/-2     
monitoring.proto +2/-26   
server.rs +6/-6     
main.go +16/-2   
Cargo.toml +0/-3     
README.md +8/-8     
config.rs +85/-28 
grpc_server.rs +2/-2     
message_processor.rs +2/-16   
nats.rs +4/-0     
zen-consumer-with-otel.json +14/-11 
zen-consumer.json +14/-11 
.ko.yaml +0/-15   
BUILD.bazel +0/-17   
BUILD.bazel +0/-24   
app.go +0/-206 
config.json +0/-165 
config.json +0/-165 
main.go +0/-86   
BUILD.bazel +1/-0     
main.go +68/-0   
README.md +3/-3     
README.md +9/-12   
flowgger.toml +2/-1     
nats_output.rs +14/-0   
otel.toml +3/-1     
otel.toml.example +5/-2     
config.rs +21/-3   
nats_output.rs +22/-5   
setup.rs +1/-0     
BUILD.bazel +0/-25   
config.json +0/-111 
main.go +0/-138 
BUILD.bazel +0/-25   
config.json +0/-77   
main.go +0/-123 
main.go +1/-1     
README.md +3/-3     
config.rs +22/-1   
main.rs +23/-3   
docker-compose.dev.yml +23/-34 
docker-compose.elx.yml +117/-0 
docker-compose.spiffe.yml +39/-192
docker-compose.yml +316/-269
README.md +6/-5     
Dockerfile.agent-gateway +94/-0   
Dockerfile.core +0/-93   
Dockerfile.core-elx +108/-0 
Dockerfile.poller +0/-70   
Dockerfile.sync +0/-95   
Dockerfile.tools +1/-2     
Dockerfile.web +0/-110 
Dockerfile.web-ng +6/-0     
agent-minimal.docker.json +6/-6     
agent.docker.json +5/-20   
agent.mtls.json +7/-10   
bootstrap-nested-spire.sh +0/-80   
datasvc.docker.json +3/-2     
datasvc.mtls.json +14/-1   
db-event-writer.docker.json +15/-11 
db-event-writer.mtls.json +10/-8   
FRICTION_POINTS.md +0/-355 
README.md +0/-207 
SETUP_GUIDE.md +0/-307 
docker-compose.edge-e2e.yml +0/-27   
manage-packages.sh +0/-211 
setup-edge-e2e.sh +0/-198 
edge-poller-restart.sh +0/-178 
downstream-agent.conf +0/-32   
env +0/-4     
server.conf +0/-51   
upstream-agent.conf +0/-32   
entrypoint-certs.sh +13/-9   
entrypoint-core.sh +0/-98   
Additional files not shown

Imported from GitHub pull request. Original GitHub pull request: #2249 Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/pull/2249 Original created: 2026-01-11T18:38:26Z Original updated: 2026-01-11T19:21:07Z Original head: carverauto/serviceradar:fix/discovery_sources_empty Original base: staging Original merged: 2026-01-11T19:21:05Z by @mfreeman451 --- ### **User description** ## IMPORTANT: Please sign the Developer Certificate of Origin Thank you for your contribution to ServiceRadar. Please note, when contributing, the developer must include a [DCO sign-off statement]( https://developercertificate.org/) indicating the DCO acceptance in one commit message. Here is an example DCO Signed-off-by line in a commit message: ``` Signed-off-by: J. Doe <j.doe@domain.com> ``` ## Describe your changes ## Issue ticket number and link ## Code checklist before requesting a review - [ ] I have signed the DCO? - [ ] The build completes without errors? - [ ] All tests are passing when running make test? ___ ### **PR Type** Enhancement, Tests ___ ### **Description** - Comprehensive database schema migration with 30+ tables for tenant data including `ocsf_devices`, `ocsf_agents`, `service_checks`, `polling_schedules`, and `integration_sources` - New stateful alert rules engine with bucketed evaluation, state tracking, and fault tolerance for log and event-based alerting - Integration sources management LiveView with CRUD operations for Armis, SNMP, Syslog, Nmap, and Custom sources - Real-time cluster monitoring view with ERTS topology visibility, gateway/agent registration tracking, and Oban job queue monitoring - Infrastructure monitoring view with role-based access control and PubSub-based caching for cluster topology - Gateway process GenServer for agent coordination with health checks, job execution, and agent discovery - Enhanced agent detail view displaying live registry data, system information, and configured service checks - Redesigned job scheduler UI with sortable tables, filtering, pagination, and support for multiple job sources - Edge package creation refactored with Ash forms, automatic certificate generation, and tenant CA provisioning - Tenant registry test suite with comprehensive coverage for registry operations and process lifecycle management - Significant codebase migration from Go to Elixir with removal of legacy poller and discovery components ___ ### Diagram Walkthrough ```mermaid flowchart LR DB["Database Schema<br/>30+ Tables"] AlertEngine["Stateful Alert<br/>Engine"] ClusterMon["Cluster<br/>Monitoring"] InfraMon["Infrastructure<br/>View"] GatewayProc["Gateway<br/>Process"] IntegSources["Integration<br/>Sources UI"] DB --> AlertEngine DB --> IntegSources ClusterMon --> InfraMon GatewayProc --> ClusterMon IntegSources --> GatewayProc AlertEngine --> ClusterMon ``` <details><summary><h3>File Walkthrough</h3></summary> <table><thead><tr><th></th><th align="left">Relevant files</th></tr></thead><tbody><tr><td><strong>Database migration</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>20260107043446_initial_schema.exs</strong><dd><code>Initial tenant schema migration with comprehensive table definitions</code></dd></summary> <hr> elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs <ul><li>Adds comprehensive database migration for initial tenant schema with <br>1416 lines of table definitions<br> <li> Creates 30+ tables including <code>ocsf_devices</code>, <code>ocsf_agents</code>, <br><code>service_checks</code>, <code>polling_schedules</code>, <code>integration_sources</code>, and related <br>entities<br> <li> Establishes foreign key relationships, indexes, and unique constraints <br>across all tables<br> <li> Implements both <code>up</code> and <code>down</code> migration functions for schema creation <br>and rollback</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576">+1416/-0</a></td> </tr> </table></details></td></tr><tr><td><strong>Enhancement</strong></td><td><details><summary>8 files</summary><table> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Integration sources LiveView with CRUD and modal management</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex <ul><li>Implements LiveView module for managing integration sources (Armis, <br>SNMP, Syslog, Nmap, Custom)<br> <li> Provides create, read, update, delete operations with modal-based UI <br>for integration source management<br> <li> Includes query management, network blacklist configuration, and <br>credential handling with JSON parsing<br> <li> Implements filtering by source type and enabled status, with <br>agent/partition assignment and sync statistics display</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cd">+1471/-0</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>New cluster monitoring LiveView with real-time status tracking</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/settings/cluster_live/index.ex <ul><li>New LiveView module for monitoring distributed Horde cluster with <br>real-time visibility into ERTS topology, gateways, agents, and Oban <br>job queues<br> <li> Implements PubSub subscriptions for cluster events, agent <br>registrations, and gateway platform updates with automatic refresh <br>scheduling<br> <li> Provides comprehensive data loading and caching mechanisms for <br>gateways and agents across cluster nodes using RPC calls<br> <li> Includes helper functions for timestamp parsing, staleness detection, <br>tenant filtering, and rendering health metrics and event logs</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-644e64ac49eaa128f9b429c8507d6a8a9ad820e2796721dd665caba8a154d24e">+1086/-0</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Refactor edge package creation with Ash forms and auto-cert generation</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex <ul><li>Migrated from Ecto changesets to <code>AshPhoenix.Form</code> for form handling <br>with automatic validation and error management<br> <li> Added automatic certificate generation via <code>create_with_tenant_cert</code> <br>with tenant CA auto-provisioning<br> <li> Changed component type from "poller" to "gateway" as primary component <br>type, added support for "sync" type<br> <li> Enhanced success modal with Docker/systemd install commands, <br>certificate details, and improved UX with loading states<br> <li> Added tenant scoping to all package operations and improved form field <br>handling with JSON config support</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62f">+546/-224</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>New infrastructure monitoring view with role-based access control</code></dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/infrastructure_live/index.ex <ul><li>New LiveView for displaying cluster infrastructure with tabs for <br>nodes, gateways, and agents with platform admin visibility controls<br> <li> Implements PubSub-based caching for gateways and agents with periodic <br>refresh and staleness detection using wall-clock time<br> <li> Provides role-based access control where non-admins see only their <br>tenant's agents while admins see full cluster topology<br> <li> Includes comprehensive data loading from cluster nodes via RPC with <br>timeout handling and fallback mechanisms</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-f1af30a84da554ef3d43b226a9303174b33dd5d27e23e9b702031483074e5f54">+1029/-0</a></td> </tr> <tr> <td> <details> <summary><strong>show.ex</strong><dd><code>Agent detail view with live registry and system info</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex <ul><li>Refactored agent display to show both live registry data and database <br>records with preference for live data<br> <li> Added live agent status banner and gateway node system information <br>(memory, processes, schedulers, uptime)<br> <li> Replaced hardcoded agent type names with dynamic lookup and added <br>service checks card displaying configured checks<br> <li> Enhanced registration timeline display with relative time formatting <br>and improved capability visualization</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5e622205d1abddd8ad7dcf7a8ca1be583804d622d3d38b75140e9b909cf0534a">+501/-159</a></td> </tr> <tr> <td> <details> <summary><strong>index.ex</strong><dd><code>Job scheduler list with filtering and auto-refresh</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> web-ng/lib/serviceradar_web_ng_web/live/admin/job_live/index.ex <ul><li>Completely redesigned job scheduler UI from card-based layout to <br>sortable table with pagination<br> <li> Added support for multiple job sources (Cron jobs and AshOban <br>triggers) with filtering and search capabilities<br> <li> Implemented auto-refresh functionality with configurable intervals and <br>manual trigger capability for jobs<br> <li> Added role-based access control for viewing platform jobs, triggering <br>jobs, and accessing Oban Web</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b275777f111009bbb4976d57623231aae2262452ef320a9f91ecbf202144115a">+610/-212</a></td> </tr> <tr> <td> <details> <summary><strong>stateful_alert_engine.ex</strong><dd><code>Stateful alert engine with bucketed evaluation</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex <ul><li>New GenServer implementing bucketed stateful alert evaluation for log <br>and event rules with time-windowed thresholds<br> <li> Supports rule matching on logs/events with configurable group-by <br>fields, severity filtering, and body content matching<br> <li> Manages alert lifecycle including firing, recovery, cooldown periods, <br>and renotification with history tracking<br> <li> Persists rule state snapshots to database and loads them on <br>initialization for fault tolerance</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-bae3a52db882de8c947e62f219a95dff8db4e155e37d9a361dbe14ec25fcd3bd">+960/-0</a>&nbsp; </td> </tr> <tr> <td> <details> <summary><strong>gateway_process.ex</strong><dd><code>Gateway process for agent coordination</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/lib/serviceradar/edge/gateway_process.ex <ul><li>New GenServer representing an agent gateway in the ERTS cluster for <br>managing check execution and agent coordination<br> <li> Provides APIs for synchronous/asynchronous job execution, health <br>checks, and result retrieval with load balancing<br> <li> Implements agent discovery with domain-based and partition-based <br>selection, registry integration, and metrics tracking<br> <li> Includes health check scheduling and graceful termination with <br>registry cleanup</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b0b102ecc88366a0e9eea09de860b80929729880cd47f1aa96d6cfcb0ccc9f8f">+466/-0</a>&nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Tests</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>tenant_registry_test.exs</strong><dd><code>Tenant registry module comprehensive test coverage</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/test/serviceradar/cluster/tenant_registry_test.exs <ul><li>Adds comprehensive test suite for <code>TenantRegistry</code> module with 310 lines <br>covering 13 test groups<br> <li> Tests registry creation, tenant isolation, gateway/agent registration, <br>and process lifecycle management<br> <li> Validates slug-to-UUID mapping, heartbeat updates, and dynamic <br>supervisor child spawning<br> <li> Includes setup/teardown with unique tenant IDs to prevent cross-test <br>pollution</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-ff7e8a23791514dee76b77ef78fa2f8bc548f42a4d09897ae375e2ca7734fca5">+310/-0</a>&nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Configuration changes</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>.gitkeep</strong><dd><code>Docker compose credentials directory placeholder</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> docker/compose/creds/.gitkeep <ul><li>Creates placeholder file to ensure directory structure is preserved in <br>version control</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-d72c41aab2d6f2c230a4340dfefe7917cdd12bed942c825aa0d4c9875a637bac">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Database schema</strong></td><td><details><summary>1 files</summary><table> <tr> <td> <details> <summary><strong>20260110054954_add_stateful_alert_rules.exs</strong><dd><code>Add stateful alert rules and state tracking tables</code>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </dd></summary> <hr> elixir/serviceradar_core/priv/repo/tenant_migrations/20260110054954_add_stateful_alert_rules.exs <ul><li>Creates <code>stateful_alert_rules</code> table with configuration for alert <br>thresholds, time windows, and notification settings<br> <li> Creates <code>stateful_alert_rule_states</code> table for tracking rule state per <br>group with bucket-based counting and cooldown tracking<br> <li> Adds unique indexes on tenant_id with name and on tenant_id with <br>rule_id and group_key for data integrity</ul> </details> </td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0f4b254a0546c5b2926a95e946ac30080b8fc11db180d64d6342dad1b97c66db">+85/-0</a>&nbsp; &nbsp; </td> </tr> </table></details></td></tr><tr><td><strong>Additional files</strong></td><td><details><summary>101 files</summary><table> <tr> <td><strong>.bazelignore</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-a5641cd37d6ad98b32cdfce1980836cc68312277bc6a7052f55da02ada5bc6cf">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>.bazelrc</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-544556920c45b42cbfe40159b082ce8af6bd929e492d076769226265f215832f">+5/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>.env-sample</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c4368a972a7fa60d9c4e333cebf68cdb9a67acb810451125c02e3b7eb2594e3d">+33/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>.env.example</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-a3046da0d15a27e89f2afe639b25748a7ad4d9290af3e7b1b6c1a5533c8f0a8c">+38/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-7829468e86c1cc5d5133195b5cb48e1ff6c75e3e9203777f6b2e379d9e4882b3">+18/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>sbom-images.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-306f4aa8e8e286f727246a7517eecd45f3535fd99a644f60d635b9fa39875f54">+1/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>web-lint.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9d090859e31fc574efb47cacf534d619a3e83d55e59da3be2484999b9055b1b2">+0/-60</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>AGENTS.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-a54ff182c7e8acf56acfd6e4b9c3ff41e2c41a31c9b211b2deb9df75d9a478f9">+177/-11</a></td> </tr> <tr> <td><strong>INSTALL.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-09b140a43ebfdd8dbec31ce72cafffd15164d2860fd390692a030bcb932b54a0">+19/-15</a>&nbsp; </td> </tr> <tr> <td><strong>MODULE.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-6136fc12446089c3db7360e923203dd114b6a1466252e71667c6791c20fe6bdc">+22/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Makefile</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-76ed074a9305c04054cdebb9e9aad2d818052b07091de1f20cad0bbac34ffb52">+58/-56</a>&nbsp; </td> </tr> <tr> <td><strong>README-Docker.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9fd61d24482efe68c22d8d41e2a1dcc440f39195aa56e7a050f2abe598179efd">+17/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>ROADMAP.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-683343bdf93f55ed3cada86151abb8051282e1936e58d4e0a04beca95dff6e51">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-884fa9353a5226345e44fbabea3300efc7a87dfbcde0b6a42521ca51823f1b68">+11/-38</a>&nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0e80ea46aeb61a873324685edb96eae864c7a2004fbb7ee404b4ec951190ba10">+12/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>mix_release.bzl</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-86ec281f99363b6b6eb1f49e21d83b7eeca93a35b552b9f305fffc6855e38ccd">+141/-49</a></td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-143f8d1549d52f28906f19ce28e5568a5be474470ff103c2c1e63c3e6b08d670">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-bfd308915d0cf522e7fc76600dee687617dc69165ab22502a1d219850c0c0860">+4/-4</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5b1bc8fe77422534739bdd3a38dc20d2634a86c171265c34e1b5d0c5a61b6bab">+5/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-61358711e980ccf505246fd3915f97cbd3a380e9b66f6fa5aad46749968c5ca3">+174/-74</a></td> </tr> <tr> <td><strong>build.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-251e7a923f45f8f903e510d10f183366bda06d281c8ecc3669e1858256e2186d">+0/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>monitoring.proto</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b56f709f4a0a3db694f2124353908318631f23e20b7846bc4b8ee869e2e0632a">+3/-26</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-bce0f4ca6548712f224b73816825d28e831acbbff7dbed3c98671ed50f65d028">+2/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-7da152990199fd73c1eecb40f9c49e0d4e6453a8ec1acb111e445c55d1ca0af0">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-f25402eade63525184cb5e7437accff93c7b9338eebe81add6dc5f2a9eb12550">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-2e9751b437fa61442aac074c7a4a912d0ac50ac3ea156ac8aedd8478d21c6bdb">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>monitoring.proto</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9faf6025eb0d3d38383f5b7ad2b733abeb38454d5e4de3e83994e94b12d87a50">+2/-26</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-2c4395fee16396339c3eea518ad9bec739174c67c9cedf62e6848c17136dd33e">+6/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-ed4d81d29a7267f93fd77e17993fd3491b9ef6ded18490b4514d10ed1d803bc2">+16/-2</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Cargo.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-fcf0c672917b64a5b953a914af013f16dddd6a1d813810236364e32f1ae70382">+0/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-643d2c3959322902c5bc9a22666b1e9ef71fa0bb87c9451b0e4147a4d5b51987">+8/-8</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-05038f3867985e757de9027609950e682bad6d1992dac6acd7c28962a3c65dc4">+85/-28</a>&nbsp; </td> </tr> <tr> <td><strong>grpc_server.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-e4564a93f6cf84ff91cd3d8141fc9272ec9b4ec19defd107afa42be01fcfed5b">+2/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>message_processor.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9fcbc5358a9009e60a8cd22d21e5a9ea652787c727732d0b869e0865495114c3">+2/-16</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-97f7335def0ad5d644b594a1076ae2d7080b11259cbb8de22c7946cc8e4b39f8">+4/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>zen-consumer-with-otel.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-68375f1f7847e1fbdf75664f6be65b1ad94ae6ce86ed73fc5964d65054668acb">+14/-11</a>&nbsp; </td> </tr> <tr> <td><strong>zen-consumer.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-4d308af9802a93a0f656e8c02a3b5fcd8991407bb18360f087470db74e1f9524">+14/-11</a>&nbsp; </td> </tr> <tr> <td><strong>.ko.yaml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c403e088cfbc9e150c604da6a056a188e7a5f5585c45cdb515460f000008b441">+0/-15</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-cf437f055db002c5a1dba0ac4a4949d0ecc4b095e8e760bf5aec0f5d4c08f572">+0/-17</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5570e460b26d54e1c23df4652073efa22cfd37353237956b5ffc4f355bbb3346">+0/-24</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>app.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-4ad8a289575edf3b163088617b7a40ae1305c29ced0c7d59b3751c57d6938072">+0/-206</a>&nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-2423ef78d36e905ae993b69ff59f5df6b2e1b9492fb0fa8c6d0aad7c76d2d229">+0/-165</a>&nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-ef778d85ac6f9652c25cb0d631f0fe8dfb3edac4dde5d719a4fc2926fb5c3216">+0/-165</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-4ab3fd1d4debc53dd2499d94a0f60c648fdae4235dd1e3678095a975f5bb434a">+0/-86</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c62c0139ebdb337369f4067567cd2c52b8e7decb3ddfabc77f9f67b2f6e5789c">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5e7731adfb877918cd65d9d5531621312496450fd550fea2682efca4ca8fe816">+68/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0b0725713b87dca1de57200214a4fe04633f0d856c39aa8032280227bf8e8141">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-f425b4378f84e0ba0c6f532facff17ff5d55b4dc6033d8bf35130a159cd2ba32">+9/-12</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>flowgger.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-af9f49f931e282dca53d1f0521b036d222fe671f77e61a876a84cf4c6d7cca4d">+2/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats_output.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-a82e2e4d413539bf0b414b5629665b19648447523994cba639c4d1238aa5a0c1">+14/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>otel.toml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c64b9ace832b8ea57a2be62f84166e03bb1904882635d444ec76a880cdf14cc0">+3/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>otel.toml.example</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c1889866f35f98cdba9cd229fc119273c5fa5fca501451db23813b575f6fec66">+5/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-abbaec651da3d6af96b482e0f77bb909b65dbe0cabd78b5803769cc9dab0a1b0">+21/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>nats_output.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-6b585ea3564a481174e04da1270e2e13edd4e2b980d02a2652d6d21e6d82a498">+22/-5</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>setup.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-3891f667deb20fd26e296d3e2742c57378d3764fe1743118e612465ae360391f">+1/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-e1f7c698e0e3a4e6afa971c1140e71cbf22593fbb19c81cb26b02c15c5dc46ec">+0/-25</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9edc2486fff55fc399e0ac96dba5137948a7ea7285f5ef7846835355684b7ab5">+0/-111</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-4b8ec845da50cd58d011e69f9d1c30530ee1968df26616b8768bb1fc03433bbe">+0/-138</a>&nbsp; </td> </tr> <tr> <td><strong>BUILD.bazel</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-4f5d2ea4260d490a0d6f28adde0b35eca8af77d22f3ee366a783946c53687619">+0/-25</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-bcac20d6b3cb81f0059e766839ba1ee59a885009249501b0ba1182ebb1daea25">+0/-77</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-78dc6bc53f1c760c66f43ff5f486bfe78a65bee8b2e0d4862293ec0892da2b29">+0/-123</a>&nbsp; </td> </tr> <tr> <td><strong>main.go</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-bc6eeb1b05bcb9179525e32fac1de9926b5823ec3504be546ab10c5c9740f544">+1/-1</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9c32ee8446458b6fd2ae7fee52016f4b707a59978b67888cd5bee2804d934528">+3/-3</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>config.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-c89b88ba4d2bf0a054d0ba69a672a92c30140b8d19503d67b980a218ffe3106d">+22/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>main.rs</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-33b655d8730ae3e9c844ee280787d11f1b0d5343119188273f89558805f814ba">+23/-3</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>docker-compose.dev.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9542f82d64bbeebd91f6236324bfe199e9657e2cb1fd9779d5d6dcdcf9cd4de1">+23/-34</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.elx.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9562070d7ad4a3e9b2d06567008cf35de1d96448d914b3b45bf6c36d97cdd914">+117/-0</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.spiffe.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-603fd9e7d40841d174f26b95d0cb0c9537430bf3f7a5da3ccbba4ea3d8ac66c9">+39/-192</a></td> </tr> <tr> <td><strong>docker-compose.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-e45e45baeda1c1e73482975a664062aa56f20c03dd9d64a827aba57775bed0d3">+316/-269</a></td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-da8fcbe728a9172b578e5d754f8e2df214c658c4321f610e63dd68bea828ab49">+6/-5</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.agent-gateway</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-332bc81a932ae08efa711a71b60fe0954d99bf17ebdab00a3baaa177a44de8b0">+94/-0</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.core</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-34849cba9f0b40185bfcba10b1a076a87f4ab2d63e08d5a88fc932c60956df66">+0/-93</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.core-elx</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5ec7a971285669999af442a0c7f141c34f7fd9180257307f5c4ed12f789a2182">+108/-0</a>&nbsp; </td> </tr> <tr> <td><strong>Dockerfile.poller</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-d3ba129830fb366bfe23b00db4ef6218b10fc981d3c04842b1b3b3b367a8982f">+0/-70</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.sync</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0227933b9961fd553af1d229e89d71a0271fdc475081bbcef49b587941af1eda">+0/-95</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.tools</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-0258db71e4070e342198965f1d046f3097640850b037df8a2287a7e239630add">+1/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>Dockerfile.web</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-89393e743136e5cf7e1d67b378bc56da802452c481f2d63399bf72aac3d21e67">+0/-110</a>&nbsp; </td> </tr> <tr> <td><strong>Dockerfile.web-ng</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-92d43af1965575d56c3380ecc8a81024aac2ff36f039ec2d3839e9fc7852bc10">+6/-0</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent-minimal.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-1f09fad94636c90373af8e270f6ba0332ae4f4d1df50a4909729280a3a9691e6">+6/-6</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-5d33fe703515d03076d31261ecf946e9c6fc668cf5bf65099d49b670739e455e">+5/-20</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>agent.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-008f2216f159a9bd5db9cc90baaf6f1e64487df7af05b56ab3b9d6c4946aa95f">+7/-10</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>bootstrap-nested-spire.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-ab4746a08fb1e0b307a1e47660cd22182e283a087cba87dcbff0fdfe750f44f1">+0/-80</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>datasvc.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-3f2719d3dbfe042e8383739e3c78e74e5f851a44e5e46bea8e79c4b79fdcc34f">+3/-2</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>datasvc.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-3a45619e57f1e6e9a31486ec7fffb33ef246e271f82bac272ee0a946b88da70a">+14/-1</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>db-event-writer.docker.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-9fc51271f7ef5bb460160013e24e44e829b730656891d26fc49d5fe72fbb3147">+15/-11</a>&nbsp; </td> </tr> <tr> <td><strong>db-event-writer.mtls.json</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-7a33f95f7545499abf0ed9fc91b58499ab209639e4885019579c959583fc7496">+10/-8</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>FRICTION_POINTS.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b0653c58880f810ba832c0500733d63de309db98b43009fe73a1862494cf41bd">+0/-355</a>&nbsp; </td> </tr> <tr> <td><strong>README.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-31849f033cfc932acee35f549c069abb1f36101c352e553dd6bff8713b29f98c">+0/-207</a>&nbsp; </td> </tr> <tr> <td><strong>SETUP_GUIDE.md</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-b4914f8640a78038e45f51235a624535672680dc902de5f107fc051f4f281913">+0/-307</a>&nbsp; </td> </tr> <tr> <td><strong>docker-compose.edge-e2e.yml</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-575d19ea771bdf8102cb9729db43a1bfd6afc2527160e54105beeac2e314f362">+0/-27</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>manage-packages.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-3c2ff6febbddb956c71557894adaf7d0a39a1f20dda120fe126364946bc47280">+0/-211</a>&nbsp; </td> </tr> <tr> <td><strong>setup-edge-e2e.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-2714e2c7e111f69ea9e9f5ddd7f6a70fa5ea96e3a53b851cb13b8b8b7cd12917">+0/-198</a>&nbsp; </td> </tr> <tr> <td><strong>edge-poller-restart.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-96a8fe52c38fd0d7c14895127df34a27be311cac89c53d28ee178661b629bd22">+0/-178</a>&nbsp; </td> </tr> <tr> <td><strong>downstream-agent.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-747de0375ced42af978ca7dac239862bdabb7f6bd0bd634f134b485517a7b4ee">+0/-32</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>env</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-686f1a954c542f2ec9bf14c3170648b65190ad242c7f3a95a0f872ae41b8b1c6">+0/-4</a>&nbsp; &nbsp; &nbsp; </td> </tr> <tr> <td><strong>server.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-025f5b5ab79526cf549ca1fdb90dd659ba76b438f05a7f77d916d18728c4b572">+0/-51</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>upstream-agent.conf</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-e8a869ddf4affa31536a8d4e4e6f09c40072a7026da2c609d93c6ecf04138902">+0/-32</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>entrypoint-certs.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-83d6800b184a5233c66c69766286b0a60fece1bc64addb112d9f8dc019437f05">+13/-9</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>entrypoint-core.sh</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-8338c1aca698162657e6ec96bfa568dd1f27250c7939107c927c11685db1e051">+0/-98</a>&nbsp; &nbsp; </td> </tr> <tr> <td><strong>Additional files not shown</strong></td> <td><a href="https://github.com/carverauto/serviceradar/pull/2249/files#diff-2f328e4cd8dbe3ad193e49d92bcf045f47a6b72b1e9487d366f6b8288589b4ca"></a></td> </tr> </table></details></td></tr></tbody></table> </details> ___
qodo-code-review[bot] commented 2026-01-11 18:40:21 +00:00 (Migrated from github.com)
Author
Owner

Imported GitHub PR comment.

Original author: @qodo-code-review[bot]
Original URL: https://github.com/carverauto/serviceradar/pull/2249#issuecomment-3735379626
Original created: 2026-01-11T18:40:21Z

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Authorization bypass

Description: load_tenant/1 calls Ash.get(Tenant, tenant_id, authorize?: false), which can bypass
authorization checks and may allow an authenticated user to load tenant records they are
not permitted to access if tenant_id can be influenced (e.g., via
current_scope.user.tenant_id being stale/forged).
index.ex [981-998]

Referred Code
defp get_tenant(socket) do
  case socket.assigns[:current_scope] do
    %{active_tenant: %Tenant{} = tenant} ->
      tenant

    %{user: %{tenant_id: tenant_id}} when not is_nil(tenant_id) ->
      load_tenant(tenant_id)

    _ ->
      nil
  end
end

defp load_tenant(tenant_id) do
  case Ash.get(Tenant, tenant_id, authorize?: false) do
    {:ok, %Tenant{} = tenant} -> tenant
    _ -> nil
  end

Tenant isolation risk

Description: Tenant scoping depends on get_tenant/1, but if it returns nil the code still calls
OnboardingPackages.list(%{limit: 50}, tenant: tenant), which could result in cross-tenant
data exposure if the downstream list/get implementations treat a nil tenant as “no tenant
filter”.
index.ex [20-27]

Referred Code
security_mode = OnboardingPackages.configured_security_mode()
tenant = get_tenant(socket)

socket =
  socket
  |> assign(:page_title, "Edge Onboarding")
  |> assign(:packages, OnboardingPackages.list(%{limit: 50}, tenant: tenant))
  |> assign(:show_create_modal, false)

Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Possible runtime crash: User-controlled component_type is converted using String.to_existing_atom/1 which can
raise and crash the LiveView instead of handling invalid values gracefully.

Referred Code
AshPhoenix.Form.for_create(OnboardingPackage, :create,
  domain: ServiceRadar.Edge,
  tenant: tenant,
  transform_params: fn _form, params, _action ->
    # Convert component_type string to atom if needed
    params =
      case params["component_type"] do
        type when is_binary(type) and type != "" ->
          Map.put(params, "component_type", String.to_existing_atom(type))

        _ ->
          params
      end

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Unstructured console logs: Logger console output is configured with a plain-text format rather than structured
logging (e.g., JSON), reducing auditability and automated monitoring effectiveness.

Referred Code
config :logger, :console,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id, :gateway_id, :partition_id]

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unsafe atom conversion: Converting external form input to an atom via String.to_existing_atom/1 risks crashes and
is unsafe input handling without a strict allowlist conversion.

Referred Code
transform_params: fn _form, params, _action ->
  # Convert component_type string to atom if needed
  params =
    case params["component_type"] do
      type when is_binary(type) and type != "" ->
        Map.put(params, "component_type", String.to_existing_atom(type))

      _ ->
        params

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: Critical admin actions (create/revoke/delete edge packages) are performed without any
explicit audit log emission in the new LiveView code, requiring verification that
underlying domain functions log these actions with user/tenant context.

Referred Code
def handle_event("create_package", %{"form" => params}, socket) do
  form = AshPhoenix.Form.validate(socket.assigns.create_form, params)

  if form.source.valid? do
    # Show loading state while creating package and generating certificates
    socket = assign(socket, :creating, true)

    # Extract validated form data
    actor = get_actor(socket)
    tenant = get_tenant(socket)
    attrs = build_package_attrs_from_form(params, socket.assigns.security_mode)

    # Use create_with_tenant_cert for automatic certificate generation
    # This will auto-generate the tenant CA if it doesn't exist
    result =
      OnboardingPackages.create_with_tenant_cert(attrs,
        actor: actor,
        tenant: tenant
      )

    case result do



 ... (clipped 86 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Detailed error surfaced: A user-facing flash message includes interpolated error_msg which may expose internal
details depending on the underlying error content.

Referred Code
  {:error, error} ->
    error_msg = format_error(error)

    {:noreply,
     socket
     |> assign(:creating, false)
     |> put_flash(:error, "Failed to create package: #{error_msg}")}
end

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
- Requires Further Human Verification
🏷️ - Compliance label
Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2249#issuecomment-3735379626 Original created: 2026-01-11T18:40:21Z --- ## PR Compliance Guide 🔍 <!-- https://github.com/carverauto/serviceradar/commit/9ab7926bd38fbdebc06fb0b22dc6c3a10eb55dc2 --> Below is a summary of compliance checks for this PR:<br> <table><tbody><tr><td colspan='2'><strong>Security Compliance</strong></td></tr> <tr><td rowspan=2>⚪</td> <td><details><summary><strong>Authorization bypass </strong></summary><br> <b>Description:</b> <code>load_tenant/1</code> calls <code>Ash.get(Tenant, tenant_id, authorize?: false)</code>, which can bypass <br>authorization checks and may allow an authenticated user to load tenant records they are <br>not permitted to access if <code>tenant_id</code> can be influenced (e.g., via <br><code>current_scope.user.tenant_id</code> being stale/forged).<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR981-R998'>index.ex [981-998]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir defp get_tenant(socket) do case socket.assigns[:current_scope] do %{active_tenant: %Tenant{} = tenant} -> tenant %{user: %{tenant_id: tenant_id}} when not is_nil(tenant_id) -> load_tenant(tenant_id) _ -> nil end end defp load_tenant(tenant_id) do case Ash.get(Tenant, tenant_id, authorize?: false) do {:ok, %Tenant{} = tenant} -> tenant _ -> nil end ``` </details></details></td></tr> <tr><td><details><summary><strong>Tenant isolation risk </strong></summary><br> <b>Description:</b> Tenant scoping depends on <code>get_tenant/1</code>, but if it returns <code>nil</code> the code still calls <br><code>OnboardingPackages.list(%{limit: 50}, tenant: tenant)</code>, which could result in cross-tenant <br>data exposure if the downstream list/get implementations treat a <code>nil</code> tenant as “no tenant <br>filter”.<br> <strong><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR20-R27'>index.ex [20-27]</a></strong><br> <details open><summary>Referred Code</summary> ```elixir security_mode = OnboardingPackages.configured_security_mode() tenant = get_tenant(socket) socket = socket |> assign(:page_title, "Edge Onboarding") |> assign(:packages, OnboardingPackages.list(%{limit: 50}, tenant: tenant)) |> assign(:show_create_modal, false) ``` </details></details></td></tr> <tr><td colspan='2'><strong>Ticket Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary>🎫 <strong>No ticket provided </strong></summary> - [ ] Create ticket/issue <!-- /create_ticket --create_ticket=true --> </details></td></tr> <tr><td colspan='2'><strong>Codebase Duplication Compliance</strong></td></tr> <tr><td>⚪</td><td><details><summary><strong>Codebase context is not defined </strong></summary> Follow the <a href='https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/'>guide</a> to enable codebase context checks. </details></td></tr> <tr><td colspan='2'><strong>Custom Compliance</strong></td></tr> <tr><td rowspan=1>🟢</td><td> <details><summary><strong>Generic: Meaningful Naming and Self-Documenting Code</strong></summary><br> **Objective:** Ensure all identifiers clearly express their purpose and intent, making code <br>self-documenting<br> **Status:** Passed<br> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=3>🔴</td> <td><details> <summary><strong>Generic: Robust Error Handling and Edge Case Management</strong></summary><br> **Objective:** Ensure comprehensive error handling that provides meaningful context and graceful <br>degradation<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR949-R961'><strong>Possible runtime crash</strong></a>: User-controlled <code>component_type</code> is converted using <code>String.to_existing_atom/1</code> which can <br>raise and crash the LiveView instead of handling invalid values gracefully.<br> <details open><summary>Referred Code</summary> ```elixir AshPhoenix.Form.for_create(OnboardingPackage, :create, domain: ServiceRadar.Edge, tenant: tenant, transform_params: fn _form, params, _action -> # Convert component_type string to atom if needed params = case params["component_type"] do type when is_binary(type) and type != "" -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> params end ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Secure Logging Practices</strong></summary><br> **Objective:** To ensure logs are useful for debugging and auditing without exposing sensitive <br>information like PII, PHI, or cardholder data.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-abaac0167f05b505cf4bc5c5d375ae769001d8cc20120919941748c79681bbaeR8-R10'><strong>Unstructured console logs</strong></a>: Logger console output is configured with a plain-text format rather than structured <br>logging (e.g., JSON), reducing auditability and automated monitoring effectiveness.<br> <details open><summary>Referred Code</summary> ```elixir config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id, :gateway_id, :partition_id] ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Security-First Input Validation and Data Handling</strong></summary><br> **Objective:** Ensure all data inputs are validated, sanitized, and handled securely to prevent <br>vulnerabilities<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR952-R960'><strong>Unsafe atom conversion</strong></a>: Converting external form input to an atom via <code>String.to_existing_atom/1</code> risks crashes and <br>is unsafe input handling without a strict allowlist conversion.<br> <details open><summary>Referred Code</summary> ```elixir transform_params: fn _form, params, _action -> # Convert component_type string to atom if needed params = case params["component_type"] do type when is_binary(type) and type != "" -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> params ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td rowspan=2>⚪</td> <td><details> <summary><strong>Generic: Comprehensive Audit Trails</strong></summary><br> **Objective:** To create a detailed and reliable record of critical system actions for security analysis <br>and compliance.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR121-R227'><strong>Missing audit logging</strong></a>: Critical admin actions (create/revoke/delete edge packages) are performed without any <br>explicit audit log emission in the new LiveView code, requiring verification that <br>underlying domain functions log these actions with user/tenant context.<br> <details open><summary>Referred Code</summary> ```elixir def handle_event("create_package", %{"form" => params}, socket) do form = AshPhoenix.Form.validate(socket.assigns.create_form, params) if form.source.valid? do # Show loading state while creating package and generating certificates socket = assign(socket, :creating, true) # Extract validated form data actor = get_actor(socket) tenant = get_tenant(socket) attrs = build_package_attrs_from_form(params, socket.assigns.security_mode) # Use create_with_tenant_cert for automatic certificate generation # This will auto-generate the tenant CA if it doesn't exist result = OnboardingPackages.create_with_tenant_cert(attrs, actor: actor, tenant: tenant ) case result do ... (clipped 86 lines) ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td><details> <summary><strong>Generic: Secure Error Handling</strong></summary><br> **Objective:** To prevent the leakage of sensitive system information through error messages while <br>providing sufficient detail for internal debugging.<br> **Status:** <br><a href='https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR171-R178'><strong>Detailed error surfaced</strong></a>: A user-facing flash message includes interpolated <code>error_msg</code> which may expose internal <br>details depending on the underlying error content.<br> <details open><summary>Referred Code</summary> ```elixir {:error, error} -> error_msg = format_error(error) {:noreply, socket |> assign(:creating, false) |> put_flash(:error, "Failed to create package: #{error_msg}")} end ``` </details> > Learn more about managing compliance <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#configuration-options'>generic rules</a> or creating your own <a href='https://qodo-merge-docs.qodo.ai/tools/compliance/#custom-compliance'>custom rules</a> </details></td></tr> <tr><td align="center" colspan="2"> - [ ] Update <!-- /compliance --update_compliance=true --> </td></tr></tbody></table> <details><summary>Compliance status legend</summary> 🟢 - Fully Compliant<br> 🟡 - Partial Compliant<br> 🔴 - Not Compliant<br> ⚪ - Requires Further Human Verification<br> 🏷️ - Compliance label<br> </details>
qodo-code-review[bot] commented 2026-01-11 18:41:38 +00:00 (Migrated from github.com)
Author
Owner

Imported GitHub PR comment.

Original author: @qodo-code-review[bot]
Original URL: https://github.com/carverauto/serviceradar/pull/2249#issuecomment-3735386494
Original created: 2026-01-11T18:41:38Z

PR Code Suggestions

Latest suggestions up to 4f60ff3

CategorySuggestion                                                                                                                                    Impact
Security
Avoid packaging sensitive Hex config

To avoid packaging sensitive credentials, modify the tar command to archive only
the .hex/cache and .hex/packages subdirectories instead of the entire ~/.hex
directory.

scripts/update-hex-cache.sh [20-24]

 echo "Regenerating hex cache tarball..."
-(cd ~ && tar -czf "$REPO_ROOT/build/hex_cache.tar.gz" .hex)
+mkdir -p "$REPO_ROOT/build"
+
+# Only package cache artifacts; avoid config/auth files under ~/.hex
+(cd ~ && tar -czf "$REPO_ROOT/build/hex_cache.tar.gz" .hex/cache .hex/packages)
 
 echo "Done! Hex cache updated at build/hex_cache.tar.gz"
 echo "Size: $(ls -lh "$REPO_ROOT/build/hex_cache.tar.gz" | awk '{print $5}')"
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a potential security risk of leaking sensitive credentials from the ~/.hex directory and provides a precise fix to only archive the necessary cache directories.

Medium
Possible issue
Normalize blank discovery sources
Suggestion Impact:The commit adds normalization to treat `nil` and empty-string sources as "unknown" and also filters out empty strings when aggregating discovery_sources in the upsert fragment. It does not implement the suggested whitespace trimming/to_string handling, so whitespace-only values may still persist.

code diff:

-      source = update.source || "unknown"
+      source = if update.source in [nil, ""], do: "unknown", else: update.source
 
       record = %{
         uid: device_id,
@@ -296,7 +296,7 @@
             metadata: fragment("COALESCE(EXCLUDED.metadata, ?)", d.metadata),
             discovery_sources:
               fragment(
-                "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)",
+                "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')",
                 d.discovery_sources

Sanitize the source value by trimming whitespace and falling back to "unknown"
if it's an empty string to prevent storing blank discovery sources.

elixir/serviceradar_core/lib/serviceradar/inventory/sync_ingestor.ex [264-279]

-source = update.source || "unknown"
+source =
+  update.source
+  |> to_string()
+  |> String.trim()
+  |> case do
+    "" -> "unknown"
+    s -> s
+  end
 
 record = %{
   uid: device_id,
   ip: update.ip,
   mac: update.mac,
   hostname: update.hostname,
   name: update.hostname || update.ip,
   is_available: update.is_available || false,
   metadata: update.metadata || %{},
   discovery_sources: [source],
   first_seen_time: timestamp,
   last_seen_time: timestamp,
   created_time: timestamp,
   modified_time: timestamp
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that an empty or whitespace-only source string would be persisted, and proposes a robust fix to normalize it to "unknown", improving data integrity.

Low
Filter empty array entries
Suggestion Impact:The commit updated the SQL fragment used for discovery_sources to add `AND src <> ''`, filtering out empty strings during array aggregation as suggested. It also added a related application-level guard to treat empty update.source values as "unknown".

code diff:

             metadata: fragment("COALESCE(EXCLUDED.metadata, ?)", d.metadata),
             discovery_sources:
               fragment(
-                "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)",
+                "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')",
                 d.discovery_sources
               ),

Update the SQL fragment to filter out empty strings in addition to NULL values
when merging discovery_sources to ensure data cleanliness.

elixir/serviceradar_core/lib/serviceradar/inventory/sync_ingestor.ex [297-301]

 discovery_sources:
   fragment(
-    "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)",
+    "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')",
     d.discovery_sources
   ),

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: This suggestion provides a good defense-in-depth measure at the database level to prevent empty strings in the discovery_sources array, complementing the application-level logic.

Low
Incremental [*]
Prevent dependency directness flapping

To prevent dependency churn, confirm if github.com/cenkalti/backoff/v5 is used
directly. If so, keep it as a direct dependency by removing the // indirect
marker.

go.mod [45]

 require (
 	github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op // indirect
-	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+	github.com/cenkalti/backoff/v5 v5.0.3
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	...
 )

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies a potential dependency management issue where moving github.com/cenkalti/backoff/v5 to an indirect dependency could cause CI churn if it's used directly. This is a valid maintainability concern.

Low
  • Update

Previous suggestions

Suggestions up to commit 9ab7926
CategorySuggestion                                                                                                                                    Impact
Security
Prevent DoS from unsafe atom conversion
Suggestion Impact:Updated the component_type conversion guard from a generic non-empty binary check to an explicit allowlist (gateway/agent/checker/sync) before calling String.to_existing_atom/1, mitigating the DoS risk.

code diff:

-        # Convert component_type string to atom if needed
+        # Convert component_type string to atom if needed (allowlist prevents DoS via atom exhaustion)
         params =
           case params["component_type"] do
-            type when is_binary(type) and type != "" ->
+            type when type in ["gateway", "agent", "checker", "sync"] ->
               Map.put(params, "component_type", String.to_existing_atom(type))
 
             _ ->

Prevent a potential denial-of-service (DoS) vulnerability by validating the
component_type against an allowlist before converting it to an atom with
String.to_existing_atom/1.

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [953-961]

 # Convert component_type string to atom if needed
 params =
   case params["component_type"] do
-    type when is_binary(type) and type != "" ->
+    type when type in ["gateway", "agent", "checker", "sync"] ->
       Map.put(params, "component_type", String.to_existing_atom(type))
 
     _ ->
       params
   end
Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies a denial-of-service (DoS) vulnerability from unsafe atom creation and provides a correct fix by validating against an allowlist, which is a critical security improvement.

High
General
Improve JSON parsing and error handling
Suggestion Impact:The commit implements the "remove redundant parsing for queries_json" part by deleting the parse_json_field("queries_json", "queries") step and documenting that queries_json is not parsed here. It does not implement the suggested form error handling / function signature changes to return and update the form on JSON decode errors.

code diff:

+  # Note: queries_json is not parsed here - queries are built from form_queries assign
   defp parse_credentials_json(params) do
-    params
-    |> parse_json_field("credentials_json", "credentials")
-    |> parse_json_field("queries_json", "queries")
+    parse_json_field(params, "credentials_json", "credentials")
   end

Remove redundant parsing for queries_json and add form error handling for
invalid JSON in credentials_json to provide user feedback instead of failing
silently.

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [1362-1379]

-defp parse_credentials_json(params) do
-  params
-  |> parse_json_field("credentials_json", "credentials")
-  |> parse_json_field("queries_json", "queries")
+defp parse_credentials_json(params, form) do
+  parse_json_field(params, "credentials_json", "credentials", form)
 end
 
-defp parse_json_field(params, json_key, target_key) do
+defp parse_json_field(params, json_key, target_key, form) do
   case Map.get(params, json_key) do
     json when is_binary(json) and json != "" ->
       case Jason.decode(json) do
-        {:ok, decoded} -> Map.put(params, target_key, decoded)
-        {:error, _} -> params
+        {:ok, decoded} ->
+          {Map.put(params, target_key, decoded), form}
+
+        {:error, _} ->
+          new_form = AshPhoenix.Form.add_error(form, json_key, "is not valid JSON")
+          {params, new_form}
       end
 
     _ ->
-      params
+      {params, form}
   end
 end
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly points out redundant code and, more importantly, fixes a silent failure mode where invalid JSON for credentials is ignored, improving both code quality and user experience.

Medium
Use proper array default syntax

For Postgres array columns, use a literal empty-array fragment like
fragment("'{_}'::text[]") for the default value instead of default: [], and
enforce null: false.

elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [219-259]

 create table(:ocsf_devices, primary_key: false, prefix: prefix()) do
   ...
-  add :discovery_sources, {:array, :text}, default: []
+  add :discovery_sources, {:array, :text},
+    null: false,
+    default: fragment("'{}'::text[]")
   ...
 end
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly recommends using a database-specific fragment for array defaults to ensure correctness, which is better practice than relying on Ecto's default: [] for migrations.

Low
Use form.valid? instead of source.valid?
Suggestion Impact:The validity check in the create_package handler was changed from form.source.valid? to the public API AshPhoenix.Form.valid?(form), matching the recommendation.

code diff:

   def handle_event("create_package", %{"form" => params}, socket) do
     form = AshPhoenix.Form.validate(socket.assigns.create_form, params)
 
-    if form.source.valid? do
+    if AshPhoenix.Form.valid?(form) do

Replace form.source.valid? with the recommended AshPhoenix.Form.valid?(form) to
check for form validity.

web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [121-124]

 def handle_event("create_package", %{"form" => params}, socket) do
   form = AshPhoenix.Form.validate(socket.assigns.create_form, params)
-  if form.source.valid? do
+  if AshPhoenix.Form.valid?(form) do
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly recommends using the public API AshPhoenix.Form.valid?/1 instead of accessing the internal form.source.valid? field, which improves code robustness and maintainability.

Low
Simplify error handling for GenServer calls

Refactor the call/2 function to remove the redundant try...catch block and use
pattern matching to handle the {:error, :noproc} return value from
GenServer.call/3.

elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex [96-101]

 defp call(tenant_id, message) do
-  GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15))
-catch
-  :exit, {:noproc, _} ->
-    {:error, :engine_not_running}
+  case GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15)) do
+    {:error, :noproc} ->
+      {:error, :engine_not_running}
+
+    result ->
+      result
+  end
 end
Suggestion importance[1-10]: 4

__

Why: The suggestion is correct and improves code style by replacing a try...catch with idiomatic pattern matching on the return value of GenServer.call/3, making the code cleaner and more readable.

Low
Possible issue
Use safe integer parsing to prevent crashes
Suggestion Impact:The event handlers for remove_query, update_query, and toggle_sweep_mode were updated to parse the id using Integer.parse/1 with a case match, returning the unchanged socket on invalid input. The update_query logic was also refactored to use a helper function, but the core safety change matches the suggestion.

code diff:

-  def handle_event("remove_query", %{"id" => id}, socket) do
-    id = String.to_integer(id)
-    queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
-    # Ensure at least one query remains
-    queries = if queries == [], do: [default_query()], else: queries
-    {:noreply, assign(socket, :form_queries, queries)}
-  end
-
-  def handle_event("update_query", %{"id" => id, "field" => field, "value" => value}, socket) do
-    id = String.to_integer(id)
-
-    queries =
-      Enum.map(socket.assigns.form_queries, fn q ->
-        if q["id"] == id, do: Map.put(q, field, value), else: q
-      end)
-
-    {:noreply, assign(socket, :form_queries, queries)}
-  end
-
-  def handle_event("toggle_sweep_mode", %{"id" => id, "mode" => mode}, socket) do
-    id = String.to_integer(id)
-
-    queries =
-      Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode))
-
-    {:noreply, assign(socket, :form_queries, queries)}
+  def handle_event("remove_query", %{"id" => id_str}, socket) do
+    case Integer.parse(id_str) do
+      {id, ""} ->
+        queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
+        # Ensure at least one query remains
+        queries = if queries == [], do: [default_query()], else: queries
+        {:noreply, assign(socket, :form_queries, queries)}
+
+      _ ->
+        {:noreply, socket}
+    end
+  end
+
+  def handle_event("update_query", %{"id" => id_str, "field" => field, "value" => value}, socket) do
+    case Integer.parse(id_str) do
+      {id, ""} ->
+        queries = update_query_field(socket.assigns.form_queries, id, field, value)
+        {:noreply, assign(socket, :form_queries, queries)}
+
+      _ ->
+        {:noreply, socket}
+    end
+  end
+
+  defp update_query_field(queries, id, field, value) do
+    Enum.map(queries, fn q ->
+      if q["id"] == id, do: Map.put(q, field, value), else: q
+    end)
+  end
+
+  def handle_event("toggle_sweep_mode", %{"id" => id_str, "mode" => mode}, socket) do
+    case Integer.parse(id_str) do
+      {id, ""} ->
+        queries =
+          Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode))
+
+        {:noreply, assign(socket, :form_queries, queries)}
+
+      _ ->
+        {:noreply, socket}
+    end
   end

Replace unsafe String.to_integer/1 with Integer.parse/1 in event handlers to
prevent crashes from invalid client-side input.

web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [224-250]

-def handle_event("remove_query", %{"id" => id}, socket) do
-  id = String.to_integer(id)
-  queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
-  # Ensure at least one query remains
-  queries = if queries == [], do: [default_query()], else: queries
-  {:noreply, assign(socket, :form_queries, queries)}
+def handle_event("remove_query", %{"id" => id_str}, socket) do
+  case Integer.parse(id_str) do
+    {id, ""} ->
+      queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id))
+      # Ensure at least one query remains
+      queries = if queries == [], do: [default_query()], else: queries
+      {:noreply, assign(socket, :form_queries, queries)}
+
+    _ ->
+      {:noreply, socket}
+  end
 end
 
-def handle_event("update_query", %{"id" => id, "field" => field, "value" => value}, socket) do
-  id = String.to_integer(id)
+def handle_event("update_query", %{"id" => id_str, "field" => field, "value" => value}, socket) do
+  case Integer.parse(id_str) do
+    {id, ""} ->
+      queries =
+        Enum.map(socket.assigns.form_queries, fn q ->
+          if q["id"] == id, do: Map.put(q, field, value), else: q
+        end)
 
-  queries =
-    Enum.map(socket.assigns.form_queries, fn q ->
-      if q["id"] == id, do: Map.put(q, field, value), else: q
-    end)
+      {:noreply, assign(socket, :form_queries, queries)}
 
-  {:noreply, assign(socket, :form_queries, queries)}
+    _ ->
+      {:noreply, socket}
+  end
 end
 
-def handle_event("toggle_sweep_mode", %{"id" => id, "mode" => mode}, socket) do
-  id = String.to_integer(id)
+def handle_event("toggle_sweep_mode", %{"id" => id_str, "mode" => mode}, socket) do
+  case Integer.parse(id_str) do
+    {id, ""} ->
+      queries =
+        Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode))
 
-  queries =
-    Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode))
+      {:noreply, assign(socket, :form_queries, queries)}
 
-  {:noreply, assign(socket, :form_queries, queries)}
+    _ ->
+      {:noreply, socket}
+  end
 end
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that using String.to_integer/1 on client-provided data is unsafe and could crash the LiveView process; switching to Integer.parse/1 improves the robustness of the event handlers.

Medium
Parallelize RPC calls to prevent blocking
Suggestion Impact:The commit refactored fetch_node_info/1 to run the RPC calls concurrently via Task.async and collect results with Task.await_many into a map, then used those results instead of sequential :rpc.call invocations (with minor differences in key naming and timeout).

code diff:

   defp fetch_node_info(node) when is_atom(node) do
+    # Parallelize RPC calls to prevent blocking LiveView if node is unresponsive
     try do
-      memory = :rpc.call(node, :erlang, :memory, [], 5000)
-      {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)
+      tasks = [
+        Task.async(fn -> {:memory, :rpc.call(node, :erlang, :memory, [], 5000)} end),
+        Task.async(fn -> {:wall_clock, :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)} end),
+        Task.async(fn -> {:process_count, :rpc.call(node, :erlang, :system_info, [:process_count], 5000)} end),
+        Task.async(fn -> {:port_count, :rpc.call(node, :erlang, :system_info, [:port_count], 5000)} end),
+        Task.async(fn -> {:otp_release, :rpc.call(node, :erlang, :system_info, [:otp_release], 5000)} end),
+        Task.async(fn -> {:schedulers, :rpc.call(node, :erlang, :system_info, [:schedulers], 5000)} end),
+        Task.async(fn -> {:schedulers_online, :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000)} end)
+      ]
+
+      results = Task.await_many(tasks, 6000) |> Map.new()
+      memory = results[:memory]
+      {uptime_ms, _} = results[:wall_clock]
 
       %{
-        process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000),
-        port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000),
-        otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)),
-        schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000),
-        schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000),
+        process_count: results[:process_count],
+        port_count: results[:port_count],
+        otp_release: to_string(results[:otp_release]),
+        schedulers: results[:schedulers],
+        schedulers_online: results[:schedulers_online],
         uptime_ms: uptime_ms,

Parallelize the sequential RPC calls in fetch_node_info/1 using Task.async to
prevent the LiveView process from blocking for an extended period if the remote
node is unresponsive.

web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [164-189]

 defp fetch_node_info(node) when is_atom(node) do
+  tasks = [
+    Task.async(fn -> {:memory, :rpc.call(node, :erlang, :memory, [], 5000)} end),
+    Task.async(fn -> {:statistics, :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)} end),
+    Task.async(fn -> {:process_count, :rpc.call(node, :erlang, :system_info, [:process_count], 5000)} end),
+    Task.async(fn -> {:port_count, :rpc.call(node, :erlang, :system_info, [:port_count], 5000)} end),
+    Task.async(fn -> {:otp_release, :rpc.call(node, :erlang, :system_info, [:otp_release], 5000)} end),
+    Task.async(fn -> {:schedulers, :rpc.call(node, :erlang, :system_info, [:schedulers], 5000)} end),
+    Task.async(fn -> {:schedulers_online, :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000)} end)
+  ]
+
   try do
-    memory = :rpc.call(node, :erlang, :memory, [], 5000)
-    {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)
+    results = Task.await_many(tasks, 5500) |> Enum.into(%{})
+
+    memory = results[:memory]
+    {uptime_ms, _} = results[:statistics]
 
     %{
-      process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000),
-      port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000),
-      otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)),
-      schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000),
-      schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000),
+      process_count: results[:process_count],
+      port_count: results[:port_count],
+      otp_release: to_string(results[:otp_release]),
+      schedulers: results[:schedulers],
+      schedulers_online: results[:schedulers_online],
       uptime_ms: uptime_ms,
       memory_total: memory[:total],
       memory_processes: memory[:processes],
       memory_system: memory[:system],
       memory_atom: memory[:atom],
       memory_binary: memory[:binary],
       memory_code: memory[:code],
       memory_ets: memory[:ets]
     }
   rescue
     _ -> nil
   catch
     :exit, _ -> nil
   end
 end
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that sequential RPC calls can block the LiveView process for a long time, and proposes an idiomatic and correct solution using Task.async to improve performance and reliability.

Medium
Use correct tenant ID for queries
Suggestion Impact:Added pattern match for %{active_tenant: %{id: tid}} to ensure queries use the active tenant context when present.

code diff:

   defp load_checks_for_agent(agent_uid, current_scope) do
     tenant_id =
       case current_scope do
+        %{active_tenant: %{id: tid}} when not is_nil(tid) -> tid
         %{user: %{tenant_id: tid}} when not is_nil(tid) -> tid
         _ -> nil
       end

Update load_checks_for_agent/2 to correctly resolve the tenant_id by
prioritizing the active tenant from current_scope, ensuring data is loaded for
the correct context.

web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [191-204]

 defp load_checks_for_agent(agent_uid, current_scope) do
   tenant_id =
     case current_scope do
+      %{active_tenant: %{id: tid}} when not is_nil(tid) -> tid
       %{user: %{tenant_id: tid}} when not is_nil(tid) -> tid
       _ -> nil
     end
 
   case ServiceCheck
        |> Ash.Query.for_read(:by_agent, %{agent_uid: agent_uid})
        |> Ash.read(tenant: tenant_id, authorize?: false) do
     {:ok, checks} -> checks
     {:error, _} -> []
   end
 end
Suggestion importance[1-10]: 7

__

Why: This suggestion correctly points out an inconsistency in tenant ID resolution that could lead to loading incorrect data. The proposed change aligns the function's logic with other parts of the PR, fixing a potential bug.

Medium
Imported GitHub PR comment. Original author: @qodo-code-review[bot] Original URL: https://github.com/carverauto/serviceradar/pull/2249#issuecomment-3735386494 Original created: 2026-01-11T18:41:38Z --- ## PR Code Suggestions ✨ <!-- 4f60ff3 --> Latest suggestions up to 4f60ff3 <table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Security</td> <td> <details><summary>Avoid packaging sensitive Hex config</summary> ___ **To avoid packaging sensitive credentials, modify the <code>tar</code> command to archive only <br>the <code>.hex/cache</code> and <code>.hex/packages</code> subdirectories instead of the entire <code>~/.hex</code> <br>directory.** [scripts/update-hex-cache.sh [20-24]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-290bcddb330e1f59c144c01e1aa8dc25d5c1fe94994c5f6c8a01207f0be681c1R20-R24) ```diff echo "Regenerating hex cache tarball..." -(cd ~ && tar -czf "$REPO_ROOT/build/hex_cache.tar.gz" .hex) +mkdir -p "$REPO_ROOT/build" + +# Only package cache artifacts; avoid config/auth files under ~/.hex +(cd ~ && tar -czf "$REPO_ROOT/build/hex_cache.tar.gz" .hex/cache .hex/packages) echo "Done! Hex cache updated at build/hex_cache.tar.gz" echo "Size: $(ls -lh "$REPO_ROOT/build/hex_cache.tar.gz" | awk '{print $5}')" ``` - [ ] **Apply / Chat** <!-- /improve --apply_suggestion=0 --> <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly identifies a potential security risk of leaking sensitive credentials from the `~/.hex` directory and provides a precise fix to only archive the necessary cache directories. </details></details></td><td align=center>Medium </td></tr><tr><td rowspan=2>Possible issue</td> <td> <details><summary>✅ <s>Normalize blank discovery sources</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit adds normalization to treat `nil` and empty-string sources as "unknown" and also filters out empty strings when aggregating discovery_sources in the upsert fragment. It does not implement the suggested whitespace trimming/to_string handling, so whitespace-only values may still persist. code diff: ```diff - source = update.source || "unknown" + source = if update.source in [nil, ""], do: "unknown", else: update.source record = %{ uid: device_id, @@ -296,7 +296,7 @@ metadata: fragment("COALESCE(EXCLUDED.metadata, ?)", d.metadata), discovery_sources: fragment( - "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)", + "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')", d.discovery_sources ``` </details> ___ **Sanitize the <code>source</code> value by trimming whitespace and falling back to "unknown" <br>if it's an empty string to prevent storing blank discovery sources.** [elixir/serviceradar_core/lib/serviceradar/inventory/sync_ingestor.ex [264-279]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-fdf70a310cef758f735fae943c2a33bc7f851a1c3d1ba66499e911fd2bc5611aR264-R279) ```diff -source = update.source || "unknown" +source = + update.source + |> to_string() + |> String.trim() + |> case do + "" -> "unknown" + s -> s + end record = %{ uid: device_id, ip: update.ip, mac: update.mac, hostname: update.hostname, name: update.hostname || update.ip, is_available: update.is_available || false, metadata: update.metadata || %{}, discovery_sources: [source], first_seen_time: timestamp, last_seen_time: timestamp, created_time: timestamp, modified_time: timestamp } ``` `[Suggestion processed]` <details><summary>Suggestion importance[1-10]: 6</summary> __ Why: The suggestion correctly identifies that an empty or whitespace-only `source` string would be persisted, and proposes a robust fix to normalize it to `"unknown"`, improving data integrity. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>✅ <s>Filter empty array entries</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit updated the SQL fragment used for discovery_sources to add `AND src <> ''`, filtering out empty strings during array aggregation as suggested. It also added a related application-level guard to treat empty update.source values as "unknown". code diff: ```diff metadata: fragment("COALESCE(EXCLUDED.metadata, ?)", d.metadata), discovery_sources: fragment( - "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)", + "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')", d.discovery_sources ), ``` </details> ___ **Update the SQL fragment to filter out empty strings in addition to <code>NULL</code> values <br>when merging <code>discovery_sources</code> to ensure data cleanliness.** [elixir/serviceradar_core/lib/serviceradar/inventory/sync_ingestor.ex [297-301]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-fdf70a310cef758f735fae943c2a33bc7f851a1c3d1ba66499e911fd2bc5611aR297-R301) ```diff discovery_sources: fragment( - "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL)", + "(SELECT array_agg(DISTINCT src) FROM unnest(array_cat(COALESCE(?, ARRAY[]::text[]), EXCLUDED.discovery_sources)) AS src WHERE src IS NOT NULL AND src <> '')", d.discovery_sources ), ``` `[Suggestion processed]` <details><summary>Suggestion importance[1-10]: 6</summary> __ Why: This suggestion provides a good defense-in-depth measure at the database level to prevent empty strings in the `discovery_sources` array, complementing the application-level logic. </details></details></td><td align=center>Low </td></tr><tr><td rowspan=1>Incremental <sup><a href='https://qodo-merge-docs.qodo.ai/core-abilities/incremental_update/'>[*]</a></sup></td> <td> <details><summary>Prevent dependency directness flapping</summary> ___ **To prevent dependency churn, confirm if <code>github.com/cenkalti/backoff/v5</code> is used <br>directly. If so, keep it as a direct dependency by removing the <code>// indirect</code> <br>marker.** [go.mod [45]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-33ef32bf6c23acb95f5902d7097b7a1d5128ca061167ec0716715b0b9eeaa5f6R45-R45) ```diff require ( github.com/antithesishq/antithesis-sdk-go v0.4.3-default-no-op // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 github.com/cespare/xxhash/v2 v2.3.0 // indirect ... ) ``` `[To ensure code accuracy, apply this suggestion manually]` <details><summary>Suggestion importance[1-10]: 5</summary> __ Why: The suggestion correctly identifies a potential dependency management issue where moving `github.com/cenkalti/backoff/v5` to an indirect dependency could cause CI churn if it's used directly. This is a valid maintainability concern. </details></details></td><td align=center>Low </td></tr> <tr><td align="center" colspan="2"> - [ ] Update <!-- /improve_multi --more_suggestions=true --> </td><td></td></tr></tbody></table> ___ #### Previous suggestions <details><summary>✅ Suggestions up to commit 9ab7926</summary> <br><table><thead><tr><td><strong>Category</strong></td><td align=left><strong>Suggestion&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; </strong></td><td align=center><strong>Impact</strong></td></tr><tbody><tr><td rowspan=1>Security</td> <td> <details><summary>✅ <s>Prevent DoS from unsafe atom conversion</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>Updated the component_type conversion guard from a generic non-empty binary check to an explicit allowlist (gateway/agent/checker/sync) before calling String.to_existing_atom/1, mitigating the DoS risk. code diff: ```diff - # Convert component_type string to atom if needed + # Convert component_type string to atom if needed (allowlist prevents DoS via atom exhaustion) params = case params["component_type"] do - type when is_binary(type) and type != "" -> + type when type in ["gateway", "agent", "checker", "sync"] -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> ``` </details> ___ **Prevent a potential denial-of-service (DoS) vulnerability by validating the <br><code>component_type</code> against an allowlist before converting it to an atom with <br><code>String.to_existing_atom/1</code>.** [web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [953-961]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR953-R961) ```diff # Convert component_type string to atom if needed params = case params["component_type"] do - type when is_binary(type) and type != "" -> + type when type in ["gateway", "agent", "checker", "sync"] -> Map.put(params, "component_type", String.to_existing_atom(type)) _ -> params end ``` <details><summary>Suggestion importance[1-10]: 9</summary> __ Why: This suggestion correctly identifies a denial-of-service (DoS) vulnerability from unsafe atom creation and provides a correct fix by validating against an allowlist, which is a critical security improvement. </details></details></td><td align=center>High </td></tr><tr><td rowspan=4>General</td> <td> <details><summary>✅ <s>Improve JSON parsing and error handling</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit implements the "remove redundant parsing for queries_json" part by deleting the parse_json_field("queries_json", "queries") step and documenting that queries_json is not parsed here. It does not implement the suggested form error handling / function signature changes to return and update the form on JSON decode errors. code diff: ```diff + # Note: queries_json is not parsed here - queries are built from form_queries assign defp parse_credentials_json(params) do - params - |> parse_json_field("credentials_json", "credentials") - |> parse_json_field("queries_json", "queries") + parse_json_field(params, "credentials_json", "credentials") end ``` </details> ___ **Remove redundant parsing for <code>queries_json</code> and add form error handling for <br>invalid JSON in <code>credentials_json</code> to provide user feedback instead of failing <br>silently.** [web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [1362-1379]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cdR1362-R1379) ```diff -defp parse_credentials_json(params) do - params - |> parse_json_field("credentials_json", "credentials") - |> parse_json_field("queries_json", "queries") +defp parse_credentials_json(params, form) do + parse_json_field(params, "credentials_json", "credentials", form) end -defp parse_json_field(params, json_key, target_key) do +defp parse_json_field(params, json_key, target_key, form) do case Map.get(params, json_key) do json when is_binary(json) and json != "" -> case Jason.decode(json) do - {:ok, decoded} -> Map.put(params, target_key, decoded) - {:error, _} -> params + {:ok, decoded} -> + {Map.put(params, target_key, decoded), form} + + {:error, _} -> + new_form = AshPhoenix.Form.add_error(form, json_key, "is not valid JSON") + {params, new_form} end _ -> - params + {params, form} end end ``` <details><summary>Suggestion importance[1-10]: 8</summary> __ Why: The suggestion correctly points out redundant code and, more importantly, fixes a silent failure mode where invalid JSON for credentials is ignored, improving both code quality and user experience. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>Use proper array default syntax</summary> ___ **For Postgres array columns, use a literal empty-array fragment like <br><code>fragment("'{_}'::text[]")</code> for the default value instead of <code>default: []</code>, and <br>enforce <code>null: false</code>.** [elixir/serviceradar_core/priv/repo/tenant_migrations/20260107043446_initial_schema.exs [219-259]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-0d217dc9822fab0d3390e8ec21040f98e67106e5c9126e043a9b701efcbfb576R219-R259) ```diff create table(:ocsf_devices, primary_key: false, prefix: prefix()) do ... - add :discovery_sources, {:array, :text}, default: [] + add :discovery_sources, {:array, :text}, + null: false, + default: fragment("'{}'::text[]") ... end ``` <details><summary>Suggestion importance[1-10]: 5</summary> __ Why: The suggestion correctly recommends using a database-specific `fragment` for array defaults to ensure correctness, which is better practice than relying on Ecto's `default: []` for migrations. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>✅ <s>Use form.valid? instead of source.valid?</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The validity check in the create_package handler was changed from form.source.valid? to the public API AshPhoenix.Form.valid?(form), matching the recommendation. code diff: ```diff def handle_event("create_package", %{"form" => params}, socket) do form = AshPhoenix.Form.validate(socket.assigns.create_form, params) - if form.source.valid? do + if AshPhoenix.Form.valid?(form) do ``` </details> ___ **Replace <code>form.source.valid?</code> with the recommended <code>AshPhoenix.Form.valid?(form)</code> to <br>check for form validity.** [web-ng/lib/serviceradar_web_ng_web/live/admin/edge_package_live/index.ex [121-124]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-d0d22a987aa942ef25088696daea862e36bd8ca083ce29e4670f63e62947c62fR121-R124) ```diff def handle_event("create_package", %{"form" => params}, socket) do form = AshPhoenix.Form.validate(socket.assigns.create_form, params) - if form.source.valid? do + if AshPhoenix.Form.valid?(form) do … ``` <details><summary>Suggestion importance[1-10]: 5</summary> __ Why: The suggestion correctly recommends using the public API `AshPhoenix.Form.valid?/1` instead of accessing the internal `form.source.valid?` field, which improves code robustness and maintainability. </details></details></td><td align=center>Low </td></tr><tr><td> <details><summary>Simplify error handling for GenServer calls</summary> ___ **Refactor the <code>call/2</code> function to remove the redundant <code>try...catch</code> block and use <br>pattern matching to handle the <code>{:error, :noproc}</code> return value from <br><code>GenServer.call/3</code>.** [elixir/serviceradar_core/lib/serviceradar/observability/stateful_alert_engine.ex [96-101]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-bae3a52db882de8c947e62f219a95dff8db4e155e37d9a361dbe14ec25fcd3bdR96-R101) ```diff defp call(tenant_id, message) do - GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15)) -catch - :exit, {:noproc, _} -> - {:error, :engine_not_running} + case GenServer.call(via_tuple(tenant_id), message, :timer.seconds(15)) do + {:error, :noproc} -> + {:error, :engine_not_running} + + result -> + result + end end ``` <details><summary>Suggestion importance[1-10]: 4</summary> __ Why: The suggestion is correct and improves code style by replacing a `try...catch` with idiomatic pattern matching on the return value of `GenServer.call/3`, making the code cleaner and more readable. </details></details></td><td align=center>Low </td></tr><tr><td rowspan=3>Possible issue</td> <td> <details><summary>✅ <s>Use safe integer parsing to prevent crashes</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The event handlers for remove_query, update_query, and toggle_sweep_mode were updated to parse the id using Integer.parse/1 with a case match, returning the unchanged socket on invalid input. The update_query logic was also refactored to use a helper function, but the core safety change matches the suggestion. code diff: ```diff - def handle_event("remove_query", %{"id" => id}, socket) do - id = String.to_integer(id) - queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) - # Ensure at least one query remains - queries = if queries == [], do: [default_query()], else: queries - {:noreply, assign(socket, :form_queries, queries)} - end - - def handle_event("update_query", %{"id" => id, "field" => field, "value" => value}, socket) do - id = String.to_integer(id) - - queries = - Enum.map(socket.assigns.form_queries, fn q -> - if q["id"] == id, do: Map.put(q, field, value), else: q - end) - - {:noreply, assign(socket, :form_queries, queries)} - end - - def handle_event("toggle_sweep_mode", %{"id" => id, "mode" => mode}, socket) do - id = String.to_integer(id) - - queries = - Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode)) - - {:noreply, assign(socket, :form_queries, queries)} + def handle_event("remove_query", %{"id" => id_str}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) + # Ensure at least one query remains + queries = if queries == [], do: [default_query()], else: queries + {:noreply, assign(socket, :form_queries, queries)} + + _ -> + {:noreply, socket} + end + end + + def handle_event("update_query", %{"id" => id_str, "field" => field, "value" => value}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = update_query_field(socket.assigns.form_queries, id, field, value) + {:noreply, assign(socket, :form_queries, queries)} + + _ -> + {:noreply, socket} + end + end + + defp update_query_field(queries, id, field, value) do + Enum.map(queries, fn q -> + if q["id"] == id, do: Map.put(q, field, value), else: q + end) + end + + def handle_event("toggle_sweep_mode", %{"id" => id_str, "mode" => mode}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = + Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode)) + + {:noreply, assign(socket, :form_queries, queries)} + + _ -> + {:noreply, socket} + end end ``` </details> ___ **Replace unsafe <code>String.to_integer/1</code> with <code>Integer.parse/1</code> in event handlers to <br>prevent crashes from invalid client-side input.** [web-ng/lib/serviceradar_web_ng_web/live/admin/integration_live/index.ex [224-250]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-61d0262af13a42905ebbd793e83537e61bfc09493df1664a82eb2536980ee1cdR224-R250) ```diff -def handle_event("remove_query", %{"id" => id}, socket) do - id = String.to_integer(id) - queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) - # Ensure at least one query remains - queries = if queries == [], do: [default_query()], else: queries - {:noreply, assign(socket, :form_queries, queries)} +def handle_event("remove_query", %{"id" => id_str}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = Enum.reject(socket.assigns.form_queries, &(&1["id"] == id)) + # Ensure at least one query remains + queries = if queries == [], do: [default_query()], else: queries + {:noreply, assign(socket, :form_queries, queries)} + + _ -> + {:noreply, socket} + end end -def handle_event("update_query", %{"id" => id, "field" => field, "value" => value}, socket) do - id = String.to_integer(id) +def handle_event("update_query", %{"id" => id_str, "field" => field, "value" => value}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = + Enum.map(socket.assigns.form_queries, fn q -> + if q["id"] == id, do: Map.put(q, field, value), else: q + end) - queries = - Enum.map(socket.assigns.form_queries, fn q -> - if q["id"] == id, do: Map.put(q, field, value), else: q - end) + {:noreply, assign(socket, :form_queries, queries)} - {:noreply, assign(socket, :form_queries, queries)} + _ -> + {:noreply, socket} + end end -def handle_event("toggle_sweep_mode", %{"id" => id, "mode" => mode}, socket) do - id = String.to_integer(id) +def handle_event("toggle_sweep_mode", %{"id" => id_str, "mode" => mode}, socket) do + case Integer.parse(id_str) do + {id, ""} -> + queries = + Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode)) - queries = - Enum.map(socket.assigns.form_queries, &toggle_sweep_mode_for_query(&1, id, mode)) + {:noreply, assign(socket, :form_queries, queries)} - {:noreply, assign(socket, :form_queries, queries)} + _ -> + {:noreply, socket} + end end ``` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies that using `String.to_integer/1` on client-provided data is unsafe and could crash the LiveView process; switching to `Integer.parse/1` improves the robustness of the event handlers. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>✅ <s>Parallelize RPC calls to prevent blocking</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>The commit refactored fetch_node_info/1 to run the RPC calls concurrently via Task.async and collect results with Task.await_many into a map, then used those results instead of sequential :rpc.call invocations (with minor differences in key naming and timeout). code diff: ```diff defp fetch_node_info(node) when is_atom(node) do + # Parallelize RPC calls to prevent blocking LiveView if node is unresponsive try do - memory = :rpc.call(node, :erlang, :memory, [], 5000) - {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000) + tasks = [ + Task.async(fn -> {:memory, :rpc.call(node, :erlang, :memory, [], 5000)} end), + Task.async(fn -> {:wall_clock, :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)} end), + Task.async(fn -> {:process_count, :rpc.call(node, :erlang, :system_info, [:process_count], 5000)} end), + Task.async(fn -> {:port_count, :rpc.call(node, :erlang, :system_info, [:port_count], 5000)} end), + Task.async(fn -> {:otp_release, :rpc.call(node, :erlang, :system_info, [:otp_release], 5000)} end), + Task.async(fn -> {:schedulers, :rpc.call(node, :erlang, :system_info, [:schedulers], 5000)} end), + Task.async(fn -> {:schedulers_online, :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000)} end) + ] + + results = Task.await_many(tasks, 6000) |> Map.new() + memory = results[:memory] + {uptime_ms, _} = results[:wall_clock] %{ - process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000), - port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000), - otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)), - schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000), - schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000), + process_count: results[:process_count], + port_count: results[:port_count], + otp_release: to_string(results[:otp_release]), + schedulers: results[:schedulers], + schedulers_online: results[:schedulers_online], uptime_ms: uptime_ms, ``` </details> ___ **Parallelize the sequential RPC calls in <code>fetch_node_info/1</code> using <code>Task.async</code> to <br>prevent the LiveView process from blocking for an extended period if the remote <br>node is unresponsive.** [web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [164-189]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-5e622205d1abddd8ad7dcf7a8ca1be583804d622d3d38b75140e9b909cf0534aR164-R189) ```diff defp fetch_node_info(node) when is_atom(node) do + tasks = [ + Task.async(fn -> {:memory, :rpc.call(node, :erlang, :memory, [], 5000)} end), + Task.async(fn -> {:statistics, :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000)} end), + Task.async(fn -> {:process_count, :rpc.call(node, :erlang, :system_info, [:process_count], 5000)} end), + Task.async(fn -> {:port_count, :rpc.call(node, :erlang, :system_info, [:port_count], 5000)} end), + Task.async(fn -> {:otp_release, :rpc.call(node, :erlang, :system_info, [:otp_release], 5000)} end), + Task.async(fn -> {:schedulers, :rpc.call(node, :erlang, :system_info, [:schedulers], 5000)} end), + Task.async(fn -> {:schedulers_online, :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000)} end) + ] + try do - memory = :rpc.call(node, :erlang, :memory, [], 5000) - {uptime_ms, _} = :rpc.call(node, :erlang, :statistics, [:wall_clock], 5000) + results = Task.await_many(tasks, 5500) |> Enum.into(%{}) + + memory = results[:memory] + {uptime_ms, _} = results[:statistics] %{ - process_count: :rpc.call(node, :erlang, :system_info, [:process_count], 5000), - port_count: :rpc.call(node, :erlang, :system_info, [:port_count], 5000), - otp_release: to_string(:rpc.call(node, :erlang, :system_info, [:otp_release], 5000)), - schedulers: :rpc.call(node, :erlang, :system_info, [:schedulers], 5000), - schedulers_online: :rpc.call(node, :erlang, :system_info, [:schedulers_online], 5000), + process_count: results[:process_count], + port_count: results[:port_count], + otp_release: to_string(results[:otp_release]), + schedulers: results[:schedulers], + schedulers_online: results[:schedulers_online], uptime_ms: uptime_ms, memory_total: memory[:total], memory_processes: memory[:processes], memory_system: memory[:system], memory_atom: memory[:atom], memory_binary: memory[:binary], memory_code: memory[:code], memory_ets: memory[:ets] } rescue _ -> nil catch :exit, _ -> nil end end ``` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: The suggestion correctly identifies that sequential RPC calls can block the LiveView process for a long time, and proposes an idiomatic and correct solution using `Task.async` to improve performance and reliability. </details></details></td><td align=center>Medium </td></tr><tr><td> <details><summary>✅ <s>Use correct tenant ID for queries</s></summary> ___ <details><summary><b>Suggestion Impact:</b></summary>Added pattern match for %{active_tenant: %{id: tid}} to ensure queries use the active tenant context when present. code diff: ```diff defp load_checks_for_agent(agent_uid, current_scope) do tenant_id = case current_scope do + %{active_tenant: %{id: tid}} when not is_nil(tid) -> tid %{user: %{tenant_id: tid}} when not is_nil(tid) -> tid _ -> nil end ``` </details> ___ **Update <code>load_checks_for_agent/2</code> to correctly resolve the <code>tenant_id</code> by <br>prioritizing the active tenant from <code>current_scope</code>, ensuring data is loaded for <br>the correct context.** [web-ng/lib/serviceradar_web_ng_web/live/agent_live/show.ex [191-204]](https://github.com/carverauto/serviceradar/pull/2249/files#diff-5e622205d1abddd8ad7dcf7a8ca1be583804d622d3d38b75140e9b909cf0534aR191-R204) ```diff defp load_checks_for_agent(agent_uid, current_scope) do tenant_id = case current_scope do + %{active_tenant: %{id: tid}} when not is_nil(tid) -> tid %{user: %{tenant_id: tid}} when not is_nil(tid) -> tid _ -> nil end case ServiceCheck |> Ash.Query.for_read(:by_agent, %{agent_uid: agent_uid}) |> Ash.read(tenant: tenant_id, authorize?: false) do {:ok, checks} -> checks {:error, _} -> [] end end ``` <details><summary>Suggestion importance[1-10]: 7</summary> __ Why: This suggestion correctly points out an inconsistency in tenant ID resolution that could lead to loading incorrect data. The proposed change aligns the function's logic with other parts of the PR, fixing a potential bug. </details></details></td><td align=center>Medium </td></tr> <tr><td align="center" colspan="2"> <!-- /improve_multi --more_suggestions=true --> </td><td></td></tr></tbody></table> </details>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
carverauto/serviceradar!2650
No description provided.