585 lines
28 KiB
Text
585 lines
28 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="settings"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.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-5">
|
|
<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 class="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-600/30 dark:bg-purple-900/20">
|
|
<div class="text-2xl font-bold text-purple-800 dark:text-purple-400">
|
|
{@report.summary.ghost_count}
|
|
</div>
|
|
<div class="text-sm text-purple-700 dark:text-purple-500">Ghosts</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 mb-6 flex items-center justify-between">
|
|
<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 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>
|
|
<.link
|
|
patch={
|
|
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=ghosts"
|
|
}
|
|
class={[
|
|
"px-4 py-2 text-sm font-medium rounded-r-lg border-l border-gray-200 dark:border-white/10",
|
|
@active_tab == "ghosts" &&
|
|
"bg-blue-600 text-white dark:bg-blue-500",
|
|
@active_tab != "ghosts" &&
|
|
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
]}
|
|
>
|
|
Ghosts ({@report.summary.ghost_count})
|
|
</.link>
|
|
</div>
|
|
|
|
<button
|
|
phx-click="refresh"
|
|
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-4 w-4" />
|
|
{t("Refresh")}
|
|
</button>
|
|
</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="mb-4 flex items-center gap-3">
|
|
<button
|
|
phx-click="auto_match"
|
|
class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
|
>
|
|
<.icon name="hero-bolt" class="h-4 w-4" />
|
|
{t("Auto-Match All")}
|
|
</button>
|
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
|
{t("Links by MAC, IP, or site assignment.")}
|
|
</span>
|
|
</div>
|
|
<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>
|
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
|
{t("Actions")}
|
|
</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">
|
|
<a
|
|
href={"http://#{entry.device.ip_address}"}
|
|
target="_blank"
|
|
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
{entry.device.ip_address}
|
|
</a>
|
|
</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>
|
|
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
|
|
<%= cond do %>
|
|
<% @open_create_for == entry.device.id -> %>
|
|
<div class="flex items-center justify-end gap-2">
|
|
<form
|
|
id={"select-manufacturer-form-#{entry.device.id}"}
|
|
phx-change="select_manufacturer"
|
|
>
|
|
<select
|
|
name="manufacturer_id"
|
|
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
|
|
>
|
|
<option value="">{t("Select manufacturer")}</option>
|
|
<%= for mfr <- @manufacturers do %>
|
|
<option
|
|
value={mfr["id"]}
|
|
selected={@selected_manufacturer == mfr["id"]}
|
|
>
|
|
{mfr["name"]}
|
|
</option>
|
|
<% end %>
|
|
</select>
|
|
</form>
|
|
<%= if @selected_manufacturer do %>
|
|
<form
|
|
id={"create-gaiia-item-form-#{entry.device.id}"}
|
|
phx-submit="create_gaiia_item"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<input type="hidden" name="device_id" value={entry.device.id} />
|
|
<select
|
|
name="model_id"
|
|
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
|
|
>
|
|
<option value="">{t("Select model")}</option>
|
|
<%= for model <- Map.get(@models, @selected_manufacturer, []) do %>
|
|
<option value={model["id"]}>{model["name"]}</option>
|
|
<% end %>
|
|
</select>
|
|
<button class="rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700">
|
|
{t("Create")}
|
|
</button>
|
|
</form>
|
|
<% end %>
|
|
<button
|
|
phx-click="cancel_create"
|
|
class="rounded px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
</div>
|
|
<% @open_link_for == entry.device.id -> %>
|
|
<div class="flex items-center justify-end gap-2">
|
|
<form
|
|
id={"search-gaiia-items-form-#{entry.device.id}"}
|
|
phx-change="search_gaiia_items"
|
|
class="flex items-center gap-2"
|
|
>
|
|
<input type="hidden" name="device_id" value={entry.device.id} />
|
|
<input
|
|
type="text"
|
|
name="query"
|
|
value={@link_search_query}
|
|
placeholder={t("Search by name or IP...")}
|
|
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
|
|
autocomplete="off"
|
|
/>
|
|
</form>
|
|
<button
|
|
phx-click="cancel_link_search"
|
|
class="rounded px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
|
|
>
|
|
{t("Cancel")}
|
|
</button>
|
|
</div>
|
|
<% true -> %>
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button
|
|
phx-click="start_link_search"
|
|
phx-value-device_id={entry.device.id}
|
|
class="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
>
|
|
{t("Link")}
|
|
</button>
|
|
<button
|
|
phx-click="open_create_form"
|
|
phx-value-device_id={entry.device.id}
|
|
class="inline-flex items-center gap-1 rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700"
|
|
>
|
|
{t("Create in Gaiia")}
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<%!-- Search results row for link search --%>
|
|
<%= if @open_link_for == entry.device.id and @link_search_results != [] do %>
|
|
<tr>
|
|
<td colspan="4" class="px-4 py-2 bg-gray-50 dark:bg-gray-800/50">
|
|
<div class="space-y-1">
|
|
<%= for item <- @link_search_results do %>
|
|
<div class="flex items-center justify-between rounded border border-gray-200 bg-white px-3 py-2 dark:border-white/10 dark:bg-gray-800">
|
|
<div>
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{item.name}
|
|
</span>
|
|
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
|
{item.ip_address || "—"}
|
|
</span>
|
|
</div>
|
|
<button
|
|
phx-click="link_to_gaiia"
|
|
phx-value-device_id={entry.device.id}
|
|
phx-value-gaiia_id={item.gaiia_id}
|
|
class="rounded bg-indigo-600 px-2 py-1 text-xs font-medium text-white hover:bg-indigo-700"
|
|
>
|
|
{t("Link")}
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
<% 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>
|
|
<% "ghosts" -> %>
|
|
<div class="space-y-2">
|
|
<%= if @report.ghost_devices == [] do %>
|
|
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
|
{t("No stale mappings — all linked Gaiia items reference existing Towerops devices.")}
|
|
</p>
|
|
<% else %>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
{t(
|
|
"These Gaiia inventory items are linked to Towerops devices that no longer exist. Unlink them to restore them to the unmapped pool."
|
|
)}
|
|
</p>
|
|
<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("Status")}
|
|
</th>
|
|
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
|
{t("Actions")}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
|
|
<%= for entry <- @report.ghost_devices 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.status || "—"}
|
|
</td>
|
|
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
|
|
<button
|
|
phx-click="unlink_ghost"
|
|
phx-value-gaiia_id={entry.inventory_item.gaiia_id}
|
|
class="rounded bg-purple-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-purple-700"
|
|
>
|
|
{t("Unlink")}
|
|
</button>
|
|
</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>
|
|
<div class="flex justify-between text-sm">
|
|
<dt class="text-gray-600 dark:text-gray-400">
|
|
{t("Ghost mappings (stale device links)")}
|
|
</dt>
|
|
<dd class="font-medium text-purple-600 dark:text-purple-400">
|
|
{@report.summary.ghost_count}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</Layouts.authenticated>
|