feat: actionable insights with fix-it buttons, proper pluralization via gettext

- Gaiia 'not tracked' insight: Map to Gaiia inventory + View reconciliation buttons
- Gaiia 'ghost' insight: Clean up stale mappings button
- Gaiia 'mismatch' insight: Review mismatches button
- Fixed device(s) → proper ngettext pluralization
- Fixed Towerops → TowerOps capitalization
This commit is contained in:
Graham McIntire 2026-02-14 14:46:19 -06:00
parent 44e9d20d35
commit d1cd608ae9
2 changed files with 75 additions and 9 deletions

View file

@ -7,6 +7,8 @@ defmodule Towerops.Workers.GaiiaInsightWorker do
"""
use Oban.Worker, queue: :maintenance
use Gettext, backend: ToweropsWeb.Gettext
alias Towerops.Gaiia.Reconciliation
alias Towerops.Integrations
alias Towerops.Preseem.Insights
@ -41,10 +43,9 @@ defmodule Towerops.Workers.GaiiaInsightWorker do
channel: "passive",
source: "gaiia",
dedup_key: "untracked",
title: "#{count} device(s) not tracked in Gaiia",
title: "#{count} #{ngettext("device", "devices", count)} not tracked in Gaiia",
description:
"#{count} device(s) discovered by Towerops have no matching Gaiia inventory item. " <>
"Consider adding them to Gaiia for complete subscriber impact tracking.",
"#{count} #{ngettext("device", "devices", count)} discovered by TowerOps #{ngettext("has", "have", count)} no matching Gaiia inventory item.",
metadata: %{
"dedup_key" => "untracked",
"finding_type" => "untracked",
@ -67,10 +68,9 @@ defmodule Towerops.Workers.GaiiaInsightWorker do
channel: "proactive",
source: "gaiia",
dedup_key: "ghost",
title: "#{count} Gaiia item(s) reference missing devices",
title: "#{count} Gaiia #{ngettext("item references", "items reference", count)} missing devices",
description:
"#{count} Gaiia inventory item(s) are mapped to devices that no longer exist in Towerops. " <>
"Review and clean up stale mappings.",
"#{count} Gaiia inventory #{ngettext("item is", "items are", count)} mapped to devices that no longer exist in TowerOps.",
metadata: %{
"dedup_key" => "ghost",
"finding_type" => "ghost",
@ -91,10 +91,9 @@ defmodule Towerops.Workers.GaiiaInsightWorker do
channel: "passive",
source: "gaiia",
dedup_key: "mismatch",
title: "#{count} data mismatch(es) between Towerops and Gaiia",
title: "#{count} data #{ngettext("mismatch", "mismatches", count)} between TowerOps and Gaiia",
description:
"#{count} device(s) have mismatched data between Towerops and Gaiia inventory. " <>
"Review IP addresses and other fields for accuracy.",
"#{count} #{ngettext("device has", "devices have", count)} mismatched data between TowerOps and Gaiia inventory.",
metadata: %{
"dedup_key" => "mismatch",
"finding_type" => "mismatch",

View file

@ -306,6 +306,73 @@
</div>
<% end %>
<%!-- Actionable recommendations --%>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "untracked" do %>
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
</p>
<div class="flex flex-wrap gap-2">
<.link
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
}
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-blue-700"
>
<.icon name="hero-arrows-right-left" class="h-3.5 w-3.5" />
Map to Gaiia inventory
</.link>
<.link
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
}
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
>
<.icon name="hero-table-cells" class="h-3.5 w-3.5" /> View reconciliation
</.link>
</div>
</div>
<% end %>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "ghost" do %>
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<p class="text-xs font-medium text-amber-800 dark:text-amber-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
</p>
<div class="flex flex-wrap gap-2">
<.link
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
}
class="inline-flex items-center gap-1 rounded-md bg-amber-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-amber-700"
>
<.icon name="hero-trash" class="h-3.5 w-3.5" /> Clean up stale mappings
</.link>
</div>
</div>
<% end %>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "mismatch" do %>
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
</p>
<div class="flex flex-wrap gap-2">
<.link
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
}
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-blue-700"
>
<.icon name="hero-table-cells" class="h-3.5 w-3.5" /> Review mismatches
</.link>
</div>
</div>
<% end %>
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<%!-- Access point --%>
<%= if insight.preseem_access_point do %>