From 3a10682d8ca52bfb4a82fe9900c12a45c473529a Mon Sep 17 00:00:00 2001 From: Graham McIntie Date: Fri, 13 Mar 2026 15:37:48 -0500 Subject: [PATCH] Add dashboard setup checklist + onboarding progress - Setup Progress bar on dashboard when devices exist but setup incomplete - Tracks: devices added, billing connected, alert routing configured, on-call schedule created - Each step links to the relevant setup page - Dismissible with X button - Progress bar shows visual completion percentage - Optional steps labeled (billing, on-call) --- lib/towerops_web/live/dashboard_live.ex | 25 ++++++++ .../live/dashboard_live.html.heex | 58 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/lib/towerops_web/live/dashboard_live.ex b/lib/towerops_web/live/dashboard_live.ex index 10dad352..e7cdc97a 100644 --- a/lib/towerops_web/live/dashboard_live.ex +++ b/lib/towerops_web/live/dashboard_live.ex @@ -150,10 +150,35 @@ defmodule ToweropsWeb.DashboardLive do |> assign(:uptime_percentage, uptime_percentage) |> assign(:recent_activity, recent_activity) |> assign(:favicon_status, favicon_status(Map.get(status_counts, :down, 0), length(active_alerts))) + |> assign(:setup_checklist, build_setup_checklist(organization, device_count)) |> load_insights(organization_id) |> load_recent_config_changes(organization_id) end + defp build_setup_checklist(organization, device_count) do + integrations = Towerops.Integrations.list_integrations(organization.id) + has_billing = Enum.any?(integrations, fn i -> i.provider in ~w(gaiia sonar splynx visp) and i.enabled end) + has_alert_routing = organization.alert_routing != "builtin" or has_escalation_policy?(organization) + schedules = Towerops.OnCall.list_schedules(organization.id) + + steps = [ + %{key: :devices, label: "Add devices to monitor", done: device_count > 0, link: ~p"/devices/new"}, + %{key: :billing, label: "Connect billing platform", done: has_billing, link: ~p"/orgs/#{organization.slug}/settings?tab=integrations", optional: true}, + %{key: :alerting, label: "Configure alert routing", done: has_alert_routing, link: ~p"/orgs/#{organization.slug}/settings?tab=general", optional: false}, + %{key: :oncall, label: "Create on-call schedule", done: length(schedules) > 0, link: ~p"/schedules/new", optional: true} + ] + + completed = Enum.count(steps, & &1.done) + %{steps: steps, completed: completed, total: length(steps), all_done: completed == length(steps)} + end + + defp has_escalation_policy?(organization) do + case Towerops.OnCall.list_escalation_policies(organization.id) do + [] -> false + _ -> true + end + end + defp load_recent_config_changes(socket, organization_id) do alias Towerops.ConfigChanges diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index 0fd1161d..59578402 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -108,6 +108,64 @@ <% end %> + <%!-- Setup Checklist — shown when devices exist but setup isn't complete --%> + <%= if assigns[:setup_checklist] && !@setup_checklist.all_done && @device_count > 0 do %> +
+
+
+ <.icon name="hero-rocket-launch" class="h-5 w-5 text-blue-600 dark:text-blue-400" /> +

+ {t("Setup Progress")} +

+ + {@setup_checklist.completed}/{@setup_checklist.total} + +
+ +
+ <%!-- Progress bar --%> +
+
+
+
+
+ <%= for step <- @setup_checklist.steps do %> + <.link + navigate={step.link} + class={[ + "flex items-center gap-2.5 rounded-md px-3 py-2 text-sm transition-colors", + if(step.done, + do: "text-gray-400 dark:text-gray-500", + else: "text-gray-700 dark:text-gray-300 hover:bg-blue-100/50 dark:hover:bg-blue-900/20" + ) + ]} + > + <%= if step.done do %> + <.icon name="hero-check-circle-solid" class="h-5 w-5 text-green-500 shrink-0" /> + <% else %> +
+ <% end %> + + {step.label} + + <%= if step[:optional] do %> + optional + <% end %> + + <% end %> +
+
+ <% end %> + <%= if assigns[:summary] && @device_count > 0 do %> <%!-- ═══════════════════════════════════════════════ CONDENSED STATUS BAR — single dense line