ux: improve mobile responsiveness across all pages
- Hide less-critical table columns on small screens (IP, subs, MRR, etc.) - Add overflow-x-auto wrappers on tables to prevent horizontal scroll - Fix <.header> component to flex-wrap action buttons on mobile - Make page action button labels icon-only on small screens - Add min-w-0 on grid children to prevent viewport overflow - Right-align device detail dt/dd values consistently - Fix schedule timeline cards with overflow-x-auto wrappers
This commit is contained in:
parent
05d79287bc
commit
19abd5508a
11 changed files with 538 additions and 470 deletions
|
|
@ -1,3 +1,22 @@
|
||||||
|
2026-03-12
|
||||||
|
fix: comprehensive mobile responsive audit and alignment improvements
|
||||||
|
- Devices index: restructured header for 390px (tabs on second row, icon-only buttons)
|
||||||
|
- Devices index: hide IP/Type/Last Seen/Response/Subs columns at sm/md/lg breakpoints
|
||||||
|
- Sites index: overflow-x-auto wrapper, hide QoE/Subs/Location/Coordinates on mobile
|
||||||
|
- Dashboard: overflow-x-auto on alerts + site health tables, hide secondary columns mobile
|
||||||
|
- Agents index: overflow-x-auto wrappers around both agent tables
|
||||||
|
- Device show: restructured sticky header for mobile (name+status / IP+agent row)
|
||||||
|
- Site show: min-w-0 on grid columns, hide IP/Subs columns in device health table
|
||||||
|
- Agent show: icon-only action buttons on mobile, fix header component flex-wrap
|
||||||
|
- Schedule show: flex-wrap header, icon-only buttons, overflow-x-auto on timelines
|
||||||
|
- core_components.ex header component: flex-wrap + flex gap-2 justify-end for actions
|
||||||
|
- Device show: all dl/dt/dd value pairs now flex-1 text-right ml-4 for consistent column
|
||||||
|
- Device show ports table: Speed column now text-right to match In/Out
|
||||||
|
Files: device_live/index.html.heex, site_live/index.html.heex, dashboard_live.html.heex,
|
||||||
|
agent_live/index.html.heex, device_live/show.html.heex, site_live/show.html.heex,
|
||||||
|
agent_live/show.html.heex, schedule_live/show.html.heex,
|
||||||
|
components/core_components.ex
|
||||||
|
|
||||||
2026-03-12
|
2026-03-12
|
||||||
feat: add real-time GraphQL API with time-series queries and subscriptions
|
feat: add real-time GraphQL API with time-series queries and subscriptions
|
||||||
- Added absinthe_phoenix dependency for WebSocket subscription support
|
- Added absinthe_phoenix dependency for WebSocket subscription support
|
||||||
|
|
|
||||||
|
|
@ -476,8 +476,8 @@ defmodule ToweropsWeb.CoreComponents do
|
||||||
|
|
||||||
def header(assigns) do
|
def header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "mb-8"]}>
|
<header class={[@actions != [] && "flex flex-wrap items-start justify-between gap-3", "mb-8"]}>
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
{render_slot(@inner_block)}
|
{render_slot(@inner_block)}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -485,7 +485,7 @@ defmodule ToweropsWeb.CoreComponents do
|
||||||
{render_slot(@subtitle)}
|
{render_slot(@subtitle)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none">{render_slot(@actions)}</div>
|
<div class="flex flex-wrap gap-2 justify-end shrink-0">{render_slot(@actions)}</div>
|
||||||
</header>
|
</header>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="mt-6">
|
<div class="mt-6 overflow-x-auto">
|
||||||
<.table
|
<.table
|
||||||
id="agents-table"
|
id="agents-table"
|
||||||
rows={@streams.agent_tokens}
|
rows={@streams.agent_tokens}
|
||||||
|
|
@ -183,108 +183,110 @@
|
||||||
"Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins."
|
"Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins."
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<.table
|
<div class="overflow-x-auto">
|
||||||
id="cloud-pollers-table"
|
<.table
|
||||||
rows={@streams.cloud_pollers}
|
id="cloud-pollers-table"
|
||||||
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
|
rows={@streams.cloud_pollers}
|
||||||
>
|
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
|
||||||
<:col :let={{_id, agent}} label={t("Name")}>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<:col :let={{_id, agent}} label={t("Name")}>
|
||||||
<span class="font-medium">{agent.name}</span>
|
<div class="flex items-center gap-2">
|
||||||
<%= if agent.id == @global_default_cloud_poller_id do %>
|
<span class="font-medium">{agent.name}</span>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 dark:bg-green-900/30 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 ring-1 ring-inset ring-green-600/20 dark:ring-green-400/30">
|
<%= if agent.id == @global_default_cloud_poller_id do %>
|
||||||
<.icon name="hero-check-circle" class="h-3 w-3" /> Default
|
<span class="inline-flex items-center gap-1 rounded-full bg-green-50 dark:bg-green-900/30 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 ring-1 ring-inset ring-green-600/20 dark:ring-green-400/30">
|
||||||
</span>
|
<.icon name="hero-check-circle" class="h-3 w-3" /> Default
|
||||||
<% end %>
|
</span>
|
||||||
<%= if agent.allow_remote_debug do %>
|
<% end %>
|
||||||
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
|
<%= if agent.allow_remote_debug do %>
|
||||||
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
|
<span class="inline-flex items-center gap-1 rounded-full bg-amber-50 dark:bg-amber-900/30 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300 ring-1 ring-inset ring-amber-600/20 dark:ring-amber-400/30">
|
||||||
</span>
|
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
|
||||||
<% end %>
|
</span>
|
||||||
</div>
|
<% end %>
|
||||||
</:col>
|
</div>
|
||||||
|
</:col>
|
||||||
|
|
||||||
<:col :let={{_id, agent}} label={t("Status")}>
|
<:col :let={{_id, agent}} label={t("Status")}>
|
||||||
<% {status, label} = agent_status(agent) %>
|
<% {status, label} = agent_status(agent) %>
|
||||||
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
|
||||||
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
|
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</:col>
|
</:col>
|
||||||
|
|
||||||
<:col :let={{_id, agent}} label={t("Device")}>
|
<:col :let={{_id, agent}} label={t("Device")}>
|
||||||
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
|
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
|
||||||
<div class="text-sm text-gray-900 dark:text-white">
|
<div class="text-sm text-gray-900 dark:text-white">
|
||||||
{counts.total} total
|
{counts.total} total
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
||||||
{counts.direct} direct
|
|
||||||
<%= if counts.total > counts.direct do %>
|
|
||||||
· {counts.total - counts.direct} inherited
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:col :let={{_id, agent}} label={t("Last Seen")}>
|
|
||||||
<div class="text-sm text-gray-900 dark:text-white">
|
|
||||||
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
|
|
||||||
</div>
|
|
||||||
<%= if agent.last_seen_at do %>
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" />
|
{counts.direct} direct
|
||||||
|
<%= if counts.total > counts.direct do %>
|
||||||
|
· {counts.total - counts.direct} inherited
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</:col>
|
||||||
<%= if agent.last_ip do %>
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
|
|
||||||
{agent.last_ip}
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</:col>
|
|
||||||
|
|
||||||
<:col :let={{_id, agent}} label={t("Version")}>
|
<:col :let={{_id, agent}} label={t("Last Seen")}>
|
||||||
<%= if agent.metadata["version"] do %>
|
<div class="text-sm text-gray-900 dark:text-white">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
|
||||||
{format_agent_version(agent.metadata["version"])}
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<%= if agent.last_seen_at do %>
|
||||||
</:col>
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||||
|
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" />
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%= if agent.last_ip do %>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
|
||||||
|
{agent.last_ip}
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</:col>
|
||||||
|
|
||||||
<:col :let={{_id, agent}} label={t("Created")}>
|
<:col :let={{_id, agent}} label={t("Version")}>
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
<%= if agent.metadata["version"] do %>
|
||||||
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||||
</div>
|
{format_agent_version(agent.metadata["version"])}
|
||||||
</:col>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</:col>
|
||||||
|
|
||||||
<:action :let={{_id, agent}}>
|
<:col :let={{_id, agent}} label={t("Created")}>
|
||||||
<%= if agent.enabled do %>
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<div class="flex items-center gap-3">
|
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
|
||||||
<.button
|
|
||||||
type="button"
|
|
||||||
phx-click="show_setup"
|
|
||||||
phx-value-id={agent.id}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{t("View Setup")}
|
|
||||||
</.button>
|
|
||||||
<.button
|
|
||||||
type="button"
|
|
||||||
phx-click="delete_agent"
|
|
||||||
phx-value-id={agent.id}
|
|
||||||
data-confirm={
|
|
||||||
t(
|
|
||||||
"Are you sure you want to delete this cloud poller? This will affect all organizations using it."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="danger"
|
|
||||||
>
|
|
||||||
{t("Delete")}
|
|
||||||
</.button>
|
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
</:col>
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
|
|
||||||
<% end %>
|
<:action :let={{_id, agent}}>
|
||||||
</:action>
|
<%= if agent.enabled do %>
|
||||||
</.table>
|
<div class="flex items-center gap-3">
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-click="show_setup"
|
||||||
|
phx-value-id={agent.id}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{t("View Setup")}
|
||||||
|
</.button>
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-click="delete_agent"
|
||||||
|
phx-value-id={agent.id}
|
||||||
|
data-confirm={
|
||||||
|
t(
|
||||||
|
"Are you sure you want to delete this cloud poller? This will affect all organizations using it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="danger"
|
||||||
|
>
|
||||||
|
{t("Delete")}
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
|
||||||
|
<% end %>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
}
|
}
|
||||||
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 dark:bg-green-500 dark:hover:bg-green-600"
|
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 dark:bg-green-500 dark:hover:bg-green-600"
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-up-tray" class="h-5 w-5" /> Update
|
<.icon name="hero-arrow-up-tray" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">Update</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:if={@current_scope.user.is_superuser}
|
:if={@current_scope.user.is_superuser}
|
||||||
|
|
@ -29,19 +30,22 @@
|
||||||
}
|
}
|
||||||
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-amber-600 text-white hover:bg-amber-700 focus:ring-amber-500 dark:bg-amber-500 dark:hover:bg-amber-600"
|
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-amber-600 text-white hover:bg-amber-700 focus:ring-amber-500 dark:bg-amber-500 dark:hover:bg-amber-600"
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-path" class="h-5 w-5" /> Restart
|
<.icon name="hero-arrow-path" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">Restart</span>
|
||||||
</button>
|
</button>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/agents/#{@agent_token.id}/edit"}
|
navigate={~p"/agents/#{@agent_token.id}/edit"}
|
||||||
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600"
|
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600"
|
||||||
>
|
>
|
||||||
<.icon name="hero-pencil" class="h-5 w-5" /> Edit Agent
|
<.icon name="hero-pencil" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">Edit Agent</span>
|
||||||
</.link>
|
</.link>
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/agents"}
|
navigate={~p"/agents"}
|
||||||
class="text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
class="inline-flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-left" class="h-5 w-5" /> Back to Agents
|
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
||||||
|
<span class="hidden sm:inline">Back to Agents</span>
|
||||||
</.link>
|
</.link>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
|
||||||
|
|
@ -363,24 +363,24 @@
|
||||||
|
|
||||||
<%!-- Alerts table --%>
|
<%!-- Alerts table --%>
|
||||||
<%= if @active_alerts != [] do %>
|
<%= if @active_alerts != [] do %>
|
||||||
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-white/10">
|
<div class="overflow-x-auto rounded-md border border-gray-200 dark:border-white/10">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
|
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="w-1 p-0"></th>
|
<th class="w-1 p-0"></th>
|
||||||
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Device")}
|
{t("Device")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Alert")}
|
{t("Alert")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Impact")}
|
{t("Impact")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("When")}
|
{t("When")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 w-12"></th>
|
<th class="hidden sm:table-cell px-3 py-1.5 w-12"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-800/50">
|
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-800/50">
|
||||||
|
|
@ -407,7 +407,7 @@
|
||||||
]} />
|
]} />
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5">
|
<td class="px-3 py-1.5">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5 flex-wrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{alert.device.id}"}
|
navigate={~p"/devices/#{alert.device.id}"}
|
||||||
class="text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
|
class="text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
|
||||||
|
|
@ -416,36 +416,36 @@
|
||||||
</.link>
|
</.link>
|
||||||
<span
|
<span
|
||||||
:if={alert.device.ip_address}
|
:if={alert.device.ip_address}
|
||||||
class="font-mono text-[11px] text-gray-400 dark:text-gray-500 dark:text-gray-400"
|
class="hidden sm:inline font-mono text-[11px] text-gray-400 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{alert.device.ip_address}
|
{alert.device.ip_address}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
:if={alert.device.site}
|
:if={alert.device.site}
|
||||||
class="text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400"
|
class="text-[10px] text-gray-400 dark:text-gray-500"
|
||||||
>
|
>
|
||||||
{alert.device.site.name}
|
{alert.device.site.name}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-xs text-gray-600 dark:text-gray-400 max-w-xs truncate">
|
<td class="px-3 py-1.5 text-xs text-gray-600 dark:text-gray-400 max-w-[120px] sm:max-w-xs truncate">
|
||||||
{alert.message}
|
{alert.message}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right whitespace-nowrap">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right whitespace-nowrap">
|
||||||
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
|
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
|
||||||
<span class="font-mono text-xs text-amber-600 dark:text-amber-400">
|
<span class="font-mono text-xs text-amber-600 dark:text-amber-400">
|
||||||
{alert.gaiia_impact["total_subscribers"]}s
|
{alert.gaiia_impact["total_subscribers"]}s
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right whitespace-nowrap">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right whitespace-nowrap">
|
||||||
<.timestamp
|
<.timestamp
|
||||||
datetime={alert.triggered_at}
|
datetime={alert.triggered_at}
|
||||||
timezone={@timezone}
|
timezone={@timezone}
|
||||||
class="font-mono text-[11px] text-gray-400 dark:text-gray-500 dark:text-gray-400"
|
class="font-mono text-[11px] text-gray-400 dark:text-gray-500"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right">
|
||||||
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -495,26 +495,26 @@
|
||||||
all sites →
|
all sites →
|
||||||
</.link>
|
</.link>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-white/10">
|
<div class="overflow-x-auto rounded-md border border-gray-200 dark:border-white/10">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
|
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Site")}
|
{t("Site")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Devices")}
|
{t("Devices")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Down")}
|
{t("Down")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("QoE")}
|
{t("QoE")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Subs")}
|
{t("Subs")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{t("Affected")}
|
{t("Affected")}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -552,7 +552,7 @@
|
||||||
<span class="text-gray-300 dark:text-gray-600">0</span>
|
<span class="text-gray-300 dark:text-gray-600">0</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right font-mono text-xs">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs">
|
||||||
<%= if site.avg_qoe do %>
|
<%= if site.avg_qoe do %>
|
||||||
<span class={
|
<span class={
|
||||||
cond do
|
cond do
|
||||||
|
|
@ -567,14 +567,14 @@
|
||||||
<span class="text-gray-300 dark:text-gray-600">—</span>
|
<span class="text-gray-300 dark:text-gray-600">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
|
||||||
<%= if site.subscribers do %>
|
<%= if site.subscribers do %>
|
||||||
{site.subscribers}
|
{site.subscribers}
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-gray-300 dark:text-gray-600">—</span>
|
<span class="text-gray-300 dark:text-gray-600">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-1.5 text-right font-mono text-xs">
|
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs">
|
||||||
<%= if site.subscribers_affected > 0 do %>
|
<%= if site.subscribers_affected > 0 do %>
|
||||||
<span class="text-red-600 dark:text-red-400 font-bold">
|
<span class="text-red-600 dark:text-red-400 font-bold">
|
||||||
{format_number(site.subscribers_affected)}
|
{format_number(site.subscribers_affected)}
|
||||||
|
|
|
||||||
|
|
@ -3,115 +3,125 @@
|
||||||
current_scope={@current_scope}
|
current_scope={@current_scope}
|
||||||
active_page="devices"
|
active_page="devices"
|
||||||
>
|
>
|
||||||
<%!-- Header: title + tabs + actions in one row --%>
|
<%!-- Header: title + actions, then tabs below on mobile --%>
|
||||||
<div class="flex items-center justify-between mb-4 border-b border-gray-200 dark:border-white/10 pb-3">
|
<div class="mb-4 border-b border-gray-200 dark:border-white/10 pb-3">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@page_title}</h1>
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<% percent =
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{@page_title}</h1>
|
||||||
if @device_quota.limit != :unlimited and @device_quota.limit > 0 do
|
<% percent =
|
||||||
(@device_quota.current / @device_quota.limit * 100) |> trunc()
|
if @device_quota.limit != :unlimited and @device_quota.limit > 0 do
|
||||||
else
|
(@device_quota.current / @device_quota.limit * 100) |> trunc()
|
||||||
0
|
else
|
||||||
end %>
|
0
|
||||||
<% badge_class =
|
end %>
|
||||||
cond do
|
<% badge_class =
|
||||||
@current_scope.user.is_superuser ->
|
cond do
|
||||||
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
@current_scope.user.is_superuser ->
|
||||||
|
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
||||||
|
|
||||||
@device_quota.limit != :unlimited and @device_quota.current >= @device_quota.limit ->
|
@device_quota.limit != :unlimited and @device_quota.current >= @device_quota.limit ->
|
||||||
"bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
|
"bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
|
||||||
|
|
||||||
percent >= 90 ->
|
percent >= 90 ->
|
||||||
"bg-orange-50 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400"
|
"bg-orange-50 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400"
|
||||||
|
|
||||||
@device_quota.limit == :unlimited ->
|
@device_quota.limit == :unlimited ->
|
||||||
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
"bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
|
"bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
|
||||||
end %>
|
end %>
|
||||||
<span class={"rounded px-2 py-0.5 text-xs font-medium #{badge_class}"}>
|
<span class={"shrink-0 rounded px-2 py-0.5 text-xs font-medium #{badge_class}"}>
|
||||||
<%= cond do %>
|
<%= cond do %>
|
||||||
<% @current_scope.user.is_superuser -> %>
|
<% @current_scope.user.is_superuser -> %>
|
||||||
{@device_quota.current} devices
|
{@device_quota.current}
|
||||||
<% @device_quota.limit == :unlimited -> %>
|
<% @device_quota.limit == :unlimited -> %>
|
||||||
{@device_quota.current} devices
|
{@device_quota.current}
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
{@device_quota.current}/{@device_quota.limit}
|
{@device_quota.current}/{@device_quota.limit}
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<%= if @has_devices do %>
|
||||||
|
<%= if @reorder_mode do %>
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-click="reset_order"
|
||||||
|
data-confirm={t("Reset all sites and devices to alphabetical order?")}
|
||||||
|
>
|
||||||
|
<.icon name="hero-arrow-path" class="h-4 w-4" />
|
||||||
|
<span class="hidden sm:inline">{t("Reset")}</span>
|
||||||
|
</.button>
|
||||||
|
<.button type="button" phx-click="toggle_reorder_mode" variant="primary">
|
||||||
|
<.icon name="hero-check" class="h-4 w-4" />
|
||||||
|
<span class="hidden sm:inline">{t("Done")}</span>
|
||||||
|
</.button>
|
||||||
|
<% else %>
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
phx-click="toggle_reorder_mode"
|
||||||
|
title="Reorder devices and sites"
|
||||||
|
>
|
||||||
|
<.icon name="hero-bars-arrow-up" class="h-4 w-4" />
|
||||||
|
</.button>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
<.button :if={!@sites_enabled || @has_sites} navigate={~p"/devices/new"} variant="primary">
|
||||||
<%!-- Inline tab switcher --%>
|
<.icon name="hero-plus" class="h-4 w-4" />
|
||||||
<nav class="flex items-center gap-1 ml-2">
|
<span class="hidden sm:inline">{t("New Device")}</span>
|
||||||
<.link
|
<span class="sm:hidden">{t("New")}</span>
|
||||||
patch={~p"/devices?tab=existing"}
|
</.button>
|
||||||
class={[
|
<%= if @has_devices and not @reorder_mode do %>
|
||||||
"px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
|
|
||||||
if @active_tab == "existing" do
|
|
||||||
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
|
|
||||||
else
|
|
||||||
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
|
|
||||||
end
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t("Existing")}
|
|
||||||
</.link>
|
|
||||||
<.link
|
|
||||||
patch={~p"/devices?tab=discovered"}
|
|
||||||
class={[
|
|
||||||
"inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
|
|
||||||
if @active_tab == "discovered" do
|
|
||||||
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
|
|
||||||
else
|
|
||||||
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
|
|
||||||
end
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{t("Discovered")}
|
|
||||||
<span
|
|
||||||
:if={@pagination && @pagination.total_count > 0}
|
|
||||||
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
|
||||||
>
|
|
||||||
{@pagination.total_count}
|
|
||||||
</span>
|
|
||||||
</.link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<%= if @has_devices do %>
|
|
||||||
<%= if @reorder_mode do %>
|
|
||||||
<.button
|
<.button
|
||||||
type="button"
|
type="button"
|
||||||
phx-click="reset_order"
|
phx-click="force_rediscover_all"
|
||||||
data-confirm={t("Reset all sites and devices to alphabetical order?")}
|
title="Rediscover all devices"
|
||||||
|
data-confirm={
|
||||||
|
t("This will trigger SNMP discovery for all SNMP-enabled devices. Continue?")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-path" class="h-4 w-4" /> Reset
|
<.icon name="hero-magnifying-glass" class="h-4 w-4" />
|
||||||
</.button>
|
|
||||||
<.button type="button" phx-click="toggle_reorder_mode" variant="primary">
|
|
||||||
<.icon name="hero-check" class="h-4 w-4" /> Done
|
|
||||||
</.button>
|
|
||||||
<% else %>
|
|
||||||
<.button type="button" phx-click="toggle_reorder_mode" title="Reorder devices and sites">
|
|
||||||
<.icon name="hero-bars-arrow-up" class="h-4 w-4" />
|
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
<.button :if={!@sites_enabled || @has_sites} navigate={~p"/devices/new"} variant="primary">
|
|
||||||
<.icon name="hero-plus" class="h-4 w-4" /> {t("New Device")}
|
|
||||||
</.button>
|
|
||||||
<%= if @has_devices and not @reorder_mode do %>
|
|
||||||
<.button
|
|
||||||
type="button"
|
|
||||||
phx-click="force_rediscover_all"
|
|
||||||
title="Rediscover all devices"
|
|
||||||
data-confirm={
|
|
||||||
t("This will trigger SNMP discovery for all SNMP-enabled devices. Continue?")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<.icon name="hero-magnifying-glass" class="h-4 w-4" />
|
|
||||||
</.button>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
<%!-- Tabs on second line --%>
|
||||||
|
<nav class="flex items-center gap-1 mt-2">
|
||||||
|
<.link
|
||||||
|
patch={~p"/devices?tab=existing"}
|
||||||
|
class={[
|
||||||
|
"px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
|
||||||
|
if @active_tab == "existing" do
|
||||||
|
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
|
||||||
|
else
|
||||||
|
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
|
||||||
|
end
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t("Existing")}
|
||||||
|
</.link>
|
||||||
|
<.link
|
||||||
|
patch={~p"/devices?tab=discovered"}
|
||||||
|
class={[
|
||||||
|
"inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
|
||||||
|
if @active_tab == "discovered" do
|
||||||
|
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
|
||||||
|
else
|
||||||
|
"text-gray-500 hover:text-gray-700 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800/50"
|
||||||
|
end
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{t("Discovered")}
|
||||||
|
<span
|
||||||
|
:if={@pagination && @pagination.total_count > 0}
|
||||||
|
class="inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||||
|
>
|
||||||
|
{@pagination.total_count}
|
||||||
|
</span>
|
||||||
|
</.link>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= case @active_tab do %>
|
<%= case @active_tab do %>
|
||||||
|
|
@ -256,7 +266,7 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
class="hidden sm:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{t("IP Address")}
|
{t("IP Address")}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -268,25 +278,25 @@
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
class="hidden md:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{t("Type")}
|
{t("Type")}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
class="hidden md:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{t("Last Seen")}
|
{t("Last Seen")}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
class="hidden lg:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{t("Response")}
|
{t("Response")}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
class="hidden lg:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{t("Subs")}
|
{t("Subs")}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -414,7 +424,7 @@
|
||||||
{row.device.name}
|
{row.device.name}
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-0 whitespace-nowrap">
|
<td class="hidden sm:table-cell p-0 whitespace-nowrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{row.device.id}"}
|
navigate={~p"/devices/#{row.device.id}"}
|
||||||
class={[
|
class={[
|
||||||
|
|
@ -452,7 +462,7 @@
|
||||||
</span>
|
</span>
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-0 whitespace-nowrap">
|
<td class="hidden md:table-cell p-0 whitespace-nowrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{row.device.id}"}
|
navigate={~p"/devices/#{row.device.id}"}
|
||||||
class={[
|
class={[
|
||||||
|
|
@ -471,7 +481,7 @@
|
||||||
</span>
|
</span>
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-0 whitespace-nowrap">
|
<td class="hidden md:table-cell p-0 whitespace-nowrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{row.device.id}"}
|
navigate={~p"/devices/#{row.device.id}"}
|
||||||
class={[
|
class={[
|
||||||
|
|
@ -483,7 +493,7 @@
|
||||||
<span class={"text-xs font-mono #{color}"}>{text}</span>
|
<span class={"text-xs font-mono #{color}"}>{text}</span>
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-0 whitespace-nowrap">
|
<td class="hidden lg:table-cell p-0 whitespace-nowrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{row.device.id}"}
|
navigate={~p"/devices/#{row.device.id}"}
|
||||||
class={[
|
class={[
|
||||||
|
|
@ -507,7 +517,7 @@
|
||||||
</div>
|
</div>
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-0 whitespace-nowrap">
|
<td class="hidden lg:table-cell p-0 whitespace-nowrap">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/devices/#{row.device.id}"}
|
navigate={~p"/devices/#{row.device.id}"}
|
||||||
class={[
|
class={[
|
||||||
|
|
|
||||||
|
|
@ -17,86 +17,88 @@
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<div class="sticky top-0 z-30 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8 py-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur-sm border-b border-gray-200/50 dark:border-white/5 mb-4">
|
<div class="sticky top-0 z-30 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8 py-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur-sm border-b border-gray-200/50 dark:border-white/5 mb-4">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="min-w-0 flex-1">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@device.name}</h1>
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<div class={[
|
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white truncate">
|
||||||
"flex items-center gap-2 px-3 py-1.5 rounded-lg font-semibold text-sm transition-all",
|
{@device.name}
|
||||||
case @device.status do
|
</h1>
|
||||||
:up ->
|
<div class={[
|
||||||
"bg-green-100 text-green-800 dark:bg-green-950/50 dark:text-green-400 ring-1 ring-green-600/20 dark:ring-green-500/30"
|
"inline-flex shrink-0 items-center gap-2 px-2.5 py-1 rounded-lg font-semibold text-sm transition-all",
|
||||||
|
|
||||||
:down ->
|
|
||||||
"bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-400 ring-1 ring-red-600/20 dark:ring-red-500/30"
|
|
||||||
|
|
||||||
:unknown ->
|
|
||||||
"bg-gray-100 text-gray-800 dark:bg-gray-800/50 dark:text-gray-400 ring-1 ring-gray-600/20 dark:ring-gray-500/30"
|
|
||||||
end
|
|
||||||
]}>
|
|
||||||
<span class={[
|
|
||||||
"h-2 w-2 rounded-full animate-pulse",
|
|
||||||
case @device.status do
|
case @device.status do
|
||||||
:up -> "bg-green-600 dark:bg-green-500"
|
:up ->
|
||||||
:down -> "bg-red-600 dark:bg-red-500"
|
"bg-green-100 text-green-800 dark:bg-green-950/50 dark:text-green-400 ring-1 ring-green-600/20 dark:ring-green-500/30"
|
||||||
:unknown -> "bg-gray-500 dark:bg-gray-400"
|
|
||||||
|
:down ->
|
||||||
|
"bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-400 ring-1 ring-red-600/20 dark:ring-red-500/30"
|
||||||
|
|
||||||
|
:unknown ->
|
||||||
|
"bg-gray-100 text-gray-800 dark:bg-gray-800/50 dark:text-gray-400 ring-1 ring-gray-600/20 dark:ring-gray-500/30"
|
||||||
end
|
end
|
||||||
]}>
|
]}>
|
||||||
|
<span class={[
|
||||||
|
"h-2 w-2 rounded-full animate-pulse",
|
||||||
|
case @device.status do
|
||||||
|
:up -> "bg-green-600 dark:bg-green-500"
|
||||||
|
:down -> "bg-red-600 dark:bg-red-500"
|
||||||
|
:unknown -> "bg-gray-500 dark:bg-gray-400"
|
||||||
|
end
|
||||||
|
]}>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{case @device.status do
|
||||||
|
:up -> "Online"
|
||||||
|
:down -> "Offline"
|
||||||
|
:unknown -> "Unknown"
|
||||||
|
end}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center gap-1.5 mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span
|
||||||
|
class="font-mono text-gray-600 dark:text-gray-400 group/ip cursor-pointer"
|
||||||
|
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
|
||||||
|
title={t("Click to copy")}
|
||||||
|
>
|
||||||
|
{@device.ip_address}
|
||||||
|
<.icon
|
||||||
|
name="hero-clipboard-document"
|
||||||
|
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/ip:opacity-100 transition-opacity text-gray-400"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="text-gray-300 dark:text-gray-600">·</span>
|
||||||
{case @device.status do
|
<%= if @agent_info.type == :cloud do %>
|
||||||
:up -> "Online"
|
<span class="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400">
|
||||||
:down -> "Offline"
|
<.icon name="hero-cloud" class="h-3 w-3" />
|
||||||
:unknown -> "Unknown"
|
<%= if @agent_info.agent_token_id do %>
|
||||||
end}
|
{t("Cloud")} ({@agent_info.name})
|
||||||
</span>
|
<% else %>
|
||||||
|
{t("Cloud")}
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="inline-flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||||
|
<.icon name="hero-server" class="h-3 w-3" />
|
||||||
|
{@agent_info.name}
|
||||||
|
</span>
|
||||||
|
<%= if @agent_info.is_offline do %>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-medium text-amber-700 bg-amber-100 dark:text-amber-400 dark:bg-amber-900/30 rounded"
|
||||||
|
title="Agent has not checked in for over 5 minutes."
|
||||||
|
>
|
||||||
|
<.icon name="hero-exclamation-triangle" class="h-3 w-3" /> Offline
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 mt-0.5">
|
<div class="shrink-0 mt-0.5">
|
||||||
<span
|
<.button navigate={~p"/devices/#{@device.id}/edit"}>
|
||||||
class="text-sm text-gray-600 dark:text-gray-400 font-mono group/ip cursor-pointer"
|
<.icon name="hero-pencil" class="h-4 w-4" />
|
||||||
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
|
<span class="hidden sm:inline">{t("Edit")}</span>
|
||||||
title={t("Click to copy")}
|
</.button>
|
||||||
>
|
|
||||||
{@device.ip_address}
|
|
||||||
<.icon
|
|
||||||
name="hero-clipboard-document"
|
|
||||||
class="h-3.5 w-3.5 inline ml-1 opacity-0 group-hover/ip:opacity-100 transition-opacity text-gray-400"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-300 dark:text-gray-600">•</span>
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{t("Polled by:")}
|
|
||||||
</span>
|
|
||||||
<%= if @agent_info.type == :cloud do %>
|
|
||||||
<span class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
|
|
||||||
<.icon name="hero-cloud" class="h-3.5 w-3.5" />
|
|
||||||
<%= if @agent_info.agent_token_id do %>
|
|
||||||
Cloud ({@agent_info.name})
|
|
||||||
<% else %>
|
|
||||||
{t("Cloud")}
|
|
||||||
<% end %>
|
|
||||||
</span>
|
|
||||||
<% else %>
|
|
||||||
<span class="inline-flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
|
||||||
<.icon name="hero-server" class="h-3.5 w-3.5" />
|
|
||||||
{@agent_info.name}
|
|
||||||
</span>
|
|
||||||
<%= if @agent_info.is_offline do %>
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-amber-700 bg-amber-100 dark:text-amber-400 dark:bg-amber-900/30 rounded-md"
|
|
||||||
title="Agent has not checked in for over 5 minutes. This device will not be polled until the agent comes back online."
|
|
||||||
>
|
|
||||||
<.icon name="hero-exclamation-triangle" class="h-3 w-3" /> Offline
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
|
||||||
<.button navigate={~p"/devices/#{@device.id}/edit"}>
|
|
||||||
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
|
|
||||||
</.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Subscriber Impact Card -->
|
<!-- Subscriber Impact Card -->
|
||||||
<%= if @subscriber_impact && @subscriber_impact.subscriber_count > 0 do %>
|
<%= if @subscriber_impact && @subscriber_impact.subscriber_count > 0 do %>
|
||||||
|
|
@ -390,14 +392,18 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= if @snmp_device do %>
|
<%= if @snmp_device do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">System Name</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
System Name
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{@snmp_device.sys_name || @device.name}
|
{@snmp_device.sys_name || @device.name}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">{t("Resolved IP")}</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{t("Resolved IP")}
|
||||||
|
</dt>
|
||||||
<dd
|
<dd
|
||||||
class="text-sm font-medium text-gray-900 dark:text-white font-mono group/rip cursor-pointer"
|
class="text-sm font-medium text-gray-900 dark:text-white font-mono group/rip cursor-pointer"
|
||||||
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
|
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
|
||||||
|
|
@ -412,15 +418,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Hardware</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Hardware</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{@snmp_device.model || "N/A"}
|
{@snmp_device.model || "N/A"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Operating System</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
Operating System
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
{cond do
|
{cond do
|
||||||
|
|
@ -515,65 +523,73 @@
|
||||||
:if={@snmp_device.serial_number}
|
:if={@snmp_device.serial_number}
|
||||||
class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5"
|
class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5"
|
||||||
>
|
>
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Serial Number</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white font-mono">
|
Serial Number
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
|
||||||
{@snmp_device.serial_number}
|
{@snmp_device.serial_number}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Object ID</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Object ID</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white font-mono">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
|
||||||
{@snmp_device.sys_object_id || "N/A"}
|
{@snmp_device.sys_object_id || "N/A"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Contact</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Contact</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{@snmp_device.sys_contact || "N/A"}
|
{@snmp_device.sys_contact || "N/A"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Location</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Location</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{@snmp_device.sys_location || "N/A"}
|
{@snmp_device.sys_location || "N/A"}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Uptime</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Uptime</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{format_uptime(@snmp_device.sys_uptime)}
|
{format_uptime(@snmp_device.sys_uptime)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Name</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Name</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{@device.name}
|
{@device.name}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">IP Address</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white font-mono">
|
IP Address
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
|
||||||
{@device.ip_address}
|
{@device.ip_address}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Device Added</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
Device Added
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{format_device_age(@device.inserted_at)}
|
{format_device_age(@device.inserted_at)}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5">
|
<div class="flex justify-between py-1.5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Last Discovered</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
Last Discovered
|
||||||
|
</dt>
|
||||||
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{if @device.last_discovery_at do
|
{if @device.last_discovery_at do
|
||||||
format_device_age(@device.last_discovery_at)
|
format_device_age(@device.last_discovery_at)
|
||||||
else
|
else
|
||||||
|
|
@ -583,8 +599,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between py-1.5">
|
<div class="flex justify-between py-1.5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">Last Polled</dt>
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Last Polled</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
{if @device.last_snmp_poll_at do
|
{if @device.last_snmp_poll_at do
|
||||||
format_device_age(@device.last_snmp_poll_at)
|
format_device_age(@device.last_snmp_poll_at)
|
||||||
else
|
else
|
||||||
|
|
@ -823,7 +839,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @storage_sensors do %>
|
<%= for sensor <- @storage_sensors do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/storage?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/storage?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -833,7 +849,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading do %>
|
<%= if sensor.latest_reading do %>
|
||||||
{format_sensor_value(
|
{format_sensor_value(
|
||||||
sensor.latest_reading.value,
|
sensor.latest_reading.value,
|
||||||
|
|
@ -935,7 +951,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @temperature_sensors do %>
|
<%= for sensor <- @temperature_sensors do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/temperature?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/temperature?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -945,7 +961,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading do %>
|
<%= if sensor.latest_reading do %>
|
||||||
{format_sensor_value(
|
{format_sensor_value(
|
||||||
sensor.latest_reading.value,
|
sensor.latest_reading.value,
|
||||||
|
|
@ -974,7 +990,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @voltage_sensors do %>
|
<%= for sensor <- @voltage_sensors do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/voltage?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/voltage?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -984,7 +1000,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading do %>
|
<%= if sensor.latest_reading do %>
|
||||||
{format_sensor_value(
|
{format_sensor_value(
|
||||||
sensor.latest_reading.value,
|
sensor.latest_reading.value,
|
||||||
|
|
@ -1014,7 +1030,7 @@
|
||||||
<%= for {_transceiver_name, sensors} <- @grouped_transceivers do %>
|
<%= for {_transceiver_name, sensors} <- @grouped_transceivers do %>
|
||||||
<%= for sensor <- sensors do %>
|
<%= for sensor <- sensors do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/dbm?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/dbm?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -1024,7 +1040,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white font-mono">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
|
||||||
<%= if sensor.latest_reading do %>
|
<%= if sensor.latest_reading do %>
|
||||||
{format_sensor_value(
|
{format_sensor_value(
|
||||||
sensor.latest_reading.value,
|
sensor.latest_reading.value,
|
||||||
|
|
@ -1056,7 +1072,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @general_counters do %>
|
<%= for sensor <- @general_counters do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -1066,7 +1082,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
||||||
{trunc(sensor.latest_reading.value)}
|
{trunc(sensor.latest_reading.value)}
|
||||||
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
|
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
|
||||||
|
|
@ -1089,7 +1105,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @firewall_counters do %>
|
<%= for sensor <- @firewall_counters do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -1099,7 +1115,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
||||||
{trunc(sensor.latest_reading.value)}
|
{trunc(sensor.latest_reading.value)}
|
||||||
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
|
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
|
||||||
|
|
@ -1129,7 +1145,7 @@
|
||||||
<dl class="space-y-1">
|
<dl class="space-y-1">
|
||||||
<%= for sensor <- @wireless_sensors do %>
|
<%= for sensor <- @wireless_sensors do %>
|
||||||
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
|
||||||
<dt class="text-sm text-gray-600 dark:text-gray-400">
|
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
|
||||||
<.link
|
<.link
|
||||||
navigate={
|
navigate={
|
||||||
~p"/devices/#{@device.id}/graph/wireless?sensor_id=#{sensor.id}"
|
~p"/devices/#{@device.id}/graph/wireless?sensor_id=#{sensor.id}"
|
||||||
|
|
@ -1139,7 +1155,7 @@
|
||||||
{sensor.sensor_descr}
|
{sensor.sensor_descr}
|
||||||
</.link>
|
</.link>
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="text-sm font-medium text-gray-900 dark:text-white">
|
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||||
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
|
||||||
<%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %>
|
<%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %>
|
||||||
{trunc(sensor.latest_reading.value)}
|
{trunc(sensor.latest_reading.value)}
|
||||||
|
|
@ -1310,7 +1326,7 @@
|
||||||
<th class="px-3 py-2 text-left font-medium">#</th>
|
<th class="px-3 py-2 text-left font-medium">#</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">Name</th>
|
<th class="px-3 py-2 text-left font-medium">Name</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">Status</th>
|
<th class="px-3 py-2 text-left font-medium">Status</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">Speed</th>
|
<th class="px-3 py-2 text-right font-medium">Speed</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">IP Address</th>
|
<th class="px-3 py-2 text-left font-medium">IP Address</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">MAC</th>
|
<th class="px-3 py-2 text-left font-medium">MAC</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">Capacity</th>
|
<th class="px-3 py-2 text-left font-medium">Capacity</th>
|
||||||
|
|
@ -1363,7 +1379,7 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-gray-900 dark:text-white text-xs font-mono">
|
<td class="px-3 py-2 text-right text-gray-900 dark:text-white text-xs font-mono">
|
||||||
{format_speed(interface.if_speed)}
|
{format_speed(interface.if_speed)}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
|
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,26 @@
|
||||||
current_scope={@current_scope}
|
current_scope={@current_scope}
|
||||||
active_page="schedules"
|
active_page="schedules"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex flex-wrap items-start justify-between gap-3 mb-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/schedules"}
|
navigate={~p"/schedules"}
|
||||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
class="shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
<.icon name="hero-arrow-left" class="h-5 w-5" />
|
||||||
</.link>
|
</.link>
|
||||||
<div>
|
<div class="min-w-0">
|
||||||
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@schedule.name}</h1>
|
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{@schedule.name}</h1>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">{@schedule.timezone}</p>
|
<p class="text-sm text-gray-500 dark:text-gray-400">{@schedule.timezone}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
<.link
|
<.link
|
||||||
navigate={~p"/schedules/#{@schedule.id}/edit"}
|
navigate={~p"/schedules/#{@schedule.id}/edit"}
|
||||||
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
>
|
>
|
||||||
<.icon name="hero-pencil" class="h-4 w-4" />
|
<.icon name="hero-pencil" class="h-4 w-4" />
|
||||||
{t("Edit")}
|
<span class="hidden sm:inline">{t("Edit")}</span>
|
||||||
</.link>
|
</.link>
|
||||||
<button
|
<button
|
||||||
phx-click="delete"
|
phx-click="delete"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-red-600 dark:text-red-400 border border-gray-300 dark:border-gray-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-gray-800 px-3 py-2 text-sm font-medium text-red-600 dark:text-red-400 border border-gray-300 dark:border-gray-600 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
|
||||||
>
|
>
|
||||||
<.icon name="hero-trash" class="h-4 w-4" />
|
<.icon name="hero-trash" class="h-4 w-4" />
|
||||||
{t("Delete")}
|
<span class="hidden sm:inline">{t("Delete")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -110,106 +110,110 @@
|
||||||
|
|
||||||
<%!-- Layer timelines --%>
|
<%!-- Layer timelines --%>
|
||||||
<%= if @layer_timelines != [] do %>
|
<%= if @layer_timelines != [] do %>
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 mb-4">
|
<div class="overflow-x-auto">
|
||||||
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 mb-4 min-w-[320px]">
|
||||||
{t("Configuration Layers")}
|
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
||||||
</h3>
|
{t("Configuration Layers")}
|
||||||
|
</h3>
|
||||||
|
|
||||||
<%= for lt <- @layer_timelines do %>
|
<%= for lt <- @layer_timelines do %>
|
||||||
<div class="mb-4 last:mb-0">
|
<div class="mb-4 last:mb-0">
|
||||||
<%!-- Day headers --%>
|
<%!-- Day headers --%>
|
||||||
<div class="flex mb-1">
|
<div class="flex mb-1">
|
||||||
<div class="w-28 shrink-0"></div>
|
<div class="w-28 shrink-0"></div>
|
||||||
<div class="flex-1 flex">
|
<div class="flex-1 flex">
|
||||||
<%= for day <- @timeline_days do %>
|
<%= for day <- @timeline_days do %>
|
||||||
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
||||||
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
{Calendar.strftime(day, "%-d %a")}
|
{Calendar.strftime(day, "%-d %a")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%!-- Timeline bar --%>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-28 shrink-0 pr-2 text-right">
|
||||||
|
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 truncate">
|
||||||
|
{lt.layer.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
||||||
|
<%= for span <- lt.spans do %>
|
||||||
|
<div
|
||||||
|
class={[
|
||||||
|
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
||||||
|
@user_colors[span.user.id] || "bg-gray-400"
|
||||||
|
]}
|
||||||
|
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
||||||
|
>
|
||||||
|
<span class="text-[10px] font-medium text-white truncate">
|
||||||
|
{display_name(span.user)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%!-- Today marker --%>
|
||||||
|
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
||||||
|
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
||||||
|
<div
|
||||||
|
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
||||||
|
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%!-- Timeline bar --%>
|
<% end %>
|
||||||
<div class="flex items-center">
|
</div>
|
||||||
<div class="w-28 shrink-0 pr-2 text-right">
|
|
||||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300 truncate">
|
|
||||||
{lt.layer.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
|
||||||
<%= for span <- lt.spans do %>
|
|
||||||
<div
|
|
||||||
class={[
|
|
||||||
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
|
||||||
@user_colors[span.user.id] || "bg-gray-400"
|
|
||||||
]}
|
|
||||||
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
|
||||||
>
|
|
||||||
<span class="text-[10px] font-medium text-white truncate">
|
|
||||||
{display_name(span.user)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<%!-- Today marker --%>
|
|
||||||
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
|
||||||
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
|
||||||
<div
|
|
||||||
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
|
||||||
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Final Schedule --%>
|
<%!-- Final Schedule --%>
|
||||||
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
|
<div class="overflow-x-auto">
|
||||||
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 min-w-[320px]">
|
||||||
{t("Final Schedule")}
|
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
|
||||||
</h3>
|
{t("Final Schedule")}
|
||||||
<%!-- Day headers --%>
|
</h3>
|
||||||
<div class="flex mb-1">
|
<%!-- Day headers --%>
|
||||||
<div class="w-28 shrink-0"></div>
|
<div class="flex mb-1">
|
||||||
<div class="flex-1 flex">
|
<div class="w-28 shrink-0"></div>
|
||||||
<%= for day <- @timeline_days do %>
|
<div class="flex-1 flex">
|
||||||
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
<%= for day <- @timeline_days do %>
|
||||||
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
<div class="flex-1 text-center border-l first:border-l-0 border-gray-200 dark:border-gray-700">
|
||||||
{Calendar.strftime(day, "%-d %a")}
|
<span class="text-[10px] text-gray-400 dark:text-gray-500">
|
||||||
</span>
|
{Calendar.strftime(day, "%-d %a")}
|
||||||
</div>
|
</span>
|
||||||
<% end %>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<%!-- Timeline bar --%>
|
||||||
<%!-- Timeline bar --%>
|
<div class="flex items-center">
|
||||||
<div class="flex items-center">
|
<div class="w-28 shrink-0"></div>
|
||||||
<div class="w-28 shrink-0"></div>
|
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
||||||
<div class="flex-1 relative h-8 bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
|
<%= for span <- @final_spans do %>
|
||||||
<%= for span <- @final_spans do %>
|
<div
|
||||||
<div
|
class={[
|
||||||
class={[
|
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
||||||
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
|
@user_colors[span.user.id] || "bg-gray-400"
|
||||||
@user_colors[span.user.id] || "bg-gray-400"
|
]}
|
||||||
]}
|
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
||||||
style={"left: #{span.start_offset / span.total_hours * 100}%; width: #{span.duration / span.total_hours * 100}%;"}
|
>
|
||||||
>
|
<span class="text-[10px] font-medium text-white truncate">
|
||||||
<span class="text-[10px] font-medium text-white truncate">
|
{display_name(span.user)}
|
||||||
{display_name(span.user)}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
<%!-- Today marker --%>
|
||||||
<%!-- Today marker --%>
|
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
||||||
<% today_offset = Date.diff(Date.utc_today(), @timeline_start) %>
|
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
||||||
<%= if today_offset >= 0 and today_offset < @timeline_days do %>
|
<div
|
||||||
<div
|
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
||||||
class="absolute inset-y-0 w-px bg-red-500 z-10"
|
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
||||||
style={"left: #{(today_offset * 24 + DateTime.utc_now().hour) / (length(@timeline_days) * 24) * 100}%;"}
|
>
|
||||||
>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%!-- Sites table --%>
|
<%!-- Sites table --%>
|
||||||
<div class="mt-6 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
|
<div class="mt-6 overflow-x-auto rounded-lg border border-gray-200 dark:border-white/10">
|
||||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-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/80">
|
<thead class="bg-gray-50 dark:bg-gray-800/80">
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -51,21 +51,21 @@
|
||||||
{t("Down")}
|
{t("Down")}
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400"
|
||||||
title={t("Quality of Experience — Preseem network quality score (0–100)")}
|
title={t("Quality of Experience — Preseem network quality score (0–100)")}
|
||||||
>
|
>
|
||||||
{t("QoE")}
|
{t("QoE")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<th class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
{t("Subscribers")}
|
{t("Subscribers")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<th class="hidden md:table-cell px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
{t("Location")}
|
{t("Location")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
{t("Coordinates")}
|
{t("Coordinates")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
{t("Last Check")}
|
{t("Last Check")}
|
||||||
</th>
|
</th>
|
||||||
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
<span class="text-gray-400 dark:text-gray-500">0</span>
|
<span class="text-gray-400 dark:text-gray-500">0</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-right text-sm">
|
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm">
|
||||||
<%= if @summary_map[site.id] && @summary_map[site.id].avg_qoe do %>
|
<%= if @summary_map[site.id] && @summary_map[site.id].avg_qoe do %>
|
||||||
<span class={qoe_color(@summary_map[site.id].avg_qoe)}>
|
<span class={qoe_color(@summary_map[site.id].avg_qoe)}>
|
||||||
{format_qoe(@summary_map[site.id].avg_qoe)}
|
{format_qoe(@summary_map[site.id].avg_qoe)}
|
||||||
|
|
@ -111,17 +111,17 @@
|
||||||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
|
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
|
||||||
<%= if @summary_map[site.id] && @summary_map[site.id].subscribers do %>
|
<%= if @summary_map[site.id] && @summary_map[site.id].subscribers do %>
|
||||||
{@summary_map[site.id].subscribers}
|
{@summary_map[site.id].subscribers}
|
||||||
<% else %>
|
<% else %>
|
||||||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
|
<td class="hidden md:table-cell px-4 py-2.5 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">
|
||||||
{site.location || "—"}
|
{site.location || "—"}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-right">
|
<td class="hidden lg:table-cell px-4 py-2.5 text-right">
|
||||||
<%= if site.latitude && site.longitude do %>
|
<%= if site.latitude && site.longitude do %>
|
||||||
<span class="text-xs font-mono text-gray-400 dark:text-gray-500">
|
<span class="text-xs font-mono text-gray-400 dark:text-gray-500">
|
||||||
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
|
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
<span class="text-gray-400 dark:text-gray-500 text-sm">—</span>
|
<span class="text-gray-400 dark:text-gray-500 text-sm">—</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-right text-xs text-gray-500 dark:text-gray-400">
|
<td class="hidden lg:table-cell px-4 py-2.5 text-right text-xs text-gray-500 dark:text-gray-400">
|
||||||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 text-right">
|
<td class="px-4 py-2.5 text-right">
|
||||||
|
|
@ -138,7 +138,8 @@
|
||||||
navigate={~p"/devices/new?site_id=#{site.id}"}
|
navigate={~p"/devices/new?site_id=#{site.id}"}
|
||||||
class="inline-flex items-center gap-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
class="inline-flex items-center gap-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
>
|
>
|
||||||
<.icon name="hero-plus" class="h-3.5 w-3.5" /> {t("Add Device")}
|
<.icon name="hero-plus" class="h-3.5 w-3.5" />
|
||||||
|
<span class="hidden sm:inline">{t("Add Device")}</span>
|
||||||
</.link>
|
</.link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@
|
||||||
|
|
||||||
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
||||||
<%!-- Left Column --%>
|
<%!-- Left Column --%>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6 min-w-0">
|
||||||
<%!-- Site Details --%>
|
<%!-- Site Details --%>
|
||||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
|
@ -397,7 +397,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%!-- Right Column --%>
|
<%!-- Right Column --%>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6 min-w-0">
|
||||||
<%!-- Device Health Grid --%>
|
<%!-- Device Health Grid --%>
|
||||||
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
|
|
@ -437,10 +437,16 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="text-xs text-gray-500 dark:text-gray-400">
|
<tr class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
<th class="px-3 py-2 text-left font-medium">{t("Device")}</th>
|
<th class="px-3 py-2 text-left font-medium">{t("Device")}</th>
|
||||||
<th class="px-3 py-2 text-left font-medium">{t("IP Address")}</th>
|
<th class="hidden sm:table-cell px-3 py-2 text-left font-medium">
|
||||||
<th class="px-3 py-2 text-right font-medium">{t("Subs")}</th>
|
{t("IP Address")}
|
||||||
|
</th>
|
||||||
|
<th class="hidden sm:table-cell px-3 py-2 text-right font-medium">
|
||||||
|
{t("Subs")}
|
||||||
|
</th>
|
||||||
<%= if @can_view_financials do %>
|
<%= if @can_view_financials do %>
|
||||||
<th class="px-3 py-2 text-right font-medium">{t("MRR")}</th>
|
<th class="hidden md:table-cell px-3 py-2 text-right font-medium">
|
||||||
|
{t("MRR")}
|
||||||
|
</th>
|
||||||
<% end %>
|
<% end %>
|
||||||
<th class="px-3 py-2 text-right font-medium">{t("Latency")}</th>
|
<th class="px-3 py-2 text-right font-medium">{t("Latency")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -462,10 +468,10 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 font-mono text-xs text-gray-500 dark:text-gray-400">
|
<td class="hidden sm:table-cell px-3 py-2 font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||||
{eq.ip_address}
|
{eq.ip_address}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
<td class="hidden sm:table-cell px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||||
<%= if di = @device_impact[eq.id] do %>
|
<%= if di = @device_impact[eq.id] do %>
|
||||||
{if di.count > 0, do: di.count, else: "—"}
|
{if di.count > 0, do: di.count, else: "—"}
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
@ -473,7 +479,7 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
<%= if @can_view_financials do %>
|
<%= if @can_view_financials do %>
|
||||||
<td class="px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
<td class="hidden md:table-cell px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||||
<%= if di = @device_impact[eq.id] do %>
|
<%= if di = @device_impact[eq.id] do %>
|
||||||
{if Decimal.gt?(di.mrr, 0), do: format_mrr(di.mrr), else: "—"}
|
{if Decimal.gt?(di.mrr, 0), do: format_mrr(di.mrr), else: "—"}
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
2026-03-12 — Mobile Responsive Improvements
|
||||||
|
* Improved layout across all pages for mobile devices
|
||||||
|
* Tables now hide less critical columns on small screens to prevent horizontal scrolling
|
||||||
|
* Action buttons in page headers compact to icon-only on small screens
|
||||||
|
* Device detail page values now consistently right-aligned for easier reading
|
||||||
|
|
||||||
2026-03-12 — Real-Time GraphQL API
|
2026-03-12 — Real-Time GraphQL API
|
||||||
* New time-series queries for sensor readings, interface traffic, and check results
|
* New time-series queries for sensor readings, interface traffic, and check results
|
||||||
* GraphQL subscriptions for live device status, alerts, and sensor reading updates
|
* GraphQL subscriptions for live device status, alerts, and sensor reading updates
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue