From 19abd5508a28553a1d5ea631cf3903f76058c85a Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 12 Mar 2026 16:49:59 -0500 Subject: [PATCH] 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 --- CHANGELOG.txt | 19 ++ .../components/core_components.ex | 6 +- .../live/agent_live/index.html.heex | 190 +++++++------- .../live/agent_live/show.html.heex | 14 +- .../live/dashboard_live.html.heex | 48 ++-- .../live/device_live/index.html.heex | 226 ++++++++-------- .../live/device_live/show.html.heex | 246 ++++++++++-------- .../live/schedule_live/show.html.heex | 206 ++++++++------- .../live/site_live/index.html.heex | 25 +- .../live/site_live/show.html.heex | 22 +- priv/static/changelog.txt | 6 + 11 files changed, 538 insertions(+), 470 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bcdbed73..14d0028f 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -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 feat: add real-time GraphQL API with time-series queries and subscriptions - Added absinthe_phoenix dependency for WebSocket subscription support diff --git a/lib/towerops_web/components/core_components.ex b/lib/towerops_web/components/core_components.ex index cfeb33f5..58f9a518 100644 --- a/lib/towerops_web/components/core_components.ex +++ b/lib/towerops_web/components/core_components.ex @@ -476,8 +476,8 @@ defmodule ToweropsWeb.CoreComponents do def header(assigns) do ~H""" -
-
+
+

{render_slot(@inner_block)}

@@ -485,7 +485,7 @@ defmodule ToweropsWeb.CoreComponents do {render_slot(@subtitle)}

-
{render_slot(@actions)}
+
{render_slot(@actions)}
""" end diff --git a/lib/towerops_web/live/agent_live/index.html.heex b/lib/towerops_web/live/agent_live/index.html.heex index 769aaafc..da192c9c 100644 --- a/lib/towerops_web/live/agent_live/index.html.heex +++ b/lib/towerops_web/live/agent_live/index.html.heex @@ -66,7 +66,7 @@
<% else %> -
+
<.table id="agents-table" rows={@streams.agent_tokens} @@ -183,108 +183,110 @@ "Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins." )}

- <.table - id="cloud-pollers-table" - rows={@streams.cloud_pollers} - row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end} - > - <:col :let={{_id, agent}} label={t("Name")}> -
- {agent.name} - <%= if agent.id == @global_default_cloud_poller_id do %> - - <.icon name="hero-check-circle" class="h-3 w-3" /> Default - - <% end %> - <%= if agent.allow_remote_debug do %> - - <.icon name="hero-bug-ant" class="h-3 w-3" /> Debug - - <% end %> -
- +
+ <.table + id="cloud-pollers-table" + rows={@streams.cloud_pollers} + row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end} + > + <:col :let={{_id, agent}} label={t("Name")}> +
+ {agent.name} + <%= if agent.id == @global_default_cloud_poller_id do %> + + <.icon name="hero-check-circle" class="h-3 w-3" /> Default + + <% end %> + <%= if agent.allow_remote_debug do %> + + <.icon name="hero-bug-ant" class="h-3 w-3" /> Debug + + <% end %> +
+ - <:col :let={{_id, agent}} label={t("Status")}> - <% {status, label} = agent_status(agent) %> - - - {label} - - + <:col :let={{_id, agent}} label={t("Status")}> + <% {status, label} = agent_status(agent) %> + + + {label} + + - <:col :let={{_id, agent}} label={t("Device")}> - <% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %> -
- {counts.total} total -
-
- {counts.direct} direct - <%= if counts.total > counts.direct do %> - · {counts.total - counts.direct} inherited - <% end %> -
- - - <:col :let={{_id, agent}} label={t("Last Seen")}> -
- <.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} /> -
- <%= if agent.last_seen_at do %> + <:col :let={{_id, agent}} label={t("Device")}> + <% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %> +
+ {counts.total} total +
- <.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 %>
- <% end %> - <%= if agent.last_ip do %> -
- {agent.last_ip} -
- <% end %> - + - <:col :let={{_id, agent}} label={t("Version")}> - <%= if agent.metadata["version"] do %> -
- {format_agent_version(agent.metadata["version"])} + <:col :let={{_id, agent}} label={t("Last Seen")}> +
+ <.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
- <% end %> - + <%= if agent.last_seen_at do %> +
+ <.timestamp datetime={agent.last_seen_at} timezone={@timezone} format="absolute" /> +
+ <% end %> + <%= if agent.last_ip do %> +
+ {agent.last_ip} +
+ <% end %> + - <:col :let={{_id, agent}} label={t("Created")}> -
- {ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)} -
- + <:col :let={{_id, agent}} label={t("Version")}> + <%= if agent.metadata["version"] do %> +
+ {format_agent_version(agent.metadata["version"])} +
+ <% end %> + - <:action :let={{_id, agent}}> - <%= if agent.enabled do %> -
- <.button - type="button" - phx-click="show_setup" - phx-value-id={agent.id} - variant="secondary" - > - {t("View Setup")} - - <.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")} - + <:col :let={{_id, agent}} label={t("Created")}> +
+ {ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
- <% else %> - Disabled - <% end %> - - + + + <:action :let={{_id, agent}}> + <%= if agent.enabled do %> +
+ <.button + type="button" + phx-click="show_setup" + phx-value-id={agent.id} + variant="secondary" + > + {t("View Setup")} + + <.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")} + +
+ <% else %> + Disabled + <% end %> + + +
<% end %> diff --git a/lib/towerops_web/live/agent_live/show.html.heex b/lib/towerops_web/live/agent_live/show.html.heex index 099e1e4a..e70743d3 100644 --- a/lib/towerops_web/live/agent_live/show.html.heex +++ b/lib/towerops_web/live/agent_live/show.html.heex @@ -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" > - <.icon name="hero-arrow-up-tray" class="h-5 w-5" /> Update + <.icon name="hero-arrow-up-tray" class="h-5 w-5" /> + <.link 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" > - <.icon name="hero-pencil" class="h-5 w-5" /> Edit Agent + <.icon name="hero-pencil" class="h-5 w-5" /> + <.link 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" /> + diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index 7d5dc155..3f5d8ed5 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -363,24 +363,24 @@ <%!-- Alerts table --%> <%= if @active_alerts != [] do %> -
+
- - - - - + @@ -407,7 +407,7 @@ ]} /> - - - -
+ {t("Device")} + {t("Alert")} + +
-
+
<.link 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" @@ -416,36 +416,36 @@ {alert.device.ip_address}
{alert.device.site.name}
+ {alert.message} + + +