towerops/lib/towerops_web/live/org/gaiia_reconciliation_live.html.heex
Graham McIntire ecc69b4e96
ux: improve navigation, tooltips, and discoverability across pages
- Replace "Back to X" links with proper breadcrumb trails on settings,
  integrations, Gaiia, and Preseem pages
- Fix active_page on config-timeline routes so Devices nav highlights
- Add title tooltip to truncated alert messages (hover to see full text)
- Add tooltips to QoE column header explaining the metric
- Add tooltips to Subs/MRR labels on dashboard status bar
2026-03-12 13:40:10 -05:00

346 lines
16 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="settings"
>
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings", navigate: ~p"/orgs/#{@organization.slug}/settings"},
%{label: "Integrations", navigate: ~p"/orgs/#{@organization.slug}/settings/integrations"},
%{label: "Gaiia"}
]} />
<.header>
{t("Gaiia Inventory Reconciliation")}
<:subtitle>
{t(
"Compare Gaiia inventory against Towerops device discovery. Reconciliation runs nightly."
)}
</:subtitle>
</.header>
<%!-- Sub-navigation tabs --%>
<div class="mt-4 border-b border-gray-200 dark:border-white/10">
<nav class="flex space-x-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
{t("Entity Mapping")}
</.link>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
{t("Reconciliation")}
</.link>
</nav>
</div>
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-gray-900 dark:text-white">
{@report.summary.total_mapped}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Mapped</div>
</div>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-600/30 dark:bg-yellow-900/20">
<div class="text-2xl font-bold text-yellow-800 dark:text-yellow-400">
{@report.summary.missing_mapping_count}
</div>
<div class="text-sm text-yellow-700 dark:text-yellow-500">Unmapped Items</div>
</div>
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-600/30 dark:bg-blue-900/20">
<div class="text-2xl font-bold text-blue-800 dark:text-blue-400">
{@report.summary.untracked_count}
</div>
<div class="text-sm text-blue-700 dark:text-blue-500">Untracked Devices</div>
</div>
<div class="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-600/30 dark:bg-red-900/20">
<div class="text-2xl font-bold text-red-800 dark:text-red-400">
{@report.summary.mismatch_count}
</div>
<div class="text-sm text-red-700 dark:text-red-500">Data Mismatches</div>
</div>
</div>
<div class="mt-6 mb-6">
<div class="inline-flex rounded-lg border border-gray-200 dark:border-white/10">
<.link
patch={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=summary"
}
class={[
"px-4 py-2 text-sm font-medium rounded-l-lg",
@active_tab == "summary" &&
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "summary" &&
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
{t("Summary")}
</.link>
<.link
patch={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=untracked"
}
class={[
"px-4 py-2 text-sm font-medium border-l border-gray-200 dark:border-white/10",
@active_tab == "untracked" &&
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "untracked" &&
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Untracked ({@report.summary.untracked_count})
</.link>
<.link
patch={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=mismatches"
}
class={[
"px-4 py-2 text-sm font-medium border-l border-gray-200 dark:border-white/10",
@active_tab == "mismatches" &&
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "mismatches" &&
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Mismatches ({@report.summary.mismatch_count})
</.link>
<.link
patch={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=unmapped"
}
class={[
"px-4 py-2 text-sm font-medium rounded-r-lg border-l border-gray-200 dark:border-white/10",
@active_tab == "unmapped" &&
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "unmapped" &&
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Unmapped ({@report.summary.missing_mapping_count})
</.link>
</div>
</div>
<%= case @active_tab do %>
<% "untracked" -> %>
<div class="space-y-2">
<%= if @report.untracked_devices == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("All Towerops devices have matching Gaiia inventory items.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Site")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for entry <- @report.untracked_devices do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{entry.device.id}"}
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{entry.device.name}
</.link>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.device.ip_address}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<%= if entry.device.site do %>
{entry.device.site.name}
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
<% "mismatches" -> %>
<div class="space-y-2">
<%= if @report.data_mismatches == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("No data mismatches found between mapped devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Field")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Towerops")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Gaiia")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for mismatch <- @report.data_mismatches do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{mismatch.device.id}"}
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{mismatch.device.name}
</.link>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{mismatch.field}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-900 dark:text-white">
{mismatch.towerops_value}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-red-600 dark:text-red-400">
{mismatch.gaiia_value}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
<% "unmapped" -> %>
<div class="space-y-2">
<%= if @report.missing_mappings == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("All Gaiia inventory items are mapped to Towerops devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Gaiia Item")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Category")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Status")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for entry <- @report.missing_mappings do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{entry.inventory_item.name}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-600 dark:text-gray-400">
{entry.inventory_item.ip_address || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.inventory_item.category || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.inventory_item.status || "—"}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
<% _ -> %>
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
Last reconciled: {ToweropsWeb.TimeHelpers.format_datetime(
@report.reconciled_at,
@current_scope.timezone,
@current_scope.time_format
)}
</p>
<%= if @report.summary.total_mapped == 0 && @report.summary.missing_mapping_count == 0 && @report.summary.untracked_count == 0 do %>
<div class="py-8 text-center">
<.icon
name="hero-check-circle"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No inventory data")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Sync Gaiia data and map inventory items to see reconciliation results.")}
</p>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="mt-4 inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Go to Entity Mapping <.icon name="hero-arrow-right" class="h-4 w-4" />
</.link>
</div>
<% else %>
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-gray-800/50">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Overview")}</h3>
<dl class="mt-4 space-y-3">
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
Mapped devices (Gaiia ↔ Towerops)
</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{@report.summary.total_mapped}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
{t("Unmapped Gaiia items (no Towerops link)")}
</dt>
<dd class="font-medium text-yellow-600 dark:text-yellow-400">
{@report.summary.missing_mapping_count}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
{t("Untracked Towerops devices (not in Gaiia)")}
</dt>
<dd class="font-medium text-blue-600 dark:text-blue-400">
{@report.summary.untracked_count}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
{t("Data mismatches (IP, serial, etc.)")}
</dt>
<dd class="font-medium text-red-600 dark:text-red-400">
{@report.summary.mismatch_count}
</dd>
</div>
</dl>
</div>
<% end %>
</div>
<% end %>
</Layouts.authenticated>