- 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
346 lines
16 KiB
Text
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>
|