Revert "refactor: apply custom color palette across entire codebase"

This reverts commit 5a7cdc7138.
This commit is contained in:
Graham McIntire 2026-06-23 12:26:57 -05:00
parent 5a7cdc7138
commit 253657280b
104 changed files with 5476 additions and 6073 deletions

View file

@ -25,27 +25,27 @@ defmodule ToweropsWeb.Components.ConsentPrompt do
>
<div class="flex min-h-screen items-center justify-center p-4">
<!-- Backdrop -->
<div class="fixed inset-0 bg-cool-steel-500 dark:bg-cool-steel-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
</div>
<!-- Modal panel -->
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-cool-steel-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-cerulean-100 dark:bg-cerulean-900 sm:mx-0 sm:h-10 sm:w-10">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900 sm:mx-0 sm:h-10 sm:w-10">
<.icon
name="hero-information-circle"
class="h-6 w-6 text-cerulean-600 dark:text-cerulean-400"
class="h-6 w-6 text-blue-600 dark:text-blue-400"
/>
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left w-full">
<h3
class="text-lg font-semibold leading-6 text-cool-steel-900 dark:text-white"
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="consent-modal-title"
>
{gettext("Updated Policies - Action Required")}
</h3>
<div class="mt-4 space-y-4">
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{gettext(
"We've updated our policies. Please review and accept the changes to continue using Towerops."
)}
@ -62,31 +62,31 @@ defmodule ToweropsWeb.Components.ConsentPrompt do
phx-click="toggle_consent"
phx-value-policy={policy_type}
required
class="h-4 w-4 rounded border-zinc-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-cerulean-500 dark:checked:border-cerulean-500"
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
<label
for={"consent-#{policy_type}"}
class="text-cool-steel-700 dark:text-cool-steel-300"
class="text-gray-700 dark:text-gray-300"
>
{gettext("I agree to the updated")}
<.link
href={policy_link(policy_type)}
target="_blank"
class="font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{policy_name(policy_type)}
</.link>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">*</span>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>
<% end %>
</div>
<div class="mt-4 pt-4 border-t border-cool-steel-200 dark:border-cool-steel-700">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs text-gray-500 dark:text-gray-400">
{gettext(
"By accepting, you acknowledge that you have read and agree to the updated policies. You can review your consent history at any time from your account settings."
)}
@ -100,7 +100,7 @@ defmodule ToweropsWeb.Components.ConsentPrompt do
type="button"
phx-click="accept_updated_policies"
phx-value-user-id={@user_id}
class="inline-flex w-full justify-center rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-cerulean-500 sm:ml-3 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
class="inline-flex w-full justify-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 sm:ml-3 sm:w-auto disabled:opacity-50 disabled:cursor-not-allowed"
id="accept-policies-button"
>
{gettext("Accept and Continue")}

View file

@ -28,7 +28,7 @@ defmodule ToweropsWeb.Components.CookieConsent do
<%= if @requires_consent do %>
<div
id="cookie-consent-banner"
class="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-cool-steel-900 border-t border-cool-steel-200 dark:border-cool-steel-700 shadow-lg"
class="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 shadow-lg"
role="dialog"
aria-live="polite"
aria-label="Cookie consent"
@ -38,15 +38,15 @@ defmodule ToweropsWeb.Components.CookieConsent do
<div class="flex-1 flex items-start gap-3">
<.icon
name="hero-information-circle"
class="h-6 w-6 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0"
/>
<div class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
<div class="text-sm text-gray-700 dark:text-gray-300">
<p class="font-medium mb-1">We respect your privacy</p>
<p>
We use essential cookies to maintain your session and ensure the security of our service. We do not use tracking or advertising cookies.
<a
href="/privacy"
class="underline hover:text-cool-steel-900 dark:hover:text-cool-steel-100 font-medium"
class="underline hover:text-gray-900 dark:hover:text-gray-100 font-medium"
>
Learn more
</a>
@ -58,7 +58,7 @@ defmodule ToweropsWeb.Components.CookieConsent do
<button
type="button"
data-cookie-consent="accept"
class="flex-1 sm:flex-none px-4 py-2 bg-cerulean-600 text-white text-sm font-medium rounded-lg hover:bg-cerulean-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500 dark:focus:ring-offset-cool-steel-900"
class="flex-1 sm:flex-none px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900"
>
Accept
</button>

View file

@ -61,7 +61,7 @@ defmodule ToweropsWeb.CoreComponents do
{@rest}
>
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<div class="pointer-events-auto w-full max-w-sm translate-y-0 transform rounded-lg bg-white opacity-100 shadow-lg outline-1 outline-black/5 transition duration-300 ease-out sm:translate-x-0 dark:bg-cool-steel-800 dark:-outline-offset-1 dark:outline-white/10 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0">
<div class="pointer-events-auto w-full max-w-sm translate-y-0 transform rounded-lg bg-white opacity-100 shadow-lg outline-1 outline-black/5 transition duration-300 ease-out sm:translate-x-0 dark:bg-gray-800 dark:-outline-offset-1 dark:outline-white/10 starting:translate-y-2 starting:opacity-0 starting:sm:translate-x-2 starting:sm:translate-y-0">
<div class="p-4">
<div class="flex items-start">
<div class="shrink-0">
@ -73,15 +73,15 @@ defmodule ToweropsWeb.CoreComponents do
<.icon
:if={@kind == :error}
name="hero-x-circle"
class="size-6 text-sweet-salmon-400"
class="size-6 text-red-400"
/>
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p :if={@title} class="text-sm font-medium text-cool-steel-900 dark:text-white">
<p :if={@title} class="text-sm font-medium text-gray-900 dark:text-white">
{@title}
</p>
<p class={[
"text-sm text-cool-steel-500 dark:text-cool-steel-400",
"text-sm text-gray-500 dark:text-gray-400",
@title && "mt-1"
]}>
{render_flash_message(msg)}
@ -90,7 +90,7 @@ defmodule ToweropsWeb.CoreComponents do
<div class="ml-4 flex shrink-0">
<button
type="button"
class="inline-flex rounded-md text-cool-steel-400 hover:text-cool-steel-500 focus:outline-2 focus:outline-offset-2 focus:outline-cerulean-600 dark:hover:text-white dark:focus:outline-cerulean-500"
class="inline-flex rounded-md text-gray-400 hover:text-gray-500 focus:outline-2 focus:outline-offset-2 focus:outline-indigo-600 dark:hover:text-white dark:focus:outline-indigo-500"
aria-label={gettext("close")}
>
<span class="sr-only">Close</span>
@ -114,7 +114,7 @@ defmodule ToweropsWeb.CoreComponents do
{@text}
<.link
navigate={@link_url}
class="font-medium text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-medium text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{@link_text}
</.link>
@ -142,15 +142,15 @@ defmodule ToweropsWeb.CoreComponents do
def button(%{rest: rest} = assigns) do
variants = %{
"primary" =>
"bg-cerulean-600 text-white hover:bg-cerulean-500 active:bg-cerulean-700 focus:ring-cerulean-500 shadow-md hover:shadow-lg dark:bg-cerulean-500 dark:hover:bg-cerulean-400 dark:active:bg-cerulean-600",
"bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-700 focus:ring-blue-500 shadow-md hover:shadow-lg dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-600",
"secondary" =>
"bg-white text-cool-steel-700 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 focus:ring-cerulean-500 dark:bg-cool-steel-800 dark:text-cool-steel-200 dark:ring-cool-steel-600 dark:hover:bg-cool-steel-700",
"bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:ring-blue-500 dark:bg-gray-800 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-gray-700",
"danger" =>
"bg-sweet-salmon-600 text-white hover:bg-sweet-salmon-500 active:bg-sweet-salmon-700 focus:ring-sweet-salmon-500 dark:bg-sweet-salmon-500 dark:hover:bg-sweet-salmon-400",
"bg-red-600 text-white hover:bg-red-500 active:bg-red-700 focus:ring-red-500 dark:bg-red-500 dark:hover:bg-red-400",
"ghost" =>
"bg-transparent text-cool-steel-700 hover:bg-cool-steel-100 focus:ring-cerulean-500 shadow-none dark:text-cool-steel-300 dark:hover:bg-cool-steel-800",
"bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-blue-500 shadow-none dark:text-gray-300 dark:hover:bg-gray-800",
nil =>
"bg-white text-cool-steel-900 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 focus:ring-cerulean-500 dark:bg-cool-steel-800 dark:text-white dark:ring-cool-steel-700 dark:hover:bg-cool-steel-700"
"bg-white text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:ring-blue-500 dark:bg-gray-800 dark:text-white dark:ring-gray-700 dark:hover:bg-gray-700"
}
sizes = %{
@ -162,7 +162,7 @@ defmodule ToweropsWeb.CoreComponents do
assigns =
assign_new(assigns, :class, fn ->
[
"inline-flex items-center justify-center gap-2 rounded-lg font-semibold shadow-sm transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:focus:ring-offset-cool-steel-900",
"inline-flex items-center justify-center gap-2 rounded-lg font-semibold shadow-sm transition-all duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:focus:ring-offset-gray-900",
Map.fetch!(sizes, assigns.size),
Map.fetch!(variants, assigns[:variant])
]
@ -325,11 +325,11 @@ defmodule ToweropsWeb.CoreComponents do
aria-invalid={to_string(@errors != [])}
class={
@class ||
"h-4 w-4 rounded border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-800"
"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
}
{@rest}
/>
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">{@label}</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">{@label}</span>
</label>
<.error :if={@errors != []} id={@error_id}>
<span :for={msg <- @errors}>{msg}</span>
@ -343,11 +343,7 @@ defmodule ToweropsWeb.CoreComponents do
~H"""
<div class="mb-4">
<label
:if={@label}
for={@id}
class="block text-sm/6 font-medium text-cool-steel-900 dark:text-white"
>
<label :if={@label} for={@id} class="block text-sm/6 font-medium text-gray-900 dark:text-white">
{@label}
</label>
<div class={["mt-2 grid grid-cols-1", @label && ""]}>
@ -358,12 +354,12 @@ defmodule ToweropsWeb.CoreComponents do
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-cool-steel-900 outline-1 -outline-offset-1 placeholder:text-cool-steel-400 focus:outline-2 focus:-outline-offset-2 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-cool-steel-800",
"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800",
@errors == [] &&
"outline-cool-steel-300 focus:outline-cerulean-600 dark:focus:outline-cerulean-500",
"outline-gray-300 focus:outline-indigo-600 dark:focus:outline-indigo-500",
@errors != [] &&
(@error_class ||
"outline-sweet-salmon-300 focus:outline-sweet-salmon-500 dark:outline-sweet-salmon-700 dark:focus:outline-sweet-salmon-500")
"outline-red-300 focus:outline-red-500 dark:outline-red-700 dark:focus:outline-red-500")
]}
multiple={@multiple}
{@rest}
@ -376,7 +372,7 @@ defmodule ToweropsWeb.CoreComponents do
fill="currentColor"
data-slot="icon"
aria-hidden="true"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-cool-steel-400 sm:size-4"
class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-400 sm:size-4"
>
<path
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
@ -398,7 +394,7 @@ defmodule ToweropsWeb.CoreComponents do
~H"""
<div class="mb-4">
<label>
<span :if={@label} class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span :if={@label} class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{@label}
</span>
<textarea
@ -408,12 +404,12 @@ defmodule ToweropsWeb.CoreComponents do
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-cool-steel-800 dark:text-white",
"block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-gray-800 dark:text-white",
@errors == [] &&
"ring-cool-steel-300 focus:ring-cerulean-600 dark:ring-cool-steel-700 dark:focus:ring-cerulean-500",
"ring-gray-300 focus:ring-blue-600 dark:ring-gray-700 dark:focus:ring-blue-500",
@errors != [] &&
(@error_class ||
"ring-sweet-salmon-300 focus:ring-sweet-salmon-500 dark:ring-sweet-salmon-700 dark:focus:ring-sweet-salmon-500")
"ring-red-300 focus:ring-red-500 dark:ring-red-700 dark:focus:ring-red-500")
]}
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
@ -432,7 +428,7 @@ defmodule ToweropsWeb.CoreComponents do
~H"""
<div class="mb-4">
<label>
<span :if={@label} class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span :if={@label} class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{@label}
</span>
<input
@ -444,12 +440,12 @@ defmodule ToweropsWeb.CoreComponents do
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-cool-steel-800 dark:text-white",
"block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-gray-800 dark:text-white",
@errors == [] &&
"ring-cool-steel-300 focus:ring-cerulean-600 dark:ring-cool-steel-700 dark:focus:ring-cerulean-500",
"ring-gray-300 focus:ring-blue-600 dark:ring-gray-700 dark:focus:ring-blue-500",
@errors != [] &&
(@error_class ||
"ring-sweet-salmon-300 focus:ring-sweet-salmon-500 dark:ring-sweet-salmon-700 dark:focus:ring-sweet-salmon-500")
"ring-red-300 focus:ring-red-500 dark:ring-red-700 dark:focus:ring-red-500")
]}
{@rest}
/>
@ -464,7 +460,7 @@ defmodule ToweropsWeb.CoreComponents do
# Helper used by inputs to generate form errors
defp error(assigns) do
~H"""
<p class="mt-2 flex gap-2 items-center text-sm text-sweet-salmon-600 dark:text-sweet-salmon-400">
<p class="mt-2 flex gap-2 items-center text-sm text-red-600 dark:text-red-400">
<.icon name="hero-exclamation-circle" class="h-5 w-5 flex-shrink-0" />
{render_slot(@inner_block)}
</p>
@ -482,10 +478,10 @@ defmodule ToweropsWeb.CoreComponents do
~H"""
<header class={[@actions != [] && "flex flex-wrap items-start justify-between gap-3", "mb-8"]}>
<div class="min-w-0">
<h1 class="text-3xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
{render_slot(@inner_block)}
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p :if={@subtitle != []} class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{render_slot(@subtitle)}
</p>
</div>
@ -531,12 +527,12 @@ defmodule ToweropsWeb.CoreComponents do
~H"""
<div class="overflow-hidden shadow-sm outline-1 outline-black/5 sm:rounded-lg dark:shadow-none dark:-outline-offset-1 dark:outline-white/10">
<table class="relative min-w-full divide-y divide-cool-steel-300 dark:divide-white/15">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/75">
<table class="relative min-w-full divide-y divide-gray-300 dark:divide-white/15">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th
:for={col <- @col}
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-cool-steel-900 sm:pl-6 dark:text-cool-steel-200"
class="py-3.5 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 sm:pl-6 dark:text-gray-200"
>
{col[:label]}
</th>
@ -548,19 +544,19 @@ defmodule ToweropsWeb.CoreComponents do
<tbody
id={@id}
phx-update={is_struct(@rows, Phoenix.LiveView.LiveStream) && "stream"}
class="divide-y divide-cool-steel-200 bg-white dark:divide-white/10 dark:bg-cool-steel-800/50"
class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-gray-800/50"
>
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class="transition-colors hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/80"
class="transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/80"
>
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={[
i == 0 && "font-medium text-cool-steel-900 dark:text-white",
i > 0 && "text-cool-steel-500 dark:text-cool-steel-400",
i == 0 && "font-medium text-gray-900 dark:text-white",
i > 0 && "text-gray-500 dark:text-gray-400",
@row_link && "p-0 text-sm whitespace-nowrap",
!@row_link && "py-4 pr-3 pl-4 text-sm whitespace-nowrap sm:pl-6",
@row_click && "cursor-pointer"
@ -610,10 +606,10 @@ defmodule ToweropsWeb.CoreComponents do
def list(assigns) do
~H"""
<div class="overflow-hidden bg-white shadow rounded-lg divide-y divide-cool-steel-200 dark:bg-cool-steel-800/50 dark:divide-white/10">
<div class="overflow-hidden bg-white shadow rounded-lg divide-y divide-gray-200 dark:bg-gray-800/50 dark:divide-white/10">
<div :for={item <- @item} class="px-6 py-4">
<dt class="text-sm font-medium text-cool-steel-900 dark:text-white">{item.title}</dt>
<dd class="mt-1 text-sm text-cool-steel-700 dark:text-cool-steel-300">{render_slot(item)}</dd>
<dt class="text-sm font-medium text-gray-900 dark:text-white">{item.title}</dt>
<dd class="mt-1 text-sm text-gray-700 dark:text-gray-300">{render_slot(item)}</dd>
</div>
</div>
"""
@ -694,20 +690,20 @@ defmodule ToweropsWeb.CoreComponents do
def pagination(assigns) do
~H"""
<%= if @meta.total_pages > 1 do %>
<div class="mt-6 flex items-center justify-between border-t border-cool-steel-200 dark:border-white/10 pt-6">
<div class="mt-6 flex items-center justify-between border-t border-gray-200 dark:border-white/10 pt-6">
<!-- Mobile: Previous/Next only -->
<div class="flex flex-1 justify-between sm:hidden">
<.link
:if={@meta.page > 1}
patch={build_pagination_path(@path, @params, @meta.page - 1)}
class="relative inline-flex items-center rounded-md border border-cool-steel-300 bg-white px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Previous
</.link>
<.link
:if={@meta.page < @meta.total_pages}
patch={build_pagination_path(@path, @params, @meta.page + 1)}
class="relative ml-3 inline-flex items-center rounded-md border border-cool-steel-300 bg-white px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Next
</.link>
@ -715,7 +711,7 @@ defmodule ToweropsWeb.CoreComponents do
<!-- Desktop: Full pagination with page numbers -->
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-cool-steel-700 dark:text-cool-steel-400">
<p class="text-sm text-gray-700 dark:text-gray-400">
Showing <span class="font-medium">{(@meta.page - 1) * @meta.per_page + 1}</span>
to
<span class="font-medium">{min(@meta.page * @meta.per_page, @meta.total_count)}</span>
@ -729,7 +725,7 @@ defmodule ToweropsWeb.CoreComponents do
<.link
:if={@meta.page > 1}
patch={build_pagination_path(@path, @params, @meta.page - 1)}
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-cool-steel-400 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:ring-white/10 dark:hover:bg-cool-steel-800"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Previous</span>
<.icon name="hero-chevron-left" class="h-5 w-5" />
@ -737,18 +733,18 @@ defmodule ToweropsWeb.CoreComponents do
<!-- Page numbers with ellipsis -->
<%= for page_num <- pagination_range(@meta.page, @meta.total_pages) do %>
<%= if page_num == :ellipsis do %>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-cool-steel-700 ring-1 ring-inset ring-cool-steel-300 dark:text-cool-steel-400 dark:ring-white/10">
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 dark:text-gray-400 dark:ring-white/10">
...
</span>
<% else %>
<.link
patch={build_pagination_path(@path, @params, page_num)}
class={[
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-cool-steel-300 dark:ring-white/10",
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-white/10",
if @meta.page == page_num do
"z-10 bg-cerulean-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cerulean-600"
"z-10 bg-blue-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
else
"text-cool-steel-900 hover:bg-cool-steel-50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
end
]}
>
@ -760,7 +756,7 @@ defmodule ToweropsWeb.CoreComponents do
<.link
:if={@meta.page < @meta.total_pages}
patch={build_pagination_path(@path, @params, @meta.page + 1)}
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-cool-steel-400 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:ring-white/10 dark:hover:bg-cool-steel-800"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<span class="sr-only">Next</span>
<.icon name="hero-chevron-right" class="h-5 w-5" />
@ -882,7 +878,7 @@ defmodule ToweropsWeb.CoreComponents do
def signal_badge(%{value: nil} = assigns) do
~H"""
<span class="inline-flex items-center rounded-md bg-cool-steel-50 px-2 py-1 text-xs font-medium text-cool-steel-600 ring-1 ring-cool-steel-500/10 ring-inset dark:bg-cool-steel-400/10 dark:text-cool-steel-400 dark:ring-cool-steel-400/20">
<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20">
</span>
"""
@ -907,13 +903,13 @@ defmodule ToweropsWeb.CoreComponents do
@color == "green" &&
"bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/20",
@color == "blue" &&
"bg-cerulean-50 text-cerulean-700 ring-cerulean-600/20 dark:bg-cerulean-400/10 dark:text-cerulean-400 dark:ring-cerulean-400/20",
"bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
@color == "yellow" &&
"bg-wheat-50 text-wheat-800 ring-wheat-600/20 dark:bg-wheat-400/10 dark:text-wheat-500 dark:ring-wheat-400/20",
"bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20",
@color == "orange" &&
"bg-orange-50 text-orange-700 ring-orange-600/20 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/20",
@color == "red" &&
"bg-sweet-salmon-50 text-sweet-salmon-700 ring-sweet-salmon-600/10 dark:bg-sweet-salmon-400/10 dark:text-sweet-salmon-400 dark:ring-sweet-salmon-400/20"
"bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20"
]}
title={@label}
>
@ -941,7 +937,7 @@ defmodule ToweropsWeb.CoreComponents do
def snr_badge(%{value: nil} = assigns) do
~H"""
<span class="inline-flex items-center rounded-md bg-cool-steel-50 px-2 py-1 text-xs font-medium text-cool-steel-600 ring-1 ring-cool-steel-500/10 ring-inset dark:bg-cool-steel-400/10 dark:text-cool-steel-400 dark:ring-cool-steel-400/20">
<span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20">
</span>
"""
@ -966,13 +962,13 @@ defmodule ToweropsWeb.CoreComponents do
@color == "green" &&
"bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/20",
@color == "blue" &&
"bg-cerulean-50 text-cerulean-700 ring-cerulean-600/20 dark:bg-cerulean-400/10 dark:text-cerulean-400 dark:ring-cerulean-400/20",
"bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
@color == "yellow" &&
"bg-wheat-50 text-wheat-800 ring-wheat-600/20 dark:bg-wheat-400/10 dark:text-wheat-500 dark:ring-wheat-400/20",
"bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20",
@color == "orange" &&
"bg-orange-50 text-orange-700 ring-orange-600/20 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/20",
@color == "red" &&
"bg-sweet-salmon-50 text-sweet-salmon-700 ring-sweet-salmon-600/10 dark:bg-sweet-salmon-400/10 dark:text-sweet-salmon-400 dark:ring-sweet-salmon-400/20"
"bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20"
]}
title={@label}
>

View file

@ -49,25 +49,25 @@ defmodule ToweropsWeb.Layouts do
assigns = Map.put_new(assigns, :requires_cookie_consent, false)
~H"""
<div class="flex min-h-screen flex-col bg-cool-steel-50 dark:bg-cool-steel-950">
<header class="border-b border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-900">
<div class="flex min-h-screen flex-col bg-gray-50 dark:bg-gray-950">
<header class="border-b border-gray-200 bg-white dark:border-white/10 dark:bg-gray-900">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<nav class="flex h-16 items-center justify-between">
<div class="flex items-center">
<.link navigate={~p"/"} class="text-xl font-bold text-cool-steel-900 dark:text-white">
<.link navigate={~p"/"} class="text-xl font-bold text-gray-900 dark:text-white">
Towerops
</.link>
</div>
<div class="flex items-center gap-x-4">
<.link
navigate={~p"/users/log-in"}
class="text-sm font-medium text-cool-steel-700 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:text-white"
class="text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
Log in
</.link>
<.link
navigate={~p"/users/register"}
class="rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500"
>
Sign up
</.link>
@ -142,11 +142,11 @@ defmodule ToweropsWeb.Layouts do
<div
id="sidebar-wrapper"
phx-hook="SidebarCollapse"
class="min-h-screen bg-cool-steel-50 dark:bg-cool-steel-950"
class="min-h-screen bg-gray-50 dark:bg-gray-950"
>
<!-- Impersonation Banner -->
<%= if @current_scope && @current_scope.impersonating? do %>
<div class="bg-wheat-400 border-b-2 border-wheat-600 relative z-50">
<div class="bg-yellow-400 border-b-2 border-yellow-600 relative z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
@ -158,7 +158,7 @@ defmodule ToweropsWeb.Layouts do
<.link
href={~p"/admin/impersonate"}
method="delete"
class="px-3 py-1 bg-white dark:bg-cool-steel-900 text-wheat-900 dark:text-wheat-200 hover:bg-wheat-50 dark:hover:bg-cool-steel-800 rounded font-medium text-sm"
class="px-3 py-1 bg-white dark:bg-gray-900 text-yellow-900 dark:text-yellow-200 hover:bg-yellow-50 dark:hover:bg-gray-800 rounded font-medium text-sm"
>
Exit
</.link>
@ -168,7 +168,7 @@ defmodule ToweropsWeb.Layouts do
<% end %>
<!-- Slim Top Bar -->
<header class="sticky top-0 z-40 border-b border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-900">
<header class="sticky top-0 z-40 border-b border-gray-200 bg-white dark:border-white/10 dark:bg-gray-900">
<div class="flex h-14 items-center justify-between px-4">
<div class="flex items-center gap-2">
<!-- Mobile hamburger -->
@ -184,7 +184,7 @@ defmodule ToweropsWeb.Layouts do
to: "#mobile-menu-button"
)
}
class="inline-flex items-center justify-center rounded-md p-2 text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 focus:outline-none dark:text-cool-steel-400 dark:hover:bg-cool-steel-800 dark:hover:text-cool-steel-200 lg:hidden"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 lg:hidden"
>
<span class="sr-only">{t("Toggle main menu")}</span>
<.icon name="hero-bars-3" class="size-6" />
@ -192,7 +192,7 @@ defmodule ToweropsWeb.Layouts do
<.link
navigate={~p"/dashboard"}
class="text-lg font-bold text-cool-steel-900 dark:text-white"
class="text-lg font-bold text-gray-900 dark:text-white"
>
Towerops
</.link>
@ -205,12 +205,12 @@ defmodule ToweropsWeb.Layouts do
type="button"
id="global-search-trigger"
phx-hook="GlobalSearchTrigger"
class="inline-flex items-center gap-2 rounded-lg border border-cool-steel-200 bg-cool-steel-50 px-3 py-1.5 text-sm text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:border-white/10 dark:bg-white/5 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-200 transition-colors"
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200 transition-colors"
title="Search (⌘K)"
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" />
<span class="hidden sm:inline text-xs">{t("Search")}</span>
<kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border border-cool-steel-300 bg-cool-steel-100 px-1.5 py-0.5 text-[10px] font-medium text-cool-steel-500 dark:border-white/20 dark:bg-white/10 dark:text-cool-steel-400">
<kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border border-gray-300 bg-gray-100 px-1.5 py-0.5 text-[10px] font-medium text-gray-500 dark:border-white/20 dark:bg-white/10 dark:text-gray-400">
K
</kbd>
</button>
@ -219,12 +219,12 @@ defmodule ToweropsWeb.Layouts do
<.link
:if={@current_organization}
navigate={~p"/alerts"}
class="relative p-2 rounded-md text-cool-steel-500 hover:text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:text-cool-steel-200 dark:hover:bg-cool-steel-800 transition-colors"
class="relative p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-colors"
title={t("Alerts")}
>
<.icon name="hero-bell" class="h-5 w-5" />
<%= if @unresolved_alert_count > 0 do %>
<span class="absolute top-0.5 right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-sweet-salmon-500 px-0.5 text-[10px] font-bold leading-none text-white ring-1 ring-white dark:ring-cool-steel-950">
<span class="absolute top-0.5 right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-0.5 text-[10px] font-bold leading-none text-white ring-1 ring-white dark:ring-gray-950">
<%= if @unresolved_alert_count > 100 do %>
&nbsp;
<% else %>
@ -237,7 +237,7 @@ defmodule ToweropsWeb.Layouts do
<!-- Help icon -->
<.link
navigate={~p"/help"}
class="p-2 rounded-md text-cool-steel-500 hover:text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:text-cool-steel-200 dark:hover:bg-cool-steel-800 transition-colors"
class="p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-colors"
title={t("Help")}
>
<.icon name="hero-question-mark-circle" class="h-5 w-5" />
@ -254,7 +254,7 @@ defmodule ToweropsWeb.Layouts do
JS.toggle(to: "#org-menu")
|> JS.toggle_attribute({"aria-expanded", "true", "false"}, to: "#org-menu-button")
}
class="inline-flex items-center justify-center rounded-full p-1.5 text-cool-steel-500 hover:text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:text-cool-steel-200 dark:hover:bg-cool-steel-800 transition-colors"
class="inline-flex items-center justify-center rounded-full p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-800 transition-colors"
title={t("Menu")}
>
<.icon name="hero-user-circle" class="h-6 w-6" />
@ -263,7 +263,7 @@ defmodule ToweropsWeb.Layouts do
<div
id="org-menu"
role="menu"
class="hidden absolute top-full right-0 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 z-50 dark:bg-cool-steel-800 dark:ring-white/10"
class="hidden absolute top-full right-0 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 z-50 dark:bg-gray-800 dark:ring-white/10"
phx-click-away={
JS.hide(to: "#org-menu")
|> JS.set_attribute({"aria-expanded", "false"}, to: "#org-menu-button")
@ -274,7 +274,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/admin"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#org-menu")}
>
{t("Admin Panel")}
@ -283,7 +283,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/users/settings"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#org-menu")}
>
{t("User Settings")}
@ -291,7 +291,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/mobile/qr-login"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#org-menu")}
>
<span class="flex items-center gap-2">
@ -302,7 +302,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/users/my-data"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#org-menu")}
>
{t("My Data")}
@ -310,17 +310,17 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/changelog"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#org-menu")}
>
{t("What's New")}
</.link>
<div class="border-t border-cool-steel-100 dark:border-white/10"></div>
<div class="border-t border-gray-100 dark:border-white/10"></div>
<.link
role="menuitem"
href={~p"/users/log-out"}
method="delete"
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
>
{t("Log out")}
</.link>
@ -337,14 +337,14 @@ defmodule ToweropsWeb.Layouts do
<aside
:if={@current_organization || (@current_scope && @current_scope.user)}
id="app-sidebar"
class="sticky top-14 left-0 z-30 hidden lg:flex lg:flex-col bg-cool-steel-800 dark:bg-cool-steel-900 border-r border-cool-steel-700 dark:border-white/10 overflow-y-auto overflow-x-hidden h-[calc(100vh-3.5rem)]"
class="sticky top-14 left-0 z-30 hidden lg:flex lg:flex-col bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-white/10 overflow-y-auto overflow-x-hidden h-[calc(100vh-3.5rem)]"
>
<!-- Nav sections -->
<nav class="flex-1 px-2 py-4 space-y-1">
<!-- MONITOR section -->
<p
data-sidebar-section
class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500"
class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500"
>
{t("Monitor")}
</p>
@ -389,7 +389,7 @@ defmodule ToweropsWeb.Layouts do
<!-- RESPOND section -->
<p
data-sidebar-section
class="px-3 mt-4 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-300 dark:text-cool-steel-400"
class="px-3 mt-4 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500"
>
{t("Respond")}
</p>
@ -416,7 +416,7 @@ defmodule ToweropsWeb.Layouts do
<!-- ANALYZE section -->
<p
data-sidebar-section
class="px-3 mt-4 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-300 dark:text-cool-steel-400"
class="px-3 mt-4 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500"
>
{t("Analyze")}
</p>
@ -455,7 +455,7 @@ defmodule ToweropsWeb.Layouts do
</nav>
<!-- Bottom pinned: org switcher + settings + collapse toggle -->
<div class="border-t border-cool-steel-200 dark:border-white/10 px-2 py-3.5 space-y-1">
<div class="border-t border-gray-200 dark:border-white/10 px-2 py-3.5 space-y-1">
<!-- Org switcher -->
<div :if={@current_organization} class="relative">
<button
@ -469,7 +469,7 @@ defmodule ToweropsWeb.Layouts do
to: "#sidebar-org-switcher-button"
)
}
class="flex items-center gap-2 w-full rounded-md px-3 py-2 text-sm font-medium text-cool-steel-200 hover:bg-cool-steel-700 hover:text-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700 dark:hover:text-cool-steel-100 transition-colors"
class="flex items-center gap-2 w-full rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white transition-colors"
data-sidebar-tooltip={@current_organization.name}
>
<.icon name="hero-building-office-2" class="h-5 w-5 shrink-0" />
@ -483,7 +483,7 @@ defmodule ToweropsWeb.Layouts do
<div
id="sidebar-org-switcher-menu"
role="menu"
class="hidden absolute bottom-full left-0 mb-1 w-48 origin-bottom-left rounded-md bg-white shadow-lg ring-1 ring-black/5 z-50 dark:bg-cool-steel-800 dark:ring-white/10"
class="hidden absolute bottom-full left-0 mb-1 w-48 origin-bottom-left rounded-md bg-white shadow-lg ring-1 ring-black/5 z-50 dark:bg-gray-800 dark:ring-white/10"
phx-click-away={
JS.hide(to: "#sidebar-org-switcher-menu")
|> JS.set_attribute({"aria-expanded", "false"}, to: "#sidebar-org-switcher-button")
@ -493,7 +493,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/orgs"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#sidebar-org-switcher-menu")}
>
{t("Switch Organization")}
@ -501,7 +501,7 @@ defmodule ToweropsWeb.Layouts do
<.link
role="menuitem"
navigate={~p"/orgs/#{@current_organization.slug}/settings"}
class="block px-4 py-2 text-sm text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
phx-click={JS.hide(to: "#sidebar-org-switcher-menu")}
>
{t("Organization Settings")}
@ -523,7 +523,7 @@ defmodule ToweropsWeb.Layouts do
<button
type="button"
data-sidebar-toggle
class="flex items-center justify-center w-full rounded-md px-3 py-2 text-cool-steel-300 hover:bg-cool-steel-700 hover:text-cool-steel-100 dark:hover:bg-cool-steel-700 dark:hover:text-cool-steel-200 transition-colors"
class="flex items-center justify-center w-full rounded-md px-3 py-2 text-gray-400 hover:bg-gray-50 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300 transition-colors"
title={t("Toggle sidebar")}
>
<.icon name="hero-chevron-double-left" class="h-5 w-5 sidebar-collapse-icon" />
@ -544,13 +544,13 @@ defmodule ToweropsWeb.Layouts do
>
</div>
<!-- Panel -->
<div class="fixed inset-y-0 left-0 w-72 bg-cool-steel-800 dark:bg-cool-steel-900 shadow-xl overflow-y-auto">
<div class="flex items-center justify-between px-4 py-4 border-b border-cool-steel-700 dark:border-white/10">
<span class="text-lg font-bold text-cool-steel-100 dark:text-white">Towerops</span>
<div class="fixed inset-y-0 left-0 w-72 bg-white dark:bg-gray-900 shadow-xl overflow-y-auto">
<div class="flex items-center justify-between px-4 py-4 border-b border-gray-200 dark:border-white/10">
<span class="text-lg font-bold text-gray-900 dark:text-white">Towerops</span>
<button
type="button"
phx-click={JS.hide(to: "#mobile-menu")}
class="p-2 rounded-md text-cool-steel-300 hover:bg-cool-steel-700 dark:hover:bg-cool-steel-700"
class="p-2 rounded-md text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800"
>
<.icon name="hero-x-mark" class="size-5" />
</button>
@ -559,19 +559,19 @@ defmodule ToweropsWeb.Layouts do
<!-- Org name -->
<div
:if={@current_organization}
class="px-4 py-3 border-b border-cool-steel-100 dark:border-white/5"
class="px-4 py-3 border-b border-gray-100 dark:border-white/5"
>
<p class="text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Organization")}
</p>
<p class="text-sm font-semibold text-cool-steel-900 dark:text-white mt-0.5">
<p class="text-sm font-semibold text-gray-900 dark:text-white mt-0.5">
{@current_organization.name}
</p>
</div>
<!-- MONITOR -->
<div class="px-2 py-3">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{t("Monitor")}
</p>
<.mobile_nav_link navigate={~p"/dashboard"} active={@active_page == "dashboard"}>
@ -596,8 +596,8 @@ defmodule ToweropsWeb.Layouts do
</div>
<!-- RESPOND -->
<div class="px-2 py-3 border-t border-cool-steel-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<div class="px-2 py-3 border-t border-gray-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{t("Respond")}
</p>
<.mobile_nav_link navigate={~p"/alerts"} active={@active_page == "alerts"}>
@ -612,8 +612,8 @@ defmodule ToweropsWeb.Layouts do
</div>
<!-- ANALYZE -->
<div class="px-2 py-3 border-t border-cool-steel-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<div class="px-2 py-3 border-t border-gray-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{t("Analyze")}
</p>
<.mobile_nav_link navigate={~p"/insights"} active={@active_page == "insights"}>
@ -628,8 +628,8 @@ defmodule ToweropsWeb.Layouts do
</div>
<!-- Account -->
<div class="px-2 py-3 border-t border-cool-steel-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<div class="px-2 py-3 border-t border-gray-100 dark:border-white/5">
<p class="px-3 mb-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
{t("Account")}
</p>
<.mobile_nav_link
@ -661,22 +661,22 @@ defmodule ToweropsWeb.Layouts do
id="beta-banner"
phx-hook="BetaBannerDismiss"
data-always-show={@always_show_banner}
class="mb-6 rounded-lg bg-cerulean-50 dark:bg-cerulean-900/20 p-4 border border-cerulean-200 dark:border-cerulean-800"
class="mb-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4 border border-blue-200 dark:border-blue-800"
>
<div class="flex items-start gap-3">
<.icon
name="hero-heart"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0 mt-0.5"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
/>
<div class="flex-1">
<h3 class="text-sm font-semibold text-cerulean-900 dark:text-cerulean-300 mb-1">
<h3 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
Thanks for testing Towerops!
</h3>
<p class="text-sm text-cerulean-800 dark:text-cerulean-300">
<p class="text-sm text-blue-800 dark:text-blue-300">
We'd love to hear your feedback, suggestions, or help you with any questions. Please email us at
<a
href="mailto:hi@towerops.net"
class="font-medium underline hover:text-cerulean-900 dark:hover:text-cerulean-200"
class="font-medium underline hover:text-blue-900 dark:hover:text-blue-200"
>
hi@towerops.net
</a>
@ -686,7 +686,7 @@ defmodule ToweropsWeb.Layouts do
<button
type="button"
data-dismiss
class="flex-shrink-0 rounded-md p-1.5 text-cerulean-600 dark:text-cerulean-400 hover:bg-cerulean-100 dark:hover:bg-cerulean-800/50 focus:outline-none focus:ring-2 focus:ring-cerulean-600 dark:focus:ring-cerulean-400"
class="flex-shrink-0 rounded-md p-1.5 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-800/50 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:focus:ring-blue-400"
aria-label="Dismiss banner"
>
<.icon name="hero-x-mark" class="h-5 w-5" />
@ -763,9 +763,9 @@ defmodule ToweropsWeb.Layouts do
"relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
if(@active,
do:
"bg-cerulean-800/30 text-cerulean-300 border-l-2 border-cerulean-400 dark:bg-cerulean-900/20 dark:text-cerulean-400",
"bg-blue-50 text-blue-700 border-l-2 border-blue-600 dark:bg-blue-900/20 dark:text-blue-400",
else:
"text-cool-steel-200 hover:bg-cool-steel-700 hover:text-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700 dark:hover:text-cool-steel-100"
"text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
)
]}
data-sidebar-tooltip={@label}
@ -774,7 +774,7 @@ defmodule ToweropsWeb.Layouts do
<%= if @badge > 0 do %>
<span
data-sidebar-text
class="ml-auto inline-flex items-center rounded-full bg-sweet-salmon-100 px-2 py-0.5 text-xs font-medium text-sweet-salmon-700 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
class="ml-auto inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
>
{if @badge > 99, do: "99+", else: @badge}
</span>
@ -794,9 +794,9 @@ defmodule ToweropsWeb.Layouts do
class={[
"flex items-center gap-3 rounded-md px-3 py-2 text-base font-medium",
if(@active,
do: "bg-cool-steel-100 text-cool-steel-900 dark:bg-cool-steel-800 dark:text-white",
do: "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white",
else:
"text-cool-steel-600 hover:bg-cool-steel-50 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800 dark:hover:text-white"
"text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white"
)
]}
>
@ -874,8 +874,8 @@ defmodule ToweropsWeb.Layouts do
assigns = Map.put_new(assigns, :requires_cookie_consent, false)
~H"""
<div class="flex min-h-screen flex-col bg-cool-steel-50 dark:bg-cool-steel-950">
<nav class="bg-white border-b border-cool-steel-200 dark:border-white/10 dark:bg-cool-steel-900">
<div class="flex min-h-screen flex-col bg-gray-50 dark:bg-gray-950">
<nav class="bg-white border-b border-gray-200 dark:border-white/10 dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-14 sm:h-16">
<div class="flex items-center">
@ -888,7 +888,7 @@ defmodule ToweropsWeb.Layouts do
|> JS.toggle(to: "#admin-menu-icon-open")
|> JS.toggle(to: "#admin-menu-icon-close")
}
class="inline-flex items-center justify-center rounded-md p-2 text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 focus:outline-none dark:text-cool-steel-400 dark:hover:bg-cool-steel-800 dark:hover:text-cool-steel-200 sm:hidden"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 sm:hidden"
>
<span class="sr-only">Open admin menu</span>
<span id="admin-menu-icon-open"><.icon name="hero-bars-3" class="size-6" /></span>
@ -899,7 +899,7 @@ defmodule ToweropsWeb.Layouts do
<.link
navigate={~p"/admin"}
class="flex items-center text-lg sm:text-xl font-bold text-cool-steel-900 dark:text-white ml-2 sm:ml-0"
class="flex items-center text-lg sm:text-xl font-bold text-gray-900 dark:text-white ml-2 sm:ml-0"
>
<span class="hidden sm:inline">Towerops Admin</span>
<span class="sm:hidden">Admin</span>
@ -909,25 +909,25 @@ defmodule ToweropsWeb.Layouts do
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
<.link
navigate={~p"/admin/users"}
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-cool-steel-700 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:text-white"
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
Users
</.link>
<.link
navigate={~p"/admin/organizations"}
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-cool-steel-700 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:text-white"
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
Organizations
</.link>
<.link
navigate={~p"/admin/security"}
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-cool-steel-700 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:text-white"
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
IP Allowlist
</.link>
<.link
navigate={~p"/admin/errors"}
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-cool-steel-700 hover:text-cool-steel-900 dark:text-cool-steel-300 dark:hover:text-white"
class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
Exceptions
</.link>
@ -936,7 +936,7 @@ defmodule ToweropsWeb.Layouts do
<div class="flex items-center gap-1 sm:gap-2">
<.link
navigate={~p"/orgs"}
class="text-xs sm:text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="text-xs sm:text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<span class="hidden sm:inline">Back to App</span>
<span class="sm:hidden">App</span>
@ -947,7 +947,7 @@ defmodule ToweropsWeb.Layouts do
<!-- Mobile menu -->
<div class="hidden sm:hidden" id="admin-mobile-menu">
<div class="space-y-1 px-2 pb-3 pt-2 border-t border-cool-steel-200 dark:border-white/10">
<div class="space-y-1 px-2 pb-3 pt-2 border-t border-gray-200 dark:border-white/10">
<.mobile_nav_link navigate={~p"/admin/users"} active={false}>
Users
</.mobile_nav_link>
@ -994,28 +994,28 @@ defmodule ToweropsWeb.Layouts do
|> assign(:formatted_time, TimeHelpers.format_iso8601(build_time, timezone))
~H"""
<footer class="border-t border-cool-steel-700 bg-cool-steel-800 dark:border-white/10 dark:bg-cool-steel-900">
<footer class="border-t border-gray-200 bg-white dark:border-white/10 dark:bg-gray-900">
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<div class="flex flex-col items-center gap-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="flex flex-col items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<p>Copyright © {Date.utc_today().year} Towerops</p>
<p class="text-xs">
<.link
navigate={~p"/privacy"}
class="hover:text-cool-steel-900 dark:hover:text-cool-steel-200 underline decoration-dotted underline-offset-2"
class="hover:text-gray-900 dark:hover:text-gray-200 underline decoration-dotted underline-offset-2"
>
Privacy Policy
</.link>
<span class="mx-2">·</span>
<.link
navigate={~p"/terms"}
class="hover:text-cool-steel-900 dark:hover:text-cool-steel-200 underline decoration-dotted underline-offset-2"
class="hover:text-gray-900 dark:hover:text-gray-200 underline decoration-dotted underline-offset-2"
>
Terms of Service
</.link>
<span class="mx-2">·</span>
<.link
href="/changelog.txt"
class="hover:text-cool-steel-900 dark:hover:text-cool-steel-200 underline decoration-dotted underline-offset-2"
class="hover:text-gray-900 dark:hover:text-gray-200 underline decoration-dotted underline-offset-2"
>
Changelog
</.link>

View file

@ -98,7 +98,7 @@
<body>
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-cerulean-600 focus:text-white focus:rounded-md focus:m-2"
class="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-blue-600 focus:text-white focus:rounded-md focus:m-2"
>
{t("Skip to main content")}
</a>

View file

@ -53,7 +53,7 @@ defmodule ToweropsWeb.MarketingLayouts do
</div>
<.link
navigate={~p"/users/register"}
class="group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 bg-cerulean-600 text-white hover:text-slate-100 hover:bg-cerulean-500 active:bg-cerulean-800 active:text-cerulean-100 focus-visible:outline-cerulean-600"
class="group inline-flex items-center justify-center rounded-full py-2 px-4 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600"
>
<span>
Get started <span class="hidden lg:inline">today</span>

View file

@ -9,7 +9,7 @@ defmodule ToweropsWeb.Components.Skeletons do
assigns = assign_new(assigns, :class, fn -> "" end)
~H"""
<div class={["rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer", @class]}></div>
<div class={["rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer", @class]}></div>
"""
end
@ -21,7 +21,7 @@ defmodule ToweropsWeb.Components.Skeletons do
def skeleton_card(assigns) do
~H"""
<div class={[
"animate-pulse rounded-lg border border-cool-steel-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-cool-steel-800/50",
"animate-pulse rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50",
@class
]}>
<.skel class="h-3 w-24" />
@ -39,13 +39,13 @@ defmodule ToweropsWeb.Components.Skeletons do
def skeleton_table(assigns) do
~H"""
<div class="overflow-hidden rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
<!-- Header -->
<div class="border-b border-cool-steel-200 bg-cool-steel-50 px-6 py-3.5 dark:border-white/10 dark:bg-cool-steel-800/75">
<div class="border-b border-gray-200 bg-gray-50 px-6 py-3.5 dark:border-white/10 dark:bg-gray-800/75">
<div class="flex gap-8">
<div
:for={_ <- 1..@cols}
class="h-3 w-20 animate-pulse rounded bg-cool-steel-300 dark:bg-cool-steel-600"
class="h-3 w-20 animate-pulse rounded bg-gray-300 dark:bg-gray-600"
>
</div>
</div>
@ -54,16 +54,16 @@ defmodule ToweropsWeb.Components.Skeletons do
<div
:for={i <- 1..@rows}
class={[
"border-b border-cool-steel-100 px-6 py-4 last:border-0 dark:border-white/5",
rem(i, 2) == 0 && "bg-cool-steel-50/50 dark:bg-cool-steel-800/25"
"border-b border-gray-100 px-6 py-4 last:border-0 dark:border-white/5",
rem(i, 2) == 0 && "bg-gray-50/50 dark:bg-gray-800/25"
]}
>
<div class="flex items-center gap-8">
<div class="h-3 w-24 animate-pulse rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
<div class="h-3 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer">
</div>
<div
:for={_ <- 2..@cols}
class="h-3 w-16 animate-pulse rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer"
class="h-3 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"
>
</div>
</div>
@ -80,17 +80,15 @@ defmodule ToweropsWeb.Components.Skeletons do
def skeleton_stat(assigns) do
~H"""
<div class={[
"animate-pulse rounded-lg border border-cool-steel-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-cool-steel-800/50",
"animate-pulse rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50",
@class
]}>
<div class="flex items-center gap-2">
<div class="h-5 w-5 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer"></div>
<div class="h-3 w-20 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer"></div>
</div>
<div class="mt-2 h-8 w-16 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="mt-1.5 h-2.5 w-14 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
<div class="h-5 w-5 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
<div class="h-3 w-20 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
</div>
<div class="mt-2 h-8 w-16 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
<div class="mt-1.5 h-2.5 w-14 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
</div>
"""
end
@ -109,7 +107,7 @@ defmodule ToweropsWeb.Components.Skeletons do
<div
:for={w <- [28, 24, 32, 20]}
class={[
"h-9 animate-pulse rounded-lg bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer",
"h-9 animate-pulse rounded-lg bg-gray-200 dark:bg-gray-700 skeleton-shimmer",
"w-#{w}"
]}
>
@ -119,42 +117,37 @@ defmodule ToweropsWeb.Components.Skeletons do
<div class="mt-8 grid gap-8 lg:grid-cols-5">
<!-- Main column: alerts/activity -->
<div class="lg:col-span-3">
<div class="h-5 w-32 animate-pulse rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer mb-4">
<div class="h-5 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer mb-4">
</div>
<div class="space-y-3">
<div
:for={_ <- 1..3}
class="animate-pulse rounded-lg border border-cool-steel-200 bg-white p-4 dark:border-white/10 dark:bg-cool-steel-800/50"
class="animate-pulse rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-800/50"
>
<div class="flex items-center gap-3">
<div class="h-8 w-8 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
<div class="flex-1">
<div class="h-3.5 w-40 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="mt-2 h-3 w-64 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
<div class="h-3.5 w-40 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
<div class="mt-2 h-3 w-64 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer">
</div>
</div>
<div class="h-3 w-12 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="h-3 w-12 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
</div>
</div>
</div>
</div>
<!-- Sidebar column: recent/quick stats -->
<div class="lg:col-span-2">
<div class="h-5 w-24 animate-pulse rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer mb-4">
<div class="h-5 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer mb-4">
</div>
<div class="space-y-3">
<div
:for={_ <- 1..4}
class="animate-pulse rounded-lg border border-cool-steel-200 bg-white p-3 dark:border-white/10 dark:bg-cool-steel-800/50"
class="animate-pulse rounded-lg border border-gray-200 bg-white p-3 dark:border-white/10 dark:bg-gray-800/50"
>
<div class="flex items-center justify-between">
<div class="h-3 w-28 rounded bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="h-5 w-10 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 skeleton-shimmer">
</div>
<div class="h-3 w-28 rounded bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
<div class="h-5 w-10 rounded-full bg-gray-200 dark:bg-gray-700 skeleton-shimmer"></div>
</div>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -6,14 +6,14 @@
<title>{t("404 - Page Not Found")}</title>
<link phx-track-static rel="stylesheet" href="/assets/css/app.css" />
</head>
<body class="bg-white dark:bg-cool-steel-900">
<body class="bg-white dark:bg-gray-900">
<div class="min-h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl w-full text-center">
<!-- Logo -->
<img src="/images/towerops_logo.png" alt="Towerops" class="h-16 w-auto mx-auto mb-8" />
<!-- Error Code -->
<h1 class="text-9xl font-bold text-cerulean-600 mb-4">404</h1>
<h1 class="text-9xl font-bold text-blue-600 mb-4">404</h1>
<!-- Squirrel Image -->
<div class="mb-8">
@ -35,7 +35,7 @@
<!-- Back Home Button -->
<a
href="/"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-cerulean-600 text-white hover:bg-cerulean-500 active:bg-cerulean-800 transition-colors"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-800 transition-colors"
>
{t("Go back home")}
</a>

View file

@ -6,14 +6,14 @@
<title>{t("500 - Server Error")}</title>
<link phx-track-static rel="stylesheet" href="/assets/css/app.css" />
</head>
<body class="bg-white dark:bg-cool-steel-900">
<body class="bg-white dark:bg-gray-900">
<div class="min-h-screen flex flex-col items-center justify-center px-4 sm:px-6 lg:px-8">
<div class="max-w-2xl w-full text-center">
<!-- Logo -->
<img src="/images/towerops_logo.png" alt="Towerops" class="h-16 w-auto mx-auto mb-8" />
<!-- Error Code -->
<h1 class="text-9xl font-bold text-cerulean-600 mb-4">500</h1>
<h1 class="text-9xl font-bold text-blue-600 mb-4">500</h1>
<!-- Error Message -->
<h2 class="text-3xl font-bold text-slate-900 mb-4">
@ -26,7 +26,7 @@
<!-- Back Home Button -->
<a
href="/"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-cerulean-600 text-white hover:bg-cerulean-500 active:bg-cerulean-800 transition-colors"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-800 transition-colors"
>
{t("Go back home")}
</a>

View file

@ -18,7 +18,7 @@
<li>
<a
href="#introduction"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Introduction
</a>
@ -26,7 +26,7 @@
<li>
<a
href="#authentication"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Authentication
</a>
@ -34,7 +34,7 @@
<li>
<a
href="#endpoint"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Endpoint
</a>
@ -49,7 +49,7 @@
<li>
<a
href="#query-devices"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Devices
</a>
@ -57,7 +57,7 @@
<li>
<a
href="#query-sites"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Sites
</a>
@ -65,7 +65,7 @@
<li>
<a
href="#query-alerts"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Alerts
</a>
@ -73,7 +73,7 @@
<li>
<a
href="#query-agents"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Agents
</a>
@ -81,7 +81,7 @@
<li>
<a
href="#query-organization"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Organization
</a>
@ -89,7 +89,7 @@
<li>
<a
href="#query-members"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Members
</a>
@ -97,7 +97,7 @@
<li>
<a
href="#query-integrations"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Integrations
</a>
@ -105,7 +105,7 @@
<li>
<a
href="#query-activity"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Activity
</a>
@ -113,7 +113,7 @@
<li>
<a
href="#query-metrics"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Metrics & Interfaces
</a>
@ -121,7 +121,7 @@
<li>
<a
href="#query-time-series"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Time-Series
</a>
@ -129,7 +129,7 @@
<li>
<a
href="#query-checks"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Service Checks
</a>
@ -137,7 +137,7 @@
<li>
<a
href="#query-schedules"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
On-Call Schedules
</a>
@ -145,7 +145,7 @@
<li>
<a
href="#query-escalation-policies"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Escalation Policies
</a>
@ -153,7 +153,7 @@
<li>
<a
href="#query-maintenance-windows"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Maintenance Windows
</a>
@ -168,7 +168,7 @@
<li>
<a
href="#mutation-devices"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Device CRUD
</a>
@ -176,7 +176,7 @@
<li>
<a
href="#mutation-sites"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Site CRUD
</a>
@ -184,7 +184,7 @@
<li>
<a
href="#mutation-alerts"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Alert Actions
</a>
@ -192,7 +192,7 @@
<li>
<a
href="#mutation-agents"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Agent Management
</a>
@ -200,7 +200,7 @@
<li>
<a
href="#mutation-organization"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Organization
</a>
@ -208,7 +208,7 @@
<li>
<a
href="#mutation-members"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Members
</a>
@ -216,7 +216,7 @@
<li>
<a
href="#mutation-integrations"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Integrations
</a>
@ -224,7 +224,7 @@
<li>
<a
href="#mutation-checks"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Service Checks
</a>
@ -232,7 +232,7 @@
<li>
<a
href="#mutation-schedules"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Schedules
</a>
@ -240,7 +240,7 @@
<li>
<a
href="#mutation-escalation-policies"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Escalation Policies
</a>
@ -248,7 +248,7 @@
<li>
<a
href="#mutation-maintenance-windows"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Maintenance Windows
</a>
@ -263,7 +263,7 @@
<li>
<a
href="#subscriptions"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Real-Time Events
</a>
@ -278,7 +278,7 @@
<li>
<a
href="#types"
class="nav-link block py-1 pl-4 pr-3 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
All Types
</a>
@ -290,7 +290,7 @@
<li class="mt-auto">
<.link
navigate={~p"/docs/api"}
class="block py-1 text-sm text-cool-steel-600 transition hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="block py-1 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
← REST API Docs
</.link>
@ -307,25 +307,25 @@
<!-- Introduction -->
<section id="introduction" class="mb-16">
<h1 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">GraphQL API</h1>
<p class="mt-4 text-lg text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">
The TowerOps GraphQL API provides a flexible, strongly-typed interface for querying and managing your network infrastructure.
Request exactly the data you need in a single request — no over-fetching, no under-fetching.
</p>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
The GraphQL API uses the same authentication as the
<.link
navigate={~p"/docs/api"}
class="text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400"
class="text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
REST API
</.link>
and provides equivalent functionality with the added benefit of nested queries and precise field selection.
</p>
<div class="mt-6 rounded-lg border border-zinc-900/10 bg-cool-steel-50 p-4 dark:border-white/10 dark:bg-cool-steel-900/40">
<div class="mt-6 rounded-lg border border-zinc-900/10 bg-gray-50 p-4 dark:border-white/10 dark:bg-gray-900/40">
<h3 class="text-sm font-semibold text-zinc-900 dark:text-white">
Official clients
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Prefer infrastructure-as-code? The Towerops Ansible collection covers every resource exposed here: <a
href="https://codeberg.org/towerops/ansible"
class="font-medium text-emerald-600 hover:underline dark:text-emerald-400"
@ -339,7 +339,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Authentication
</h2>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
Authenticate using a Bearer token in the
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-mono text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
Authorization
@ -355,7 +355,7 @@
<!-- Endpoint -->
<section id="endpoint" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">Endpoint</h2>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
All GraphQL requests are sent as
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-mono text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
POST
@ -365,7 +365,7 @@
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
<pre class="text-sm text-green-400"><code>POST https://app.towerops.net/api/graphql</code></pre>
</div>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
Send a JSON body with a
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-mono text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
query
@ -388,7 +388,7 @@
</h2>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">devices</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
List all devices. Optionally filter by site or status.
</p>
<div class="mt-3 overflow-x-auto">
@ -404,7 +404,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">siteId</td>
<td class="py-2 pr-4">ID</td>
@ -444,7 +444,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">device</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">Get a single device by ID.</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">Get a single device by ID.</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw(~S[{
device(id: "device-uuid") {
@ -469,9 +469,7 @@
</h2>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">sites</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
List all sites for the organization.
</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">List all sites for the organization.</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw(~S[{
sites {
@ -487,7 +485,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">site</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">Get a single site by ID.</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">Get a single site by ID.</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw(~S[{ site(id: "site-uuid") { id name location } }]) %></code></pre>
</div>
@ -500,7 +498,7 @@
</h2>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">alerts</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
List alerts. Filter by status or device.
</p>
<div class="mt-3 overflow-x-auto">
@ -516,7 +514,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">status</td>
<td class="py-2 pr-4">String</td>
@ -671,9 +669,7 @@
</h2>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">deviceMetrics</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
Get time-series metrics for a device.
</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">Get time-series metrics for a device.</p>
<div class="mt-3 overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
@ -687,7 +683,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">deviceId</td>
<td class="py-2 pr-4">ID!</td>
@ -719,9 +715,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">deviceInterfaces</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
Get SNMP interfaces for a device.
</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">Get SNMP interfaces for a device.</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw(~S[{
deviceInterfaces(deviceId: "device-uuid") {
@ -744,7 +738,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Query: Time-Series
</h2>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
Query historical sensor readings, interface traffic rates, and monitoring check results.
All time-series queries support a
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-mono text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
@ -754,7 +748,7 @@
</p>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">deviceSensors</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
List all SNMP sensors discovered on a device.
</p>
<div class="mt-3 overflow-x-auto">
@ -770,7 +764,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr>
<td class="py-2 pr-4 font-mono text-xs">deviceId</td>
<td class="py-2 pr-4">ID!</td>
@ -794,7 +788,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">sensorReadings</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Get historical readings for a specific sensor.
</p>
<div class="mt-3 overflow-x-auto">
@ -810,7 +804,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">sensorId</td>
<td class="py-2 pr-4">ID!</td>
@ -836,7 +830,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">interfaceTraffic</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Get traffic rates (bits per second) computed from SNMP counter deltas for a network interface.
</p>
<div class="mt-3 overflow-x-auto">
@ -852,7 +846,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">interfaceId</td>
<td class="py-2 pr-4">ID!</td>
@ -879,7 +873,7 @@
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">checkResults</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Get historical results for a monitoring check (ping, HTTP, etc.).
</p>
<div class="mt-3 overflow-x-auto">
@ -895,7 +889,7 @@
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr class="border-b border-zinc-100 dark:border-zinc-800">
<td class="py-2 pr-4 font-mono text-xs">checkId</td>
<td class="py-2 pr-4">ID!</td>
@ -927,7 +921,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Query: Service Checks
</h2>
<p class="mt-2 text-base text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-base text-gray-600 dark:text-gray-400">
List and inspect HTTP, TCP, DNS, and ping service checks.
</p>
@ -967,7 +961,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Query: On-Call Schedules
</h2>
<p class="mt-2 text-base text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-base text-gray-600 dark:text-gray-400">
Inspect schedules and find out who is currently on call.
</p>
@ -1028,7 +1022,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Query: Escalation Policies
</h2>
<p class="mt-2 text-base text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-base text-gray-600 dark:text-gray-400">
Inspect escalation policies and their rules + targets.
</p>
@ -1074,7 +1068,7 @@
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Query: Maintenance Windows
</h2>
<p class="mt-2 text-base text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-base text-gray-600 dark:text-gray-400">
Inspect planned and active maintenance windows. The optional <code>filter</code>
argument accepts <code>active</code>, <code>upcoming</code>, or <code>past</code>.
</p>
@ -1169,9 +1163,7 @@
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">
updateSite / deleteSite
</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
Same pattern as device mutations.
</p>
<p class="mt-2 text-gray-600 dark:text-gray-400">Same pattern as device mutations.</p>
</section>
<!-- Mutations: Alerts -->
@ -1209,7 +1201,7 @@
</h2>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">createAgent</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Creates a new agent token. The raw token is only returned once.
</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
@ -1549,7 +1541,7 @@ mutation {
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Subscriptions
</h2>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
Subscribe to real-time events via WebSocket. Connect to
<code class="rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-mono text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200">
wss://app.towerops.net/socket/graphql
@ -1558,7 +1550,7 @@ mutation {
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Connecting</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Pass your API token in the connection parameters when establishing the WebSocket:
</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
@ -1576,7 +1568,7 @@ const absintheSocket = AbsintheSocket.create(phoenixSocket);]) %></code></pre>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">
deviceStatusChanged
</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Subscribe to device status changes. Optionally filter by a specific device ID, or omit to receive all device events in your organization.
</p>
<div class="mt-3 overflow-x-auto">
@ -1592,7 +1584,7 @@ const absintheSocket = AbsintheSocket.create(phoenixSocket);]) %></code></pre>
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr>
<td class="py-2 pr-4 font-mono text-xs">deviceId</td>
<td class="py-2 pr-4">ID</td>
@ -1623,7 +1615,7 @@ subscription {
</div>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">alertEvent</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Subscribe to alert lifecycle events (created, acknowledged, resolved) for your organization.
</p>
<div class="mt-4 rounded-lg bg-zinc-900 p-4 dark:bg-zinc-800">
@ -1643,7 +1635,7 @@ subscription {
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">
sensorReadingsUpdated
</h3>
<p class="mt-2 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-gray-600 dark:text-gray-400">
Subscribe to live sensor reading updates for a specific device. Delivers batched readings as they are polled.
</p>
<div class="mt-3 overflow-x-auto">
@ -1659,7 +1651,7 @@ subscription {
</th>
</tr>
</thead>
<tbody class="text-cool-steel-600 dark:text-cool-steel-400">
<tbody class="text-gray-600 dark:text-gray-400">
<tr>
<td class="py-2 pr-4 font-mono text-xs">deviceId</td>
<td class="py-2 pr-4">ID!</td>
@ -1691,12 +1683,12 @@ subscription {
<h2 class="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white">
Types Reference
</h2>
<p class="mt-4 text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-gray-600 dark:text-gray-400">
All field names use camelCase in GraphQL queries (Absinthe automatically converts from Elixir's snake_case).
</p>
<h3 class="mt-8 text-lg font-semibold text-zinc-900 dark:text-white">Device</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, name, ipAddress, description, displayOrder, status, lastCheckedAt, lastStatusChangeAt,
monitoringEnabled, checkIntervalSeconds, snmpEnabled, snmpVersion, snmpCommunity, snmpCommunitySource,
snmpPort, snmpTransport, snmpTransportSource, lastDiscoveryAt, lastSnmpPollAt,
@ -1706,7 +1698,7 @@ subscription {
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Site</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, name, description, location, displayOrder, snmpVersion, snmpCommunity, snmpPort, snmpTransport,
snmpv3SecurityLevel, snmpv3Username, snmpv3AuthProtocol, snmpv3PrivProtocol,
mikrotikUsername, mikrotikPort, mikrotikSshPort, mikrotikUseSsl, mikrotikEnabled,
@ -1714,19 +1706,19 @@ subscription {
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Alert</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, alertType, message, triggeredAt, acknowledgedAt, resolvedAt, emailSentAt,
deviceId, acknowledgedById, gaiiaImpact, insertedAt, device, acknowledgedByEmail
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Agent</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, name, enabled, isCloudPoller, allowRemoteDebug, lastSeenAt, lastIp, metadata,
organizationId, insertedAt, updatedAt, deviceCount
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Organization</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, name, slug, subscriptionPlan, useSites, snmpVersion, snmpCommunity, snmpPort, snmpTransport,
snmpv3SecurityLevel, snmpv3Username, snmpv3AuthProtocol, snmpv3PrivProtocol,
mikrotikEnabled, mikrotikUsername, mikrotikPort, mikrotikSshPort, mikrotikUseSsl,
@ -1734,61 +1726,61 @@ subscription {
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Member</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, email, role, isDefault, userId, insertedAt
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Integration</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, provider, enabled, syncIntervalMinutes, lastSyncedAt, lastSyncStatus, organizationId, insertedAt, updatedAt
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">ActivityItem</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
summary, detail, timestamp, severity, icon, deviceName, siteName, link, type
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Sensor</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
id, sensorType, sensorUnit, sensorDescr, currentValue, lastCheckedAt, monitored
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">SensorReading</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
sensorId, value, status, checkedAt
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">
InterfaceTrafficPoint
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
timestamp, inBps (Float), outBps (Float), inErrors (Int), outErrors (Int)
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">CheckResultPoint</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
checkId, value, status, checkedAt, responseTimeMs
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">DeviceStatusEvent</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
deviceId, deviceName, status, changedAt
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">AlertEvent</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
alertId, alertType, severity, message, deviceId, eventType, triggeredAt
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">
SensorReadingsEvent
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
deviceId, deviceName, readings (list of SensorReadingSnapshot: sensorId, sensorType, sensorDescr, sensorUnit, value, checkedAt)
</p>
<h3 class="mt-6 text-lg font-semibold text-zinc-900 dark:text-white">Utility Types</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<strong>DeleteResult:</strong>
success (Boolean!), message (String)<br />
<strong>TestResult:</strong>

View file

@ -1,16 +1,16 @@
<MarketingLayouts.marketing flash={@flash}>
<!-- Hero Section -->
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pt-20 pb-16 text-center lg:pt-32">
<div class="inline-flex items-center rounded-full px-4 py-1.5 mb-8 bg-cerulean-50 dark:bg-cerulean-950 text-cerulean-700 dark:text-cerulean-300 text-sm font-medium ring-1 ring-cerulean-600/20">
<div class="inline-flex items-center rounded-full px-4 py-1.5 mb-8 bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 text-sm font-medium ring-1 ring-blue-600/20">
<span class="mr-2">📡</span> Built specifically for WISP &amp; ISP operators
</div>
<h1 class="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-slate-900 dark:text-white sm:text-7xl">
See the
<span class="relative whitespace-nowrap text-cerulean-600 dark:text-cerulean-400">
<span class="relative whitespace-nowrap text-blue-600 dark:text-blue-400">
<svg
aria-hidden="true"
viewBox="0 0 418 42"
class="absolute top-2/3 left-0 h-[0.58em] w-full fill-cerulean-300/70 dark:fill-cerulean-500/40"
class="absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-300/70 dark:fill-blue-500/40"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
@ -24,19 +24,19 @@
When a tower goes down, instantly see which customers are affected, how much MRR is at risk,
and what to fix first.
</p>
<p class="mx-auto mt-4 max-w-2xl text-xl font-semibold tracking-tight text-cerulean-600 dark:text-cerulean-400">
<p class="mx-auto mt-4 max-w-2xl text-xl font-semibold tracking-tight text-blue-600 dark:text-blue-400">
First 10 devices always free. No credit card required.
</p>
<div class="mt-10 flex justify-center gap-x-6">
<.link
navigate={~p"/users/register"}
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 bg-cerulean-600 text-white hover:text-slate-100 hover:bg-cerulean-500 active:bg-cerulean-800 active:text-cerulean-100 focus-visible:outline-cerulean-600"
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 bg-blue-600 text-white hover:text-slate-100 hover:bg-blue-500 active:bg-blue-800 active:text-blue-100 focus-visible:outline-blue-600"
>
Start monitoring free →
</.link>
<a
href="#features"
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold ring-1 ring-slate-200 dark:ring-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:ring-slate-300 dark:hover:ring-slate-600 active:bg-slate-100 dark:active:bg-slate-800 focus-visible:outline-cerulean-600"
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold ring-1 ring-slate-200 dark:ring-slate-700 text-slate-700 dark:text-slate-300 hover:text-slate-900 dark:hover:text-white hover:ring-slate-300 dark:hover:ring-slate-600 active:bg-slate-100 dark:active:bg-slate-800 focus-visible:outline-blue-600"
>
See how it works
</a>
@ -58,24 +58,24 @@
</div>
<div class="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="rounded-lg bg-slate-800 p-6 text-center">
<div class="text-3xl font-bold text-sweet-salmon-400">45 min</div>
<div class="text-3xl font-bold text-red-400">45 min</div>
<div class="mt-2 text-sm text-slate-400">Average time to assess outage impact</div>
</div>
<div class="rounded-lg bg-slate-800 p-6 text-center">
<div class="text-3xl font-bold text-sweet-salmon-400">5+</div>
<div class="text-3xl font-bold text-red-400">5+</div>
<div class="mt-2 text-sm text-slate-400">Dashboards to check during incidents</div>
</div>
<div class="rounded-lg bg-slate-800 p-6 text-center">
<div class="text-3xl font-bold text-sweet-salmon-400">???</div>
<div class="text-3xl font-bold text-red-400">???</div>
<div class="mt-2 text-sm text-slate-400">Subscribers affected right now</div>
</div>
<div class="rounded-lg bg-slate-800 p-6 text-center">
<div class="text-3xl font-bold text-sweet-salmon-400">$???</div>
<div class="text-3xl font-bold text-red-400">$???</div>
<div class="mt-2 text-sm text-slate-400">Revenue at risk from this outage</div>
</div>
</div>
<div class="mt-12 text-center">
<p class="text-lg font-medium text-cerulean-400">
<p class="text-lg font-medium text-blue-400">
TowerOps gives you one place to see it all.
</p>
</div>
@ -86,14 +86,14 @@
<section
id="features"
aria-label="Features for WISP operators"
class="relative overflow-hidden bg-cerulean-600 pt-20 pb-28 sm:py-32"
class="relative overflow-hidden bg-blue-600 pt-20 pb-28 sm:py-32"
>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative">
<div class="max-w-2xl md:mx-auto md:text-center xl:max-w-none">
<h2 class="font-display text-3xl tracking-tight text-white sm:text-4xl md:text-5xl">
Operational intelligence built for WISPs.
</h2>
<p class="mt-6 text-lg tracking-tight text-cerulean-100">
<p class="mt-6 text-lg tracking-tight text-blue-100">
Not another generic monitoring tool. TowerOps understands tower sites, subscribers, and revenue.
</p>
</div>
@ -106,7 +106,7 @@
</div>
<h3 class="text-lg font-semibold text-white">Impact Dashboard</h3>
</div>
<p class="mt-4 text-sm text-cerulean-100">
<p class="mt-4 text-sm text-blue-100">
See MRR at risk, affected subscriber counts, and active incidents at a glance.
Know the business impact of every network event before your customers call.
</p>
@ -119,7 +119,7 @@
</div>
<h3 class="text-lg font-semibold text-white">Subscriber Trace</h3>
</div>
<p class="mt-4 text-sm text-cerulean-100">
<p class="mt-4 text-sm text-blue-100">
Help desk calls in? Search by name or account and instantly see their device status,
QoE scores, alerts, and whether nearby subscribers are also affected.
</p>
@ -132,7 +132,7 @@
</div>
<h3 class="text-lg font-semibold text-white">Smart Alert Triage</h3>
</div>
<p class="mt-4 text-sm text-cerulean-100">
<p class="mt-4 text-sm text-blue-100">
Alerts grouped by tower site and ranked by severity and subscriber impact.
Filter by critical/unresolved/all. Resolve directly from the alert view.
</p>
@ -146,7 +146,7 @@
</div>
<h3 class="text-lg font-semibold text-white">Tower Site Intelligence</h3>
</div>
<p class="mt-4 text-sm text-cerulean-100">
<p class="mt-4 text-sm text-blue-100">
Enriched site pages with subscriber counts, device health grids, active alerts,
latency charts, and active alerts — all on one screen.
</p>
@ -159,7 +159,7 @@
</div>
<h3 class="text-lg font-semibold text-white">Billing &amp; QoE Integrations</h3>
</div>
<p class="mt-4 text-sm text-cerulean-100">
<p class="mt-4 text-sm text-blue-100">
Connect Gaiia, Sonar, Splynx, or VISP for subscriber data, Preseem for QoE metrics,
NetBox for DCIM, and PagerDuty for alerting. TowerOps pulls it all together.
</p>
@ -187,7 +187,7 @@
<!-- Step 1 -->
<div class="relative">
<div class="flex items-center gap-x-4">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-cerulean-600 text-white font-semibold text-lg">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-white font-semibold text-lg">
1
</div>
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">
@ -205,7 +205,7 @@
<!-- Step 2 -->
<div class="relative">
<div class="flex items-center gap-x-4">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-cerulean-600 text-white font-semibold text-lg">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-white font-semibold text-lg">
2
</div>
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">
@ -217,25 +217,25 @@
TowerOps automatically maps subscribers to devices and tower sites.
</p>
<div class="mt-6 flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
Gaiia
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
Sonar
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
Preseem
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
Splynx
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
VISP
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
NetBox
</span>
<span class="inline-flex items-center rounded-full bg-cerulean-50 dark:bg-cerulean-950 px-3 py-1 text-xs font-medium text-cerulean-700 dark:text-cerulean-300 ring-1 ring-cerulean-600/20">
<span class="inline-flex items-center rounded-full bg-blue-50 dark:bg-blue-950 px-3 py-1 text-xs font-medium text-blue-700 dark:text-blue-300 ring-1 ring-blue-600/20">
PagerDuty
</span>
</div>
@ -243,7 +243,7 @@
<!-- Step 3 -->
<div class="relative">
<div class="flex items-center gap-x-4">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-cerulean-600 text-white font-semibold text-lg">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-blue-600 text-white font-semibold text-lg">
3
</div>
<h3 class="text-xl font-semibold text-slate-900 dark:text-white">
@ -254,12 +254,9 @@
Your dashboard lights up with subscriber impact, revenue at risk, device health,
and site status. Help desk gets subscriber trace. NOC gets alert triage. Everyone wins.
</p>
<div class="mt-6 rounded-lg border-2 border-cerulean-200 dark:border-cerulean-800 bg-white dark:bg-white/5 p-4">
<div class="mt-6 rounded-lg border-2 border-blue-200 dark:border-blue-800 bg-white dark:bg-white/5 p-4">
<div class="flex items-center gap-2">
<.icon
name="hero-chart-bar"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400"
/>
<.icon name="hero-chart-bar" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span class="text-sm font-medium text-slate-900 dark:text-white">
Impact Dashboard
</span>
@ -328,7 +325,7 @@
</p>
</div>
<div class="mt-16 mx-auto max-w-xl">
<div class="rounded-2xl bg-white dark:bg-slate-800 p-8 shadow-lg dark:shadow-black/30 ring-2 ring-cerulean-600 text-center">
<div class="rounded-2xl bg-white dark:bg-slate-800 p-8 shadow-lg dark:shadow-black/30 ring-2 ring-blue-600 text-center">
<div class="mt-2">
<span class="text-5xl font-bold text-slate-900 dark:text-white">$2</span>
<span class="text-lg text-slate-500 dark:text-slate-400">/device/month</span>
@ -343,63 +340,63 @@
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Unlimited devices
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Unlimited agents &amp; networks
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> SNMP monitoring &amp; alerts
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Impact dashboard
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> All billing integrations
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Preseem &amp; PagerDuty
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Subscriber trace &amp; alert triage
</div>
<%!-- Config change tracking hidden — feature temporarily disabled --%>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> Team access &amp; email alerts
</div>
<div class="flex items-center gap-2">
<.icon
name="hero-check"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/> API access
</div>
</div>
<.link
navigate={~p"/users/register"}
class="mt-10 inline-block rounded-full px-8 py-3 text-sm font-semibold text-white bg-cerulean-600 hover:bg-cerulean-500"
class="mt-10 inline-block rounded-full px-8 py-3 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-500"
>
Get started free
</.link>
@ -408,7 +405,7 @@
<p class="mt-8 text-center text-sm text-slate-500 dark:text-slate-400">
No credit card required. Need a custom plan for 500+ devices? <a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-500 dark:hover:text-cerulean-300 underline"
class="text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 underline"
>Talk to us</a>.
</p>
</div>
@ -455,19 +452,19 @@
</section>
<!-- Final CTA -->
<section aria-label="Get started" class="relative overflow-hidden bg-cerulean-600 py-32">
<section aria-label="Get started" class="relative overflow-hidden bg-blue-600 py-32">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 relative">
<div class="mx-auto max-w-lg text-center">
<h2 class="font-display text-3xl tracking-tight text-white sm:text-4xl">
Stop guessing. Start seeing impact.
</h2>
<p class="mt-4 text-lg tracking-tight text-cerulean-100">
<p class="mt-4 text-lg tracking-tight text-blue-100">
First 10 devices always free. Deploy an agent, connect your billing system,
and see the difference in your first outage.
</p>
<.link
navigate={~p"/users/register"}
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 mt-10 bg-white text-slate-900 hover:bg-cerulean-50 active:bg-cerulean-200 active:text-slate-600 focus-visible:outline-white"
class="group inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 mt-10 bg-white text-slate-900 hover:bg-blue-50 active:bg-blue-200 active:text-slate-600 focus-visible:outline-white"
>
Start monitoring free →
</.link>

View file

@ -1,36 +1,36 @@
<% content = fn -> %>
<div class="space-y-8">
<div>
<h1 class="text-3xl font-bold text-cool-steel-900 dark:text-white">Privacy Policy</h1>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Privacy Policy</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Version 1.0 | Effective Date: January 29, 2026
</p>
</div>
<div class="prose prose-sm dark:prose-invert max-w-none">
<section class="space-y-4">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
Our Commitment to Privacy
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
At Towerops, we value your privacy. This policy outlines how we collect, use, and protect your data when you use our network monitoring platform.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
Data Collection and Use
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We collect and store information necessary to provide our network monitoring services, including:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Account information (email address, organization details)</li>
<li>Network device data (IP addresses, SNMP data, device configurations)</li>
<li>Monitoring metrics and alerts</li>
<li>Usage information to improve our services</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
<strong class="font-semibold">
Your data is used exclusively for providing our monitoring services to you.
</strong>
@ -39,11 +39,11 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">Data Protection</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Data Protection</h2>
<p class="text-gray-700 dark:text-gray-300">
We implement industry-standard security measures to protect your data:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Encrypted connections (HTTPS/TLS) for all data transmission</li>
<li>Secure database storage with encryption at rest</li>
<li>Access controls and authentication mechanisms</li>
@ -52,35 +52,33 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">Data Retention</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Data Retention</h2>
<p class="text-gray-700 dark:text-gray-300">
We retain your data for as long as your account is active and as necessary to provide our services. Time-series monitoring data is retained according to your subscription plan. When you delete your account, we will delete your data within 30 days, except where we are required to retain it for legal or compliance purposes.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
Third-Party Services
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Third-Party Services</h2>
<p class="text-gray-700 dark:text-gray-300">
We use limited third-party services to operate our platform:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Cloud infrastructure providers for hosting</li>
<li>Email delivery services for notifications</li>
<li>Payment processors for subscription billing</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
These services are bound by their own privacy policies and are used solely to deliver our services to you.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">Your Rights</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Your Rights</h2>
<p class="text-gray-700 dark:text-gray-300">
You have the right to:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Access your data at any time through your account</li>
<li>Export your data in standard formats</li>
<li>Request deletion of your account and associated data</li>
@ -90,31 +88,29 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">Cookie Policy</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Cookie Policy</h2>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">
What Are Cookies?
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Cookies are small text files that are placed on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and keeping you logged in.
</p>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">
What Cookies Do We Use?
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Towerops uses only <strong class="font-semibold">essential cookies</strong>
that are strictly necessary for the operation of our platform. We do not use third-party tracking, advertising, or analytics cookies.
</p>
<div class="mt-4">
<h4 class="text-base font-semibold text-cool-steel-900 dark:text-white">
Essential Cookies
</h4>
<p class="text-cool-steel-700 dark:text-cool-steel-300 mt-2">
<h4 class="text-base font-semibold text-gray-900 dark:text-white">Essential Cookies</h4>
<p class="text-gray-700 dark:text-gray-300 mt-2">
These cookies are required for our website to function properly and cannot be disabled:
</p>
<ul class="list-disc pl-6 space-y-3 text-cool-steel-700 dark:text-cool-steel-300 mt-3">
<ul class="list-disc pl-6 space-y-3 text-gray-700 dark:text-gray-300 mt-3">
<li>
<strong class="font-semibold">Session Cookie (_towerops_web_key)</strong>
<br />
@ -156,20 +152,18 @@
</ul>
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">
Third-Party Cookies
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We do not use any third-party cookies, tracking pixels, or analytics services that would place cookies on your device.
</p>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
Managing Cookies
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">Managing Cookies</h3>
<p class="text-gray-700 dark:text-gray-300">
Since we only use essential cookies required for the website to function, disabling them will prevent you from using Towerops. However, you can manage cookies through your browser settings:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300 mt-3">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300 mt-3">
<li>
<strong class="font-semibold">Chrome:</strong>
Settings → Privacy and security → Cookies and other site data
@ -188,36 +182,36 @@
</li>
</ul>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">
Cookie Consent (EU/EEA Users)
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
If you are accessing Towerops from the European Union or European Economic Area, you will see a cookie consent banner on your first visit. Since we only use essential cookies necessary for the website to function, this banner is informational. Under GDPR, essential cookies do not require explicit consent as they are strictly necessary for the service you have requested.
</p>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6">
Changes to Our Cookie Policy
</h3>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
If we introduce new types of cookies in the future (such as analytics or marketing cookies), we will update this policy and, where required, request your consent before using them.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
Changes to This Policy
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We may update this privacy policy from time to time. We will notify you of any significant changes by email or through our platform.
</p>
</section>
<section class="space-y-4 mt-8 pt-8 border-t border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">Contact Us</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<section class="space-y-4 mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Contact Us</h2>
<p class="text-gray-700 dark:text-gray-300">
If you have questions about this privacy policy or how we handle your data, please contact us at <a
href="mailto:hi@towerops.net"
class="text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300"
>hi@towerops.net</a>.
</p>
</section>

View file

@ -1,71 +1,71 @@
<% content = fn -> %>
<div class="space-y-8">
<div>
<h1 class="text-3xl font-bold text-cool-steel-900 dark:text-white">Terms of Service</h1>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Terms of Service</h1>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Version 1.0 | Effective Date: January 29, 2026
</p>
</div>
<div class="prose prose-sm dark:prose-invert max-w-none">
<section class="space-y-4">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
1. Acceptance of Terms
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
By accessing and using Towerops ("Service"), you accept and agree to be bound by these Terms of Service ("Terms"). If you do not agree to these Terms, please do not use the Service.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
These Terms constitute a legally binding agreement between you ("User", "you", or "your") and Towerops ("we", "us", or "our").
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
2. Service Description
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Towerops is a network monitoring and alerting platform that enables you to:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Monitor network devices via SNMP protocol</li>
<li>Collect and visualize time-series metrics</li>
<li>Receive alerts for device failures and performance issues</li>
<li>Manage network topology and device relationships</li>
<li>Deploy remote monitoring agents on your network</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We reserve the right to modify, suspend, or discontinue any part of the Service at any time with reasonable notice to users.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
3. User Accounts and Registration
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
To use the Service, you must:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Register for an account with accurate and complete information</li>
<li>Be at least 18 years old or have parental/guardian consent</li>
<li>Maintain the security and confidentiality of your account credentials</li>
<li>Notify us immediately of any unauthorized access to your account</li>
<li>Be responsible for all activities that occur under your account</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
You may not share your account with others, and you may not use another user's account without permission.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
4. Acceptable Use Policy
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
You agree to use the Service only for lawful purposes and in accordance with these Terms. You agree NOT to:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>
Use the Service to monitor devices or networks you do not own or have explicit permission to monitor
</li>
@ -86,25 +86,25 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
5. User Data and Privacy
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Our collection and use of your personal data is governed by our <.link
navigate={~p"/privacy"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline font-medium"
class="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
Privacy Policy
</.link>, which is incorporated into these Terms by reference.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
You retain all rights to the data you collect through the Service. We do not claim ownership of your monitoring data, device configurations, or network information.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
<strong class="font-semibold">GDPR Rights:</strong>
If you are located in the European Union or European Economic Area, you have specific rights under the General Data Protection Regulation (GDPR), including:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Right to access your personal data</li>
<li>Right to rectification of inaccurate data</li>
<li>Right to erasure ("right to be forgotten")</li>
@ -113,10 +113,10 @@
<li>Right to object to processing</li>
<li>Right to withdraw consent</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
To exercise these rights, please contact us at <a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline font-medium"
class="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
hi@towerops.net
</a>.
@ -124,25 +124,25 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
6. Service Availability and Support
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We strive to provide reliable and uninterrupted service, but we do not guarantee that the Service will be:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Available at all times without interruption</li>
<li>Error-free or free from bugs</li>
<li>Secure from unauthorized access or cyberattacks</li>
<li>Compatible with all hardware, software, or network configurations</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We may perform scheduled maintenance that temporarily interrupts service. We will provide advance notice of scheduled downtime when possible.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Support is provided via email at <a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline font-medium"
class="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
hi@towerops.net
</a>. Response times vary based on your subscription plan.
@ -150,37 +150,37 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
7. Subscription and Payment
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
The Service is provided on a subscription basis. By subscribing, you agree to:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Pay all fees associated with your selected subscription plan</li>
<li>Provide accurate and current billing information</li>
<li>Authorize recurring charges to your payment method</li>
<li>Notify us of any changes to your payment information</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Subscriptions automatically renew unless canceled before the renewal date. You may cancel your subscription at any time through your account settings. No refunds are provided for partial subscription periods.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We reserve the right to change our pricing with 30 days' notice. Price changes will not affect your current subscription period.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
8. Intellectual Property
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
The Service, including all software, designs, text, graphics, logos, and other content, is owned by Towerops and protected by copyright, trademark, and other intellectual property laws.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We grant you a limited, non-exclusive, non-transferable license to access and use the Service for your internal business purposes. You may not:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Copy, modify, or create derivative works of the Service</li>
<li>Sell, resell, rent, or lease access to the Service</li>
<li>Remove or alter any copyright, trademark, or proprietary notices</li>
@ -189,15 +189,15 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
9. Limitation of Liability
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
<strong class="font-semibold uppercase">
To the maximum extent permitted by law:
</strong>
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>
The Service is provided "as is" without warranties of any kind, either express or implied
</li>
@ -217,19 +217,19 @@
We are not liable for damages resulting from unauthorized access to your account
</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Some jurisdictions do not allow the exclusion of certain warranties or limitation of liability, so these limitations may not apply to you.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
10. Indemnification
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
You agree to indemnify, defend, and hold harmless Towerops and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, and expenses (including legal fees) arising from:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Your use or misuse of the Service</li>
<li>Your violation of these Terms</li>
<li>Your violation of any rights of another person or entity</li>
@ -238,129 +238,127 @@
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
11. Termination
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
You may terminate your account at any time by:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Canceling your subscription through account settings</li>
<li>
Contacting us at
<a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline font-medium"
class="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
hi@towerops.net
</a>
</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We may suspend or terminate your account immediately if:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>You violate these Terms</li>
<li>Your payment fails or your account is past due</li>
<li>You engage in fraudulent or illegal activities</li>
<li>We are required to do so by law</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Upon termination, your right to use the Service ceases immediately. We will delete your data within 30 days of account termination, except where we are required to retain it for legal purposes.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
12. Data Export and Deletion
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Before terminating your account, you should export any data you wish to retain. We provide data export functionality through your account settings.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
<strong class="font-semibold">GDPR Right to Erasure:</strong>
You may request deletion of your account and all associated data at any time. We will process deletion requests within 30 days, except for data we are legally required to retain.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
13. Changes to Terms
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
We may modify these Terms at any time. If we make material changes, we will notify you by:
</p>
<ul class="list-disc pl-6 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-disc pl-6 space-y-2 text-gray-700 dark:text-gray-300">
<li>Email to your registered email address</li>
<li>Notice on the Service homepage</li>
<li>In-app notification</li>
</ul>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
Your continued use of the Service after changes take effect constitutes acceptance of the modified Terms. If you do not agree to the changes, you must stop using the Service and terminate your account.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
14. Dispute Resolution
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
In the event of any dispute arising from these Terms or your use of the Service, you agree to first contact us to seek an informal resolution at <a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline font-medium"
class="text-blue-600 dark:text-blue-400 hover:underline font-medium"
>
hi@towerops.net
</a>.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
If we cannot resolve the dispute informally within 30 days, either party may pursue formal dispute resolution through binding arbitration or in the courts, subject to the governing law provisions below.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
15. Governing Law and Jurisdiction
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
These Terms are governed by and construed in accordance with the laws of the State of Texas, United States, without regard to its conflict of law provisions.
</p>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
<strong class="font-semibold">For EU/EEA Users:</strong>
Nothing in these Terms affects your statutory rights as a consumer under EU or local law. If you are located in the EU/EEA, you may also have the right to bring legal proceedings in the courts of your country of residence.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
16. Severability
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
If any provision of these Terms is found to be unenforceable or invalid, that provision shall be limited or eliminated to the minimum extent necessary so that these Terms shall otherwise remain in full force and effect.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
17. Entire Agreement
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<p class="text-gray-700 dark:text-gray-300">
These Terms, together with our Privacy Policy, constitute the entire agreement between you and Towerops regarding the Service and supersede all prior agreements and understandings.
</p>
</section>
<section class="space-y-4 mt-8">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">
Contact Information
</h2>
<p class="text-cool-steel-700 dark:text-cool-steel-300">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Contact Information</h2>
<p class="text-gray-700 dark:text-gray-300">
If you have any questions about these Terms, please contact us:
</p>
<ul class="list-none space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="list-none space-y-2 text-gray-700 dark:text-gray-300">
<li>
<strong class="font-semibold">Email:</strong>
<a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
hi@towerops.net
</a>
@ -369,7 +367,7 @@
<strong class="font-semibold">Support:</strong>
<a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
hi@towerops.net
</a>
@ -378,7 +376,7 @@
<strong class="font-semibold">Privacy Inquiries:</strong>
<a
href="mailto:hi@towerops.net"
class="text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
hi@towerops.net
</a>
@ -387,7 +385,7 @@
</section>
<section class="space-y-4 mt-8">
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 italic">
<p class="text-sm text-gray-600 dark:text-gray-400 italic">
By using Towerops, you acknowledge that you have read, understood, and agree to be bound by these Terms of Service.
</p>
</section>

View file

@ -1,21 +1,21 @@
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="mx-auto max-w-4xl">
<div class="overflow-hidden rounded-2xl bg-white shadow-xl dark:bg-cool-steel-900 lg:grid lg:grid-cols-5">
<div class="overflow-hidden rounded-2xl bg-white shadow-xl dark:bg-gray-900 lg:grid lg:grid-cols-5">
<!-- Left: Brand panel -->
<div class="hidden lg:col-span-2 lg:flex lg:flex-col lg:justify-between bg-gradient-to-br from-cerulean-600 via-cerulean-700 to-cerulean-800 p-8 text-white">
<div class="hidden lg:col-span-2 lg:flex lg:flex-col lg:justify-between bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-8 text-white">
<div>
<div class="flex items-center gap-2">
<.icon name="hero-signal" class="size-8" />
<span class="text-xl font-bold">TowerOps</span>
</div>
<p class="mt-6 text-lg font-medium leading-relaxed text-cerulean-100">
<p class="mt-6 text-lg font-medium leading-relaxed text-blue-100">
{t("Start monitoring your network in minutes.")}
</p>
</div>
<div class="mt-auto space-y-4">
<div class="rounded-lg bg-white/10 p-4 backdrop-blur-sm">
<p class="text-sm font-semibold text-white">{t("Free tier includes:")}</p>
<ul class="mt-2 space-y-1.5 text-sm text-cerulean-100">
<ul class="mt-2 space-y-1.5 text-sm text-blue-100">
<li class="flex items-center gap-2">
<.icon name="hero-check" class="size-4 text-green-300" />
{t("Monitor up to 10 devices")}
@ -41,23 +41,23 @@
<div class="col-span-3 px-6 py-10 sm:px-12">
<!-- Mobile brand mark -->
<div class="mb-6 flex items-center justify-center gap-2 lg:hidden">
<.icon name="hero-signal" class="size-7 text-cerulean-600 dark:text-cerulean-400" />
<span class="text-lg font-bold text-cool-steel-900 dark:text-white">TowerOps</span>
<.icon name="hero-signal" class="size-7 text-blue-600 dark:text-blue-400" />
<span class="text-lg font-bold text-gray-900 dark:text-white">TowerOps</span>
</div>
<div class="text-center lg:text-left">
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
<%= if @invitation do %>
{t_auth("Join %{organization}", organization: @invitation.organization.name)}
<% else %>
{t_auth("Create your account")}
<% end %>
</h1>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t_auth("Already registered?")}
<.link
navigate={~p"/users/log-in"}
class="font-semibold text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{t_auth("Log in")}
</.link>
@ -75,8 +75,8 @@
<% end %>
<%= if @invitation do %>
<div class="mt-5 rounded-lg bg-cerulean-50 dark:bg-cerulean-900/20 p-4 border border-cerulean-200 dark:border-cerulean-800">
<p class="text-sm text-cerulean-900 dark:text-cerulean-100">
<div class="mt-5 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4 border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-900 dark:text-blue-100">
{t_auth("You've been invited to join %{organization} as a %{role}.",
organization: @invitation.organization.name,
role: @invitation.role
@ -110,27 +110,27 @@
<div class="mb-4 -mt-2">
<div class="flex gap-1">
<div
class="h-1 flex-1 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 password-strength-bar"
class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700 password-strength-bar"
data-index="1"
>
</div>
<div
class="h-1 flex-1 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 password-strength-bar"
class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700 password-strength-bar"
data-index="2"
>
</div>
<div
class="h-1 flex-1 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 password-strength-bar"
class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700 password-strength-bar"
data-index="3"
>
</div>
<div
class="h-1 flex-1 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 password-strength-bar"
class="h-1 flex-1 rounded-full bg-gray-200 dark:bg-gray-700 password-strength-bar"
data-index="4"
>
</div>
</div>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-500">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-500">
{t_auth("Use 12+ characters for a strong password")}
</p>
</div>
@ -154,21 +154,18 @@
name="user[privacy_policy_consent]"
type="checkbox"
required
class="h-4 w-4 rounded border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:checked:bg-cerulean-500 dark:checked:border-cerulean-500"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
<label
for="privacy_policy_consent"
class="text-cool-steel-700 dark:text-cool-steel-300"
>
<label for="privacy_policy_consent" class="text-gray-700 dark:text-gray-300">
{t_auth("I agree to the %{link}",
link:
raw(
~s(<a href="/privacy" target="_blank" class="font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300">#{t_auth("Privacy Policy")}</a>)
~s(<a href="/privacy" target="_blank" class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">#{t_auth("Privacy Policy")}</a>)
)
)}
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">*</span>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>
@ -180,21 +177,18 @@
name="user[terms_of_service_consent]"
type="checkbox"
required
class="h-4 w-4 rounded border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:checked:bg-cerulean-500 dark:checked:border-cerulean-500"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
<label
for="terms_of_service_consent"
class="text-cool-steel-700 dark:text-cool-steel-300"
>
<label for="terms_of_service_consent" class="text-gray-700 dark:text-gray-300">
{t_auth("I agree to the %{link}",
link:
raw(
~s(<a href="/terms" target="_blank" class="font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300">#{t_auth("Terms of Service")}</a>)
~s(<a href="/terms" target="_blank" class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300">#{t_auth("Terms of Service")}</a>)
)
)}
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">*</span>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>

View file

@ -29,7 +29,7 @@
<div class="mt-6 text-center">
<.link
navigate={~p"/users/log-in"}
class="text-sm font-semibold text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{t_auth("Back to log in")}
</.link>

View file

@ -62,7 +62,7 @@
<p
:if={!@user.confirmed_at}
class="mt-6 rounded-lg border border-cerulean-200 bg-cerulean-50 p-4 text-sm text-cerulean-700 dark:border-cerulean-800 dark:bg-cerulean-950 dark:text-cerulean-200"
class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
>
{t_auth("Tip: If you prefer passwords, you can enable them in the user settings.")}
</p>

View file

@ -1,27 +1,25 @@
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="mx-auto max-w-4xl">
<div class="overflow-hidden rounded-2xl bg-white shadow-xl dark:bg-cool-steel-900 lg:grid lg:grid-cols-5">
<div class="overflow-hidden rounded-2xl bg-white shadow-xl dark:bg-gray-900 lg:grid lg:grid-cols-5">
<!-- Left: Brand panel -->
<div class="hidden lg:col-span-2 lg:flex lg:flex-col lg:justify-between bg-gradient-to-br from-cerulean-600 via-cerulean-700 to-cerulean-800 p-8 text-white">
<div class="hidden lg:col-span-2 lg:flex lg:flex-col lg:justify-between bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-800 p-8 text-white">
<div>
<div class="flex items-center gap-2">
<.icon name="hero-signal" class="size-8" />
<span class="text-xl font-bold">TowerOps</span>
</div>
<p class="mt-6 text-lg font-medium leading-relaxed text-cerulean-100">
<p class="mt-6 text-lg font-medium leading-relaxed text-blue-100">
{t("Monitor your entire network from a single dashboard.")}
</p>
</div>
<div class="mt-auto">
<div class="rounded-lg bg-white/10 p-4 backdrop-blur-sm">
<p class="text-sm italic text-cerulean-100">
<p class="text-sm italic text-blue-100">
{t(
"\"TowerOps cut our response time to network issues by 80%. We can see problems before customers even notice.\""
)}
</p>
<p class="mt-3 text-xs font-semibold text-cerulean-200">
{t("— WISP Network Operator")}
</p>
<p class="mt-3 text-xs font-semibold text-blue-200">{t("— WISP Network Operator")}</p>
</div>
</div>
</div>
@ -30,16 +28,16 @@
<div class="col-span-3 px-6 py-10 sm:px-12">
<!-- Mobile brand mark -->
<div class="mb-6 flex items-center justify-center gap-2 lg:hidden">
<.icon name="hero-signal" class="size-7 text-cerulean-600 dark:text-cerulean-400" />
<span class="text-lg font-bold text-cool-steel-900 dark:text-white">TowerOps</span>
<.icon name="hero-signal" class="size-7 text-blue-600 dark:text-blue-400" />
<span class="text-lg font-bold text-gray-900 dark:text-white">TowerOps</span>
</div>
<div class="text-center lg:text-left">
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{t_auth("Log in")}
</h1>
<%= if @current_scope do %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t_auth("You need to reauthenticate to perform sensitive actions on your account.")}
</p>
<% else %>
@ -55,18 +53,18 @@
<div
:if={local_mail_adapter?()}
class="mt-5 rounded-lg bg-cerulean-50 p-4 dark:bg-cerulean-950"
class="mt-5 rounded-lg bg-blue-50 p-4 dark:bg-blue-950"
>
<div class="flex">
<.icon
name="hero-information-circle"
class="h-6 w-6 flex-shrink-0 text-cerulean-600 dark:text-cerulean-400"
class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400"
/>
<div class="ml-3">
<p class="text-sm text-cerulean-700 dark:text-cerulean-200">
<p class="text-sm text-blue-700 dark:text-blue-200">
{t_auth("You are running the local mail adapter.")}
</p>
<p class="mt-1 text-sm text-cerulean-700 dark:text-cerulean-200">
<p class="mt-1 text-sm text-blue-700 dark:text-blue-200">
{t_auth("To see sent emails, visit")}
<a href="/dev/mailbox" class="font-medium underline">{t_auth("the mailbox page")}</a>.
</p>
@ -102,18 +100,18 @@
required
/>
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<label class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
<input
type="checkbox"
name={@form[:remember_me].name}
value="true"
class="h-4 w-4 rounded border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-800"
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
/>
{t_auth("Remember me")}
</label>
<.link
navigate={~p"/users/reset-password"}
class="text-sm font-semibold text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{t_auth("Forgot password?")}
</.link>
@ -127,10 +125,10 @@
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-cool-steel-200 dark:border-white/10"></div>
<div class="w-full border-t border-gray-200 dark:border-white/10"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white px-3 text-cool-steel-500 dark:bg-cool-steel-900 dark:text-cool-steel-500">
<span class="bg-white px-3 text-gray-500 dark:bg-gray-900 dark:text-gray-500">
{t("or")}
</span>
</div>
@ -153,10 +151,10 @@
</.button>
</.form>
<p class="text-center text-sm text-cool-steel-500 dark:text-cool-steel-500">
<p class="text-center text-sm text-gray-500 dark:text-gray-500">
<.link
href={~p"/users/confirm"}
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 hover:underline"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 hover:underline"
>
{t_auth("Didn't receive confirmation instructions?")}
</.link>

View file

@ -22,7 +22,7 @@
phx-mounted={JS.focus()}
/>
<div class="mt-2">
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth(
"Enter the 6-digit code from your authenticator app, or use a recovery code (XXXX-XXXX format)."
)}
@ -38,7 +38,7 @@
<div class="mt-6 text-center">
<.link
href={~p"/users/log-in"}
class="text-sm font-semibold text-cool-steel-600 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
class="text-sm font-semibold text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
{t_auth("← Back to login")}
</.link>

View file

@ -47,38 +47,38 @@
<div class="my-4 border-t border-cool-steel-200 dark:border-cool-steel-700" />
<div>
<h3 class="text-lg font-semibold leading-6 text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
{t_auth("Alert Notification Devices")}
</h3>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t_auth("Manage mobile devices that receive push notifications for alerts")}
</p>
<div class="mt-6 space-y-4">
<%= if Enum.empty?(@mobile_sessions) do %>
<div class="rounded-lg border border-dashed border-cool-steel-300 p-8 text-center dark:border-white/10">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-cool-steel-400" />
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="rounded-lg border border-dashed border-gray-300 p-8 text-center dark:border-white/10">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t_auth("No mobile devices registered")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t_auth("Add a mobile device to receive push notifications for alerts")}
</p>
</div>
<% else %>
<div class="space-y-3">
<%= for session <- @mobile_sessions do %>
<div class="flex items-center justify-between rounded-lg border border-cool-steel-200 p-4 dark:border-white/10">
<div class="flex items-center justify-between rounded-lg border border-gray-200 p-4 dark:border-white/10">
<div class="flex items-center gap-3">
<.icon name="hero-device-phone-mobile" class="h-5 w-5 text-cool-steel-400" />
<.icon name="hero-device-phone-mobile" class="h-5 w-5 text-gray-400" />
<div>
<p class="font-medium text-cool-steel-900 dark:text-white">
<p class="font-medium text-gray-900 dark:text-white">
{session.device_name || t_auth("Unknown Device")}
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{session.device_os} • {session.app_version}
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth("Last used %{date}",
date: ToweropsWeb.TimeHelpers.format_date(session.last_used_at, "UTC")
)}
@ -108,7 +108,7 @@
do:
"bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/20 dark:text-green-400 dark:hover:bg-green-900/30",
else:
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800/50 dark:text-cool-steel-400 dark:hover:bg-cool-steel-800"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800/50 dark:text-gray-400 dark:hover:bg-gray-800"
)
]}
>
@ -131,7 +131,7 @@
<button
type="submit"
data-confirm={t_auth("Are you sure you want to remove this device?")}
class="text-sm font-semibold text-sweet-salmon-600 hover:text-sweet-salmon-500 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-sm font-semibold text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300"
>
{t_auth("Remove")}
</button>
@ -144,7 +144,7 @@
<.link
navigate={~p"/mobile/qr-login"}
class="inline-flex items-center gap-2 rounded-lg bg-cool-steel-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-cool-steel-700 dark:bg-cool-steel-50 dark:text-cool-steel-900 dark:hover:bg-cool-steel-300"
class="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-700 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-300"
>
<.icon name="hero-qr-code" class="h-4 w-4" /> {t_auth("Add Mobile Device")}
</.link>

View file

@ -22,7 +22,7 @@
phx-mounted={JS.focus()}
/>
<div class="mt-2">
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth(
"Enter the 6-digit code from your authenticator app. Recovery codes are not allowed for sensitive operations."
)}
@ -39,7 +39,7 @@
<.link
href={~p"/users/log-out"}
method="delete"
class="text-sm font-semibold text-cool-steel-600 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
class="text-sm font-semibold text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
{t_auth("← Log out instead")}
</.link>

View file

@ -29,31 +29,31 @@ defmodule ToweropsWeb.AccountLive.Activity do
active_page="settings"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Activity Log
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
View all actions performed by you and on your account data for transparency and security.
</p>
</div>
<div class="mt-8 space-y-4">
<%= if Enum.empty?(@audit_logs) do %>
<div class="text-center py-12 bg-white dark:bg-cool-steel-900 rounded-lg shadow">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-cool-steel-400" />
<h3 class="mt-2 text-sm font-medium text-cool-steel-900 dark:text-white">
<div class="text-center py-12 bg-white dark:bg-gray-900 rounded-lg shadow">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
No activity logs
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Your activity will appear here as you use the platform.
</p>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<ul role="list" class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<div class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
<%= for log <- @audit_logs do %>
<li class="px-6 py-4 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800">
<li class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-800">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
@ -63,12 +63,12 @@ defmodule ToweropsWeb.AccountLive.Activity do
]}>
{format_action(log.action)}
</span>
<p class="text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{action_description(log)}
</p>
</div>
<div class="mt-2 flex flex-col gap-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-2 flex flex-col gap-1 text-sm text-gray-500 dark:text-gray-400">
<%= if log.superuser do %>
<div class="flex items-center gap-1">
<.icon name="hero-user" class="h-4 w-4" />
@ -81,7 +81,7 @@ defmodule ToweropsWeb.AccountLive.Activity do
<%= if log.request_path do %>
<div class="flex items-center gap-1">
<.icon name="hero-link" class="h-4 w-4" />
<code class="text-xs bg-cool-steel-100 dark:bg-cool-steel-800 px-1 rounded">
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded">
{log.request_path}
</code>
</div>
@ -99,7 +99,7 @@ defmodule ToweropsWeb.AccountLive.Activity do
<.icon name="hero-eye" class="h-4 w-4 mt-0.5" />
<div>
<span class="font-medium">Data accessed:</span>
<code class="text-xs bg-cool-steel-100 dark:bg-cool-steel-800 px-1 rounded ml-1">
<code class="text-xs bg-gray-100 dark:bg-gray-800 px-1 rounded ml-1">
{inspect(log.data_accessed, pretty: true, limit: 3)}
</code>
</div>
@ -108,10 +108,10 @@ defmodule ToweropsWeb.AccountLive.Activity do
<%= if log.metadata && map_size(log.metadata) > 0 do %>
<details class="mt-1">
<summary class="cursor-pointer text-xs text-cerulean-600 dark:text-cerulean-400 hover:underline">
<summary class="cursor-pointer text-xs text-blue-600 dark:text-blue-400 hover:underline">
View metadata
</summary>
<pre class="mt-1 text-xs bg-cool-steel-100 dark:bg-cool-steel-800 p-2 rounded overflow-x-auto">
<pre class="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
{Jason.encode!(log.metadata, pretty: true)}
</pre>
</details>
@ -120,7 +120,7 @@ defmodule ToweropsWeb.AccountLive.Activity do
</div>
<div class="ml-4 flex-shrink-0 text-right">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs text-gray-500 dark:text-gray-400">
{format_timestamp(log.inserted_at, @current_scope)}
</p>
</div>
@ -130,13 +130,13 @@ defmodule ToweropsWeb.AccountLive.Activity do
</ul>
</div>
<div class="bg-cerulean-50 dark:bg-cerulean-900/20 border border-cerulean-200 dark:border-cerulean-800 rounded-lg p-4">
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex gap-3">
<.icon
name="hero-information-circle"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
/>
<div class="text-sm text-cerulean-800 dark:text-cerulean-300">
<div class="text-sm text-blue-800 dark:text-blue-300">
<p class="font-medium">GDPR Transparency</p>
<p class="mt-1">
This activity log shows actions performed by you and on your account data.
@ -177,10 +177,10 @@ defmodule ToweropsWeb.AccountLive.Activity do
"bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20"
:warning ->
"bg-wheat-50 text-wheat-700 ring-wheat-600/20 dark:bg-wheat-500/10 dark:text-wheat-400 dark:ring-wheat-500/20"
"bg-yellow-50 text-yellow-700 ring-yellow-600/20 dark:bg-yellow-500/10 dark:text-yellow-400 dark:ring-yellow-500/20"
:danger ->
"bg-sweet-salmon-50 text-sweet-salmon-700 ring-sweet-salmon-600/20 dark:bg-sweet-salmon-500/10 dark:text-sweet-salmon-400 dark:ring-sweet-salmon-500/20"
"bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:ring-red-500/20"
:impersonation ->
"bg-orange-50 text-orange-700 ring-orange-600/20 dark:bg-orange-500/10 dark:text-orange-400 dark:ring-orange-500/20"
@ -189,10 +189,10 @@ defmodule ToweropsWeb.AccountLive.Activity do
"bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-500/10 dark:text-purple-400 dark:ring-purple-500/20"
:info ->
"bg-cerulean-50 text-cerulean-700 ring-cerulean-600/20 dark:bg-cerulean-500/10 dark:text-cerulean-400 dark:ring-cerulean-500/20"
"bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-500/10 dark:text-blue-400 dark:ring-blue-500/20"
:default ->
"bg-cool-steel-50 text-cool-steel-700 ring-cool-steel-600/20 dark:bg-cool-steel-500/10 dark:text-cool-steel-400 dark:ring-cool-steel-500/20"
"bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-gray-500/10 dark:text-gray-400 dark:ring-gray-500/20"
end
end

View file

@ -68,25 +68,25 @@ defmodule ToweropsWeb.AccountLive.MyData do
active_page="settings"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Account Settings
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Manage your account email address, password, and security settings
</p>
</div>
<header class="border-b border-cool-steel-200 dark:border-white/5">
<header class="border-b border-gray-200 dark:border-white/5">
<nav class="flex overflow-x-auto py-4">
<ul
role="list"
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-cool-steel-500 sm:px-6 lg:px-8 dark:text-cool-steel-400"
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
>
<li>
<.link
navigate={~p"/users/settings?tab=personal"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Personal
</.link>
@ -94,7 +94,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/settings?tab=account"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Account
</.link>
@ -102,7 +102,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/settings?tab=sessions"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Sessions
</.link>
@ -110,7 +110,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/settings?tab=api"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
API
</.link>
@ -118,7 +118,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/settings?tab=notifications"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Notifications
</.link>
@ -126,7 +126,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/settings?tab=security"}
class="text-cool-steel-500 hover:text-cerulean-600 dark:text-cool-steel-400 dark:hover:text-cerulean-400"
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Security
</.link>
@ -134,7 +134,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
<li>
<.link
navigate={~p"/users/my-data"}
class="text-cerulean-600 dark:text-cerulean-400"
class="text-indigo-600 dark:text-indigo-400"
>
My Data
</.link>
@ -145,57 +145,43 @@ defmodule ToweropsWeb.AccountLive.MyData do
<div class="space-y-8 py-8">
<div>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
This page shows all personal data we have stored about you in compliance with GDPR Article 15 (Right to Access).
</p>
</div>
<!-- Profile Information -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
Profile Information
</h2>
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">Profile Information</h2>
</div>
<div class="px-4 py-5 sm:p-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Email Address
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
{@user_data.profile.email}
</dd>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Email Address</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{@user_data.profile.email}</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
First Name
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">First Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@user_data.profile.first_name || "Not set"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Last Name
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Last Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@user_data.profile.last_name || "Not set"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Timezone
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Timezone</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@user_data.profile.timezone}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Account Created
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Account Created</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_datetime(
@user_data.profile.inserted_at,
@current_scope.timezone,
@ -204,13 +190,11 @@ defmodule ToweropsWeb.AccountLive.MyData do
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Email Confirmed
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Email Confirmed</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{if @user_data.profile.confirmed_at, do: "Yes", else: "No"}
<%= if @user_data.profile.confirmed_at do %>
<span class="text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-500 dark:text-gray-400">
({ToweropsWeb.TimeHelpers.format_date(
@user_data.profile.confirmed_at,
@current_scope.timezone
@ -224,46 +208,40 @@ defmodule ToweropsWeb.AccountLive.MyData do
</section>
<!-- Consent Records -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
Consent Records
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Your consent to our Privacy Policy and Terms of Service
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.consents) do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
No consent records found.
</p>
<% else %>
<div class="space-y-4">
<%= for consent <- @user_data.consents do %>
<div class="border border-cool-steel-200 dark:border-cool-steel-700 rounded-lg p-4">
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Type
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Type</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{String.replace(consent.consent_type, "_", " ") |> String.capitalize()}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Version
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
{consent.version}
</dd>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Version</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{consent.version}</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Granted
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_datetime(
consent.granted_at,
@current_scope.timezone,
@ -272,12 +250,12 @@ defmodule ToweropsWeb.AccountLive.MyData do
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Status
</dt>
<dd class="mt-1 text-sm">
<%= if consent.revoked_at do %>
<span class="inline-flex items-center rounded-md bg-sweet-salmon-50 px-2 py-1 text-xs font-medium text-sweet-salmon-700 inset-ring-1 inset-ring-sweet-salmon-600/20 dark:bg-sweet-salmon-500/10 dark:text-sweet-salmon-400 dark:inset-ring-sweet-salmon-500/20">
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 inset-ring-1 inset-ring-red-600/20 dark:bg-red-500/10 dark:text-red-400 dark:inset-ring-red-500/20">
Revoked {ToweropsWeb.TimeHelpers.format_date(
consent.revoked_at,
@current_scope.timezone
@ -298,7 +276,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
phx-click="revoke_consent"
phx-value-consent-id={consent.id}
data-confirm="Are you sure you want to revoke this consent? This may limit your ability to use certain features."
class="text-sm font-medium text-sweet-salmon-600 hover:text-sweet-salmon-700 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Revoke Consent
</button>
@ -313,50 +291,42 @@ defmodule ToweropsWeb.AccountLive.MyData do
<%= if @is_owner do %>
<!-- Organizations -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
Organizations
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Organizations you own or are a member of
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.organizations) do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
No organization memberships.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No organization memberships.</p>
<% else %>
<div class="space-y-4">
<%= for org <- @user_data.organizations do %>
<div class="border border-cool-steel-200 dark:border-cool-steel-700 rounded-lg p-4">
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-3 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Name
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">{org.name}</dd>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{org.name}</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Role
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Role</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{if org.is_owner, do: "Owner", else: "Member"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Slug
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">{org.slug}</dd>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Slug</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{org.slug}</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Created
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_date(
org.inserted_at,
@current_scope.timezone
@ -372,52 +342,50 @@ defmodule ToweropsWeb.AccountLive.MyData do
</section>
<!-- Devices -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
Devices
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Network devices monitored by your organizations
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.devices) do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
No devices configured.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No devices configured.</p>
<% else %>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Name
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
IP Address
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Organization
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Created
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<%= for device <- @user_data.devices do %>
<tr>
<td class="px-3 py-4 text-sm text-cool-steel-900 dark:text-white">
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
{device.name}
</td>
<td class="px-3 py-4 text-sm text-cool-steel-900 dark:text-white">
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
{device.ip_address}
</td>
<td class="px-3 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{device.site.organization.name}
</td>
<td class="px-3 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_date(
device.inserted_at,
@current_scope.timezone
@ -428,7 +396,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
</tbody>
</table>
</div>
<p class="mt-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
Total devices: {length(@user_data.devices)}
</p>
<% end %>
@ -436,46 +404,44 @@ defmodule ToweropsWeb.AccountLive.MyData do
</section>
<!-- Alerts -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
Recent Alerts
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Alerts generated for your devices (last 90 days)
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.alerts) do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
No alerts in the last 90 days.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No alerts in the last 90 days.</p>
<% else %>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Device
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Status
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Created
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<%= for alert <- Enum.take(@user_data.alerts, 50) do %>
<tr>
<td class="px-3 py-4 text-sm text-cool-steel-900 dark:text-white">
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
{alert.device.name}
</td>
<td class="px-3 py-4 text-sm text-cool-steel-900 dark:text-white">
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
{alert.alert_type}
</td>
<td class="px-3 py-4 text-sm">
@ -486,7 +452,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
{alert_status_text(alert)}
</span>
</td>
<td class="px-3 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_datetime(
alert.inserted_at,
@current_scope.timezone,
@ -498,7 +464,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
</tbody>
</table>
</div>
<p class="mt-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
Showing {min(50, length(@user_data.alerts))} of {length(@user_data.alerts)} alerts
</p>
<% end %>
@ -507,33 +473,31 @@ defmodule ToweropsWeb.AccountLive.MyData do
<% end %>
<!-- Audit Logs -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
Audit Log
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Administrative actions related to your account
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.audit_logs) do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
No audit log entries.
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">No audit log entries.</p>
<% else %>
<div class="space-y-3">
<%= for log <- Enum.take(@user_data.audit_logs, 20) do %>
<div class="border-l-4 border-cerulean-400 bg-cool-steel-50 dark:bg-cool-steel-800 p-3">
<div class="border-l-4 border-blue-400 bg-gray-50 dark:bg-gray-800 p-3">
<div class="flex items-start">
<div class="flex-1">
<p class="text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{log.action}
</p>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
By: {if log.superuser, do: log.superuser.email, else: "System"}
</p>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-500">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-500">
{ToweropsWeb.TimeHelpers.format_datetime(
log.inserted_at,
@current_scope.timezone,
@ -545,7 +509,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
</div>
<% end %>
</div>
<p class="mt-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
Showing {min(20, length(@user_data.audit_logs))} of {length(@user_data.audit_logs)} entries
</p>
<% end %>
@ -553,22 +517,22 @@ defmodule ToweropsWeb.AccountLive.MyData do
</section>
<!-- Export Data -->
<section class="bg-cerulean-50 dark:bg-cerulean-900/20 border border-cerulean-200 dark:border-cerulean-800 rounded-lg p-6">
<section class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6">
<div class="flex items-start gap-3">
<.icon
name="hero-arrow-down-tray"
class="h-6 w-6 text-cerulean-600 dark:text-cerulean-400 flex-shrink-0"
class="h-6 w-6 text-blue-600 dark:text-blue-400 flex-shrink-0"
/>
<div class="flex-1">
<h3 class="text-sm font-semibold text-cerulean-900 dark:text-cerulean-300 mb-1">
<h3 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
Export Your Data
</h3>
<p class="text-sm text-cerulean-800 dark:text-cerulean-300 mb-4">
<p class="text-sm text-blue-800 dark:text-blue-300 mb-4">
You can download all your data in JSON format. This includes your profile, organizations, devices, alerts, and audit logs.
</p>
<.link
href={~p"/api/v1/account/data"}
class="inline-flex items-center gap-2 px-4 py-2 bg-cerulean-600 text-white text-sm font-medium rounded-lg hover:bg-cerulean-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Download My Data (JSON)
</.link>
@ -577,15 +541,15 @@ defmodule ToweropsWeb.AccountLive.MyData do
</section>
<!-- Data Processing Information -->
<section class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-cool-steel-200 dark:border-cool-steel-700">
<h2 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<section class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200 dark:border-gray-700">
<h2 class="text-lg font-medium text-gray-900 dark:text-white">
How We Use Your Data
</h2>
</div>
<div class="px-4 py-5 sm:p-6">
<div class="prose prose-sm dark:prose-invert max-w-none">
<ul class="space-y-2 text-sm text-cool-steel-700 dark:text-cool-steel-300">
<ul class="space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li>
<strong>Profile Information:</strong>
Used to identify your account and personalize your experience
@ -610,10 +574,10 @@ defmodule ToweropsWeb.AccountLive.MyData do
Security records of administrative actions for compliance and security purposes
</li>
</ul>
<p class="mt-4 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">
For more information, please read our <.link
navigate={~p"/privacy"}
class="text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400"
class="text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Privacy Policy
</.link>.
@ -699,8 +663,8 @@ defmodule ToweropsWeb.AccountLive.MyData do
defp alert_status_class(alert) do
cond do
alert.resolved_at -> "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
alert.acknowledged_at -> "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
true -> "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
alert.acknowledged_at -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
true -> "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
end
end

View file

@ -141,7 +141,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="mx-auto max-w-2xl px-4 py-12">
<div class="bg-white dark:bg-cool-steel-900 rounded-lg shadow-lg p-8">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-8">
<%= if @show_recovery_codes do %>
<!-- Recovery Codes Display -->
<div class="text-center mb-8">
@ -149,27 +149,27 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
name="hero-shield-check"
class="w-16 h-16 mx-auto mb-4 text-green-600 dark:text-green-400"
/>
<h1 class="text-3xl font-bold text-cool-steel-900 dark:text-white mb-2">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Save Your Recovery Codes
</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-gray-600 dark:text-gray-400">
Two-factor authentication has been enabled successfully!
</p>
</div>
<div class="space-y-6">
<!-- Warning -->
<div class="rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-4 border border-wheat-200 dark:border-wheat-800">
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4 border border-yellow-200 dark:border-yellow-800">
<div class="flex">
<.icon
name="hero-exclamation-triangle"
class="h-5 w-5 text-wheat-400 mt-0.5"
class="h-5 w-5 text-yellow-400 mt-0.5"
/>
<div class="ml-3">
<h3 class="text-sm font-medium text-wheat-800 dark:text-wheat-200">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Important: Save these recovery codes now
</h3>
<div class="mt-2 text-sm text-wheat-700 dark:text-wheat-300">
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<ul class="list-disc pl-5 space-y-1">
<li>These codes will only be shown once and cannot be retrieved later</li>
<li>Store them in a secure location like a password manager</li>
@ -181,14 +181,14 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
</div>
</div>
<!-- Recovery Codes -->
<div class="border border-cool-steel-200 dark:border-cool-steel-700 rounded-lg p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4 text-center">
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 text-center">
Your Recovery Codes
</h2>
<div class="grid grid-cols-2 gap-3 max-w-md mx-auto">
<%= for code <- @recovery_codes do %>
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 rounded-md p-3 text-center">
<code class="text-lg font-mono text-cool-steel-900 dark:text-white font-semibold">
<div class="bg-gray-50 dark:bg-gray-800 rounded-md p-3 text-center">
<code class="text-lg font-mono text-gray-900 dark:text-white font-semibold">
{code}
</code>
</div>
@ -199,7 +199,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
<button
type="button"
phx-click={JS.dispatch("phx:copy", to: "#recovery-codes-text")}
class="inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 shadow-sm text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-clipboard-document" class="w-5 h-5 mr-2" /> Copy All
</button>
@ -207,7 +207,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
<a
href={"data:text/plain;charset=utf-8," <> URI.encode(Enum.join(@recovery_codes, "\n"))}
download="towerops-recovery-codes.txt"
class="inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 shadow-sm text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 shadow-sm text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-arrow-down-tray" class="w-5 h-5 mr-2" /> Download
</a>
@ -226,7 +226,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
<button
type="button"
phx-click="confirm_recovery_codes_saved"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-cerulean-600 hover:bg-cerulean-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
I've Saved My Recovery Codes <.icon name="hero-arrow-right" class="w-5 h-5 ml-2" />
</button>
@ -237,29 +237,29 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
<div class="text-center mb-8">
<.icon
name="hero-shield-check"
class="w-16 h-16 mx-auto mb-4 text-cerulean-600 dark:text-cerulean-400"
class="w-16 h-16 mx-auto mb-4 text-blue-600 dark:text-blue-400"
/>
<h1 class="text-3xl font-bold text-cool-steel-900 dark:text-white mb-2">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Set Up Two-Factor Authentication
</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-gray-600 dark:text-gray-400">
To keep your account secure, two-factor authentication is required for all users.
</p>
</div>
<div class="space-y-6">
<!-- Step 1: Scan QR Code -->
<div class="border-t border-cool-steel-200 dark:border-cool-steel-700 pt-6">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-cerulean-600 text-white rounded-full mr-3 text-sm">
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-blue-600 text-white rounded-full mr-3 text-sm">
1
</span>
Install an Authenticator App
</h2>
<p class="text-cool-steel-600 dark:text-cool-steel-400 ml-11 mb-4">
<p class="text-gray-600 dark:text-gray-400 ml-11 mb-4">
Download an authenticator app on your phone if you haven't already:
</p>
<ul class="ml-11 space-y-2 text-cool-steel-700 dark:text-cool-steel-300">
<ul class="ml-11 space-y-2 text-gray-700 dark:text-gray-300">
<li class="flex items-center">
<.icon
name="hero-check-circle"
@ -282,35 +282,35 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
</div>
<!-- Step 2: Scan QR Code -->
<div class="border-t border-cool-steel-200 dark:border-cool-steel-700 pt-6">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-cerulean-600 text-white rounded-full mr-3 text-sm">
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-blue-600 text-white rounded-full mr-3 text-sm">
2
</span>
Scan the QR Code
</h2>
<p class="text-cool-steel-600 dark:text-cool-steel-400 ml-11 mb-4">
<p class="text-gray-600 dark:text-gray-400 ml-11 mb-4">
Open your authenticator app and scan this QR code:
</p>
<%= if @qr_code do %>
<div class="ml-11 flex justify-center">
<div class="bg-white dark:bg-cool-steel-900 p-4 rounded-lg border-2 border-cool-steel-200 dark:border-cool-steel-700">
<div class="bg-white dark:bg-gray-900 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700">
<img src={@qr_code} alt="TOTP QR Code" class="w-64 h-64" />
</div>
</div>
<% else %>
<div class="ml-11 flex justify-center">
<div class="bg-cool-steel-100 dark:bg-cool-steel-800 p-4 rounded-lg border-2 border-cool-steel-200 dark:border-cool-steel-700 w-72 h-72 flex items-center justify-center">
<p class="text-cool-steel-500 dark:text-cool-steel-400">Loading QR code...</p>
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 w-72 h-72 flex items-center justify-center">
<p class="text-gray-500 dark:text-gray-400">Loading QR code...</p>
</div>
</div>
<% end %>
</div>
<!-- Step 3: Enter Code -->
<div class="border-t border-cool-steel-200 dark:border-cool-steel-700 pt-6">
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-cerulean-600 text-white rounded-full mr-3 text-sm">
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<span class="flex items-center justify-center w-8 h-8 bg-blue-600 text-white rounded-full mr-3 text-sm">
3
</span>
Enter the Code from Your App
@ -327,7 +327,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
<div>
<label
for="code"
class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2 text-center"
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 text-center"
>
6-digit code
</label>
@ -341,17 +341,17 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
placeholder="000000"
required
autocomplete="off"
class="block w-full rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 text-2xl text-center tracking-widest font-mono"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white shadow-sm focus:border-blue-500 focus:ring-blue-500 text-2xl text-center tracking-widest font-mono"
phx-mounted={JS.focus()}
/>
</div>
<%= if @error do %>
<div class="rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/30 p-4">
<div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<div class="flex">
<.icon name="hero-exclamation-circle" class="h-5 w-5 text-sweet-salmon-400" />
<.icon name="hero-exclamation-circle" class="h-5 w-5 text-red-400" />
<div class="ml-3">
<p class="text-sm text-sweet-salmon-800 dark:text-sweet-salmon-200">
<p class="text-sm text-red-800 dark:text-red-200">
{@error}
</p>
</div>
@ -369,14 +369,14 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
</div>
</div>
<div class="mt-8 p-4 bg-cerulean-50 dark:bg-cerulean-900/20 rounded-lg border border-cerulean-200 dark:border-cerulean-800">
<div class="mt-8 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div class="flex">
<.icon
name="hero-information-circle"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400 mt-0.5"
class="h-5 w-5 text-blue-600 dark:text-blue-400 mt-0.5"
/>
<div class="ml-3">
<p class="text-sm text-cerulean-900 dark:text-cerulean-100">
<p class="text-sm text-blue-900 dark:text-blue-100">
<strong>Keep your device safe:</strong>
You'll need to enter a code from your authenticator app each time you log in.
Make sure to keep your phone secure and backed up.

View file

@ -179,50 +179,44 @@ defmodule ToweropsWeb.ActivityFeedLive do
end
defp filter_active_class(:config_change), do: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
defp filter_active_class(:alert_fired),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
defp filter_active_class(:alert_fired), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
defp filter_active_class(:alert_resolved), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
defp filter_active_class(:device_event), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
defp filter_active_class(:sync), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
defp filter_active_class(:sync), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
defp filter_active_class(:device_added), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
defp filter_active_class(_), do: "bg-cool-steel-200 text-cool-steel-700 dark:bg-cool-steel-700 dark:text-cool-steel-300"
defp filter_active_class(_), do: "bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300"
@doc false
def severity_dot_color(:critical, _), do: "bg-sweet-salmon-500"
def severity_dot_color(:warning, _), do: "bg-wheat-500"
def severity_dot_color(:critical, _), do: "bg-red-500"
def severity_dot_color(:warning, _), do: "bg-yellow-500"
def severity_dot_color(:info, :alert_resolved), do: "bg-green-500"
def severity_dot_color(:info, :sync), do: "bg-cerulean-500"
def severity_dot_color(:info, :sync), do: "bg-blue-500"
def severity_dot_color(:info, :device_added), do: "bg-cyan-500"
def severity_dot_color(:info, _), do: "bg-cool-steel-400"
def severity_dot_color(_, _), do: "bg-cool-steel-400"
def severity_dot_color(:info, _), do: "bg-gray-400"
def severity_dot_color(_, _), do: "bg-gray-400"
@doc false
def severity_icon_color(:critical, _), do: "text-sweet-salmon-500 dark:text-sweet-salmon-400"
def severity_icon_color(:warning, _), do: "text-wheat-500 dark:text-wheat-400"
def severity_icon_color(:critical, _), do: "text-red-500 dark:text-red-400"
def severity_icon_color(:warning, _), do: "text-yellow-500 dark:text-yellow-400"
def severity_icon_color(:info, :alert_resolved), do: "text-green-500 dark:text-green-400"
def severity_icon_color(:info, :sync), do: "text-cerulean-500 dark:text-cerulean-400"
def severity_icon_color(:info, :sync), do: "text-blue-500 dark:text-blue-400"
def severity_icon_color(:info, :device_added), do: "text-cyan-500 dark:text-cyan-400"
def severity_icon_color(_, _), do: "text-cool-steel-400 dark:text-cool-steel-500"
def severity_icon_color(_, _), do: "text-gray-400 dark:text-gray-500"
@doc false
def severity_text_color(:critical, _), do: "text-sweet-salmon-700 dark:text-sweet-salmon-400"
def severity_text_color(:warning, _), do: "text-wheat-700 dark:text-wheat-400"
def severity_text_color(:critical, _), do: "text-red-700 dark:text-red-400"
def severity_text_color(:warning, _), do: "text-yellow-700 dark:text-yellow-400"
def severity_text_color(:info, :alert_resolved), do: "text-green-700 dark:text-green-400"
def severity_text_color(_, _), do: "text-cool-steel-900 dark:text-white"
def severity_text_color(_, _), do: "text-gray-900 dark:text-white"
@doc false
def type_badge_class(:config_change), do: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
def type_badge_class(:alert_fired),
do: "bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def type_badge_class(:alert_fired), do: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
def type_badge_class(:alert_resolved), do: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
def type_badge_class(:device_event), do: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"
def type_badge_class(:sync), do: "bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def type_badge_class(:sync), do: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
def type_badge_class(:device_added), do: "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400"
def type_badge_class(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
def type_badge_class(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
@doc false
def type_label(:config_change), do: "Config"

View file

@ -24,7 +24,7 @@
<div class="relative">
<.icon
name="hero-magnifying-glass"
class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-cool-steel-400 dark:text-cool-steel-500"
class="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500"
/>
<input
type="text"
@ -38,7 +38,7 @@
:if={@search != ""}
type="button"
phx-click="clear_search"
class="absolute right-3 top-1/2 -translate-y-1/2 text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="h-5 w-5" />
</button>
@ -48,9 +48,7 @@
<%!-- Filter Chips with Counts --%>
<div class="mt-4 flex flex-wrap items-center gap-2">
<span class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 mr-1">
Filter:
</span>
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 mr-1">Filter:</span>
<%= for {type, label, icon} <- filter_options() do %>
<button
type="button"
@ -61,7 +59,7 @@
if(type in @active_types,
do: filter_active_class(type),
else:
"bg-cool-steel-100 text-cool-steel-500 dark:bg-cool-steel-800 dark:text-cool-steel-500 hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-700"
)
]}
>
@ -73,7 +71,7 @@
"inline-flex items-center justify-center min-w-[18px] h-[18px] rounded-full px-1 text-[10px] font-bold",
if(type in @active_types,
do: "bg-white/30 dark:bg-black/20",
else: "bg-cool-steel-200 dark:bg-cool-steel-700"
else: "bg-gray-200 dark:bg-gray-700"
)
]}
>
@ -86,21 +84,18 @@
<%!-- Timeline --%>
<div class="mt-8">
<%= if @items == [] do %>
<div class="rounded-xl border-2 border-dashed border-cool-steel-200 bg-white p-16 text-center dark:border-cool-steel-700 dark:bg-cool-steel-800/50">
<div class="mx-auto w-16 h-16 rounded-full bg-cool-steel-100 dark:bg-cool-steel-700 flex items-center justify-center mb-4">
<.icon
name="hero-signal-slash"
class="h-8 w-8 text-cool-steel-400 dark:text-cool-steel-500"
/>
<div class="rounded-xl border-2 border-dashed border-gray-200 bg-white p-16 text-center dark:border-gray-700 dark:bg-gray-800/50">
<div class="mx-auto w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center mb-4">
<.icon name="hero-signal-slash" class="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
<%= if @search != "" do %>
No results for "{@search}"
<% else %>
{t("No activity yet")}
<% end %>
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400 max-w-md mx-auto">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
<%= if @search != "" do %>
{t("Try adjusting your search terms or clearing the filters.")}
<% else %>
@ -122,13 +117,13 @@
<% else %>
<%!-- Item count summary --%>
<div class="mb-4 flex items-center justify-between">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
Showing {length(@items)} events
<span :if={@search != ""}>
matching "<strong class="text-cool-steel-700 dark:text-cool-steel-300">{@search}</strong>"
matching "<strong class="text-gray-700 dark:text-gray-300">{@search}</strong>"
</span>
</span>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500 flex items-center gap-1">
<span class="text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75">
</span>
@ -138,35 +133,35 @@
</span>
</div>
<div class="rounded-lg border border-cool-steel-200 dark:border-cool-steel-700 overflow-hidden">
<div class="rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
<table class="w-full text-sm border-collapse">
<thead>
<tr class="border-b border-cool-steel-200 dark:border-cool-steel-700 bg-cool-steel-50 dark:bg-cool-steel-800/80">
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/80">
<th class="w-1 p-0"></th>
<th class="py-2 pl-3 pr-4 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 whitespace-nowrap w-28">
<th class="py-2 pl-3 pr-4 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 whitespace-nowrap w-28">
Time
</th>
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 w-28">
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 w-28">
Type
</th>
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Event
</th>
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 w-48">
<th class="py-2 px-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 w-48">
Device / Site
</th>
<th class="w-8 p-0"></th>
</tr>
</thead>
<tbody class="bg-white dark:bg-cool-steel-900 divide-y divide-cool-steel-100 dark:divide-cool-steel-800">
<tbody class="bg-white dark:bg-gray-900 divide-y divide-gray-100 dark:divide-gray-800">
<%= for {period, period_items} <- @grouped_items do %>
<%!-- Period group header row --%>
<tr class="bg-cool-steel-50/80 dark:bg-cool-steel-800/40">
<tr class="bg-gray-50/80 dark:bg-gray-800/40">
<td colspan="6" class="px-4 py-1.5">
<span class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{period}
</span>
<span class="ml-1.5 text-xs text-cool-steel-400 dark:text-cool-steel-500">
<span class="ml-1.5 text-xs text-gray-400 dark:text-gray-500">
({length(period_items)})
</span>
</td>
@ -176,9 +171,8 @@
<tr
id={item.id}
class={[
"group transition-colors hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/50",
item.severity == :critical &&
"bg-sweet-salmon-50/50 dark:bg-sweet-salmon-900/10"
"group transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/50",
item.severity == :critical && "bg-red-50/50 dark:bg-red-900/10"
]}
>
<%!-- Severity bar --%>
@ -203,7 +197,7 @@
severity_icon_color(item.severity, item.type)
]}
/>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400 tabular-nums">
<span class="text-xs text-gray-500 dark:text-gray-400 tabular-nums">
{relative_time(item.timestamp, @now)}
</span>
</div>
@ -233,7 +227,7 @@
</p>
<p
:if={item.detail && item.detail != ""}
class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400 leading-snug"
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 leading-snug"
>
{item.detail}
</p>
@ -244,21 +238,21 @@
<.link
:if={item.device_name && item.link}
navigate={item.link}
class="inline-flex items-center gap-1 text-xs font-medium text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300 leading-snug"
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 leading-snug"
>
<.icon name="hero-server-stack" class="h-3 w-3 flex-shrink-0" />
{item.device_name}
</.link>
<span
:if={item.device_name && !item.link}
class="inline-flex items-center gap-1 text-xs text-cool-steel-600 dark:text-cool-steel-300 leading-snug"
class="inline-flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300 leading-snug"
>
<.icon name="hero-server-stack" class="h-3 w-3 flex-shrink-0" />
{item.device_name}
</span>
<div
:if={item.site_name}
class="mt-0.5 inline-flex items-center gap-1 text-xs text-cool-steel-400 dark:text-cool-steel-500"
class="mt-0.5 inline-flex items-center gap-1 text-xs text-gray-400 dark:text-gray-500"
>
<.icon name="hero-map-pin" class="h-3 w-3 flex-shrink-0" />
{item.site_name}
@ -273,7 +267,7 @@
>
<.link
navigate={item.link}
class="text-cool-steel-400 hover:text-cerulean-500 dark:text-cool-steel-600 dark:hover:text-cerulean-400"
class="text-gray-400 hover:text-blue-500 dark:text-gray-600 dark:hover:text-blue-400"
>
<.icon name="hero-arrow-right" class="h-4 w-4" />
</.link>

View file

@ -1,8 +1,8 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{t("All Agents")}</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Agents")}</h1>
<p class="text-gray-600 dark:text-gray-400">
{t("Agents across all organizations and cloud pollers")}
</p>
</div>
@ -10,7 +10,7 @@
<%!-- Cloud Pollers --%>
<%= if @has_cloud_pollers do %>
<div>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-3">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
<.icon name="hero-cloud" class="w-5 h-5 inline mr-1" /> Cloud Pollers
</h2>
<.table
@ -22,7 +22,7 @@
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-wheat-50 dark:bg-wheat-900/30 px-2 py-0.5 text-xs font-medium text-wheat-700 dark:text-wheat-300 ring-1 ring-inset ring-wheat-600/20 dark:ring-wheat-400/30">
<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">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
@ -39,10 +39,10 @@
<:col :let={{_id, agent}} label={t("Devices")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-cool-steel-900 dark:text-white">
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<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
@ -51,11 +51,11 @@
</:col>
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-cool-steel-900 dark:text-white">
<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-cool-steel-500 dark:text-cool-steel-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}
@ -67,7 +67,7 @@
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 font-mono">
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
@ -79,7 +79,7 @@
<%!-- Organization Agents --%>
<%= if @has_org_agents do %>
<div>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-3">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">
<.icon name="hero-server" class="w-5 h-5 inline mr-1" /> Organization Agents
</h2>
<.table
@ -92,12 +92,12 @@
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-wheat-50 dark:bg-wheat-900/30 px-2 py-0.5 text-xs font-medium text-wheat-700 dark:text-wheat-300 ring-1 ring-inset ring-wheat-600/20 dark:ring-wheat-400/30">
<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">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{agent.organization.name}
</div>
</div>
@ -113,10 +113,10 @@
<:col :let={{_id, agent}} label={t("Devices")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-cool-steel-900 dark:text-white">
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<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
@ -125,11 +125,11 @@
</:col>
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-cool-steel-900 dark:text-white">
<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-cool-steel-500 dark:text-cool-steel-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}
@ -141,7 +141,7 @@
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 font-mono">
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>

View file

@ -81,24 +81,21 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
def render(assigns) do
~H"""
<Layouts.admin flash={@flash} timezone={@current_scope.user.timezone}>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Audit Logs
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
View and search all system audit logs. Records are retained for 3 years.
</p>
</div>
<!-- Search Filters -->
<div class="mt-8 bg-white dark:bg-cool-steel-900 shadow rounded-lg p-6">
<div class="mt-8 bg-white dark:bg-gray-900 shadow rounded-lg p-6">
<form id="audit-search-form" phx-change="search" phx-submit="search" class="space-y-4">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label
for="email"
class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300"
>
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
User Email
</label>
<input
@ -107,21 +104,18 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
id="email"
value={@search_email}
placeholder="user@example.com"
class="mt-1 block w-full rounded-md border-cool-steel-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 sm:text-sm dark:bg-cool-steel-800 dark:border-cool-steel-600 dark:text-white"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-800 dark:border-gray-600 dark:text-white"
/>
</div>
<div>
<label
for="action"
class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300"
>
<label for="action" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Action
</label>
<select
name="action"
id="action"
class="mt-1 block w-full rounded-md border-cool-steel-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 sm:text-sm dark:bg-cool-steel-800 dark:border-cool-steel-600 dark:text-white"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-800 dark:border-gray-600 dark:text-white"
>
<option value="">All Actions</option>
<%= for action <- available_actions() do %>
@ -135,7 +129,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
<div>
<label
for="date_from"
class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300"
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
From Date
</label>
@ -144,15 +138,12 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
name="date_from"
id="date_from"
value={@date_from}
class="mt-1 block w-full rounded-md border-cool-steel-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 sm:text-sm dark:bg-cool-steel-800 dark:border-cool-steel-600 dark:text-white"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-800 dark:border-gray-600 dark:text-white"
/>
</div>
<div>
<label
for="date_to"
class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300"
>
<label for="date_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
To Date
</label>
<input
@ -160,7 +151,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
name="date_to"
id="date_to"
value={@date_to}
class="mt-1 block w-full rounded-md border-cool-steel-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 sm:text-sm dark:bg-cool-steel-800 dark:border-cool-steel-600 dark:text-white"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm dark:bg-gray-800 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
@ -168,7 +159,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
<div class="flex gap-3">
<button
type="submit"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-cerulean-600 hover:bg-cerulean-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<.icon name="hero-magnifying-glass" class="h-4 w-4 mr-2" /> Search
</button>
@ -176,14 +167,14 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
id="audit-clear-filters"
type="button"
phx-click="clear_filters"
class="inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<.icon name="hero-x-mark" class="h-4 w-4 mr-2" /> Clear
</button>
<button
type="button"
phx-click="export"
class="inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500"
class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4 mr-2" /> Export CSV
</button>
@ -194,45 +185,45 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
<!-- Results -->
<div class="mt-8">
<%= if Enum.empty?(@audit_logs) do %>
<div class="text-center py-12 bg-white dark:bg-cool-steel-900 rounded-lg shadow">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-cool-steel-400" />
<h3 class="mt-2 text-sm font-medium text-cool-steel-900 dark:text-white">
<div class="text-center py-12 bg-white dark:bg-gray-900 rounded-lg shadow">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
No audit logs found
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Try adjusting your search filters.
</p>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-900 shadow rounded-lg overflow-hidden">
<div class="bg-white dark:bg-gray-900 shadow rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Timestamp
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Action
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Actor
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Target User
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
IP Address
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Details
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<%= for log <- @audit_logs do %>
<tr class="hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-cool-steel-900 dark:text-white">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_datetime(
log.inserted_at,
@current_scope.timezone,
@ -247,37 +238,37 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
{format_action(log.action)}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= if log.superuser do %>
{log.superuser.email}
<% else %>
<span class="text-cool-steel-400">System</span>
<span class="text-gray-400">System</span>
<% end %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= if log.target_user do %>
{log.target_user.email}
<% else %>
<span class="text-cool-steel-400">-</span>
<span class="text-gray-400">-</span>
<% end %>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{log.ip_address || "-"}
</td>
<td class="px-6 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
<%= if log.request_path do %>
<div class="text-xs">
<code class="bg-cool-steel-100 dark:bg-cool-steel-800 px-1 rounded">
<code class="bg-gray-100 dark:bg-gray-800 px-1 rounded">
{log.request_path}
</code>
</div>
<% end %>
<%= if log.metadata && map_size(log.metadata) > 0 do %>
<details class="mt-1">
<summary class="cursor-pointer text-xs text-cerulean-600 dark:text-cerulean-400 hover:underline">
<summary class="cursor-pointer text-xs text-blue-600 dark:text-blue-400 hover:underline">
Metadata
</summary>
<pre class="mt-1 text-xs bg-cool-steel-100 dark:bg-cool-steel-800 p-2 rounded overflow-x-auto max-w-md">
<pre class="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto max-w-md">
{Jason.encode!(log.metadata, pretty: true)}
</pre>
</details>
@ -290,8 +281,8 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
</div>
<!-- Pagination -->
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 px-6 py-4 flex items-center justify-between border-t border-cool-steel-200 dark:border-cool-steel-700">
<div class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
<div class="bg-gray-50 dark:bg-gray-800 px-6 py-4 flex items-center justify-between border-t border-gray-200 dark:border-gray-700">
<div class="text-sm text-gray-700 dark:text-gray-300">
Page {@page} Showing {length(@audit_logs)} records
</div>
<div class="flex gap-2">
@ -300,7 +291,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
type="button"
phx-click="prev_page"
disabled={@page == 1}
class="relative inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 disabled:opacity-50 disabled:cursor-not-allowed"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
@ -309,7 +300,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
type="button"
phx-click="next_page"
disabled={length(@audit_logs) < @per_page}
class="relative inline-flex items-center px-4 py-2 border border-cool-steel-300 dark:border-cool-steel-600 text-sm font-medium rounded-md text-cool-steel-700 dark:text-cool-steel-300 bg-white dark:bg-cool-steel-800 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700 disabled:opacity-50 disabled:cursor-not-allowed"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
@ -414,23 +405,12 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
defp audit_badge_class_for_category(category) do
case category do
:success ->
"bg-green-50 text-green-700 ring-green-600/20"
:warning ->
"bg-wheat-50 text-wheat-700 ring-wheat-600/20"
:danger ->
"bg-sweet-salmon-50 text-sweet-salmon-700 ring-sweet-salmon-600/20"
:impersonation ->
"bg-orange-50 text-orange-700 ring-orange-600/20"
:info ->
"bg-cerulean-50 text-cerulean-700 ring-cerulean-600/20"
:default ->
"bg-cool-steel-50 dark:bg-cool-steel-800 text-cool-steel-700 dark:text-cool-steel-300 ring-cool-steel-600/20"
:success -> "bg-green-50 text-green-700 ring-green-600/20"
:warning -> "bg-yellow-50 text-yellow-700 ring-yellow-600/20"
:danger -> "bg-red-50 text-red-700 ring-red-600/20"
:impersonation -> "bg-orange-50 text-orange-700 ring-orange-600/20"
:info -> "bg-blue-50 text-blue-700 ring-blue-600/20"
:default -> "bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 ring-gray-600/20"
end
end

View file

@ -1,123 +1,119 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{t("Dashboard")}</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
System overview and recent activity
</p>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("Dashboard")}</h1>
<p class="text-gray-600 dark:text-gray-400">System overview and recent activity</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
{t("Users")}
</h2>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white">{@user_count}</p>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t("Users")}</h2>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{@user_count}</p>
<.link
navigate={~p"/admin/users"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
View all users →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t("Organizations")}
</h2>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white">{@org_count}</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{@org_count}</p>
<.link
navigate={~p"/admin/organizations"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
View all organizations →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
<.icon name="hero-queue-list" class="w-5 h-5 inline mr-1" /> Oban Dashboard
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Monitor background jobs and queues")}
</p>
<.link
navigate={~p"/admin/oban"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
Open Oban →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
<.icon name="hero-chart-bar" class="w-5 h-5 inline mr-1" /> LiveDashboard
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("System metrics and performance")}
</p>
<.link
navigate={~p"/admin/dashboard"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
Open Dashboard →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
<.icon name="hero-bug-ant" class="w-5 h-5 inline mr-1" /> Exceptions
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Tracked errors and exceptions")}
</p>
<.link
navigate={~p"/admin/errors"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
Open Error Tracker →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
<.icon name="hero-cpu-chip" class="w-5 h-5 inline mr-1" /> Job Monitoring
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Real-time polling and discovery job monitoring")}
</p>
<.link
navigate={~p"/admin/monitoring"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
Open Monitoring →
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
<.icon name="hero-server" class="w-5 h-5 inline mr-1" /> All Agents
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Agents across all organizations and cloud pollers")}
</p>
<.link
navigate={~p"/admin/agents"}
class="text-cerulean-600 dark:text-cerulean-400 hover:underline text-sm"
class="text-blue-600 dark:text-blue-400 hover:underline text-sm"
>
View all agents →
</.link>
</div>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-cool-steel-200 dark:border-white/10 flex items-center justify-between">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Recent Audit Logs")}
</h2>
<.link
navigate={~p"/admin/audit"}
class="text-sm text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View all →
</.link>

View file

@ -96,9 +96,9 @@ defmodule ToweropsWeb.Admin.MonitoringLive do
end
defp event_border_color("completed"), do: "border-green-500"
defp event_border_color("cancelled"), do: "border-sweet-salmon-500"
defp event_border_color("discarded"), do: "border-cool-steel-400"
defp event_border_color(_), do: "border-cool-steel-300 dark:border-cool-steel-600"
defp event_border_color("cancelled"), do: "border-red-500"
defp event_border_color("discarded"), do: "border-gray-400"
defp event_border_color(_), do: "border-gray-300 dark:border-gray-600"
defp event_outcome(job) do
case job.state do

View file

@ -1,93 +1,83 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{t("Job Monitoring Dashboard")}
</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-gray-600 dark:text-gray-400">
{t("Real-time monitoring of polling and discovery jobs")}
</p>
</div>
<!-- Health Metrics Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Completed (1h)
</h3>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white mt-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Completed (1h)</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
{Map.get(@health_metrics, :completed_last_hour, 0)}
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-500 mt-1">jobs completed</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">jobs completed</p>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Failed (1h)
</h3>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white mt-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Failed (1h)</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
{Map.get(@health_metrics, :failed_last_hour, 0)}
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-500 mt-1">failures</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">failures</p>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
{t("Avg Duration")}
</h3>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white mt-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Avg Duration")}</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
<%= if Map.get(@health_metrics, :avg_execution_time_seconds) do %>
{Map.get(@health_metrics, :avg_execution_time_seconds) |> Float.round(1)}s
<% else %>
{t("N/A")}
<% end %>
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-500 mt-1">last hour</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">last hour</p>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
{t("Active Jobs")}
</h3>
<p class="text-3xl font-bold text-cool-steel-900 dark:text-white mt-2">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Active Jobs")}</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
{length(@active_jobs)}
</p>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-500 mt-1">executing now</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">executing now</p>
</div>
</div>
<!-- Active Operations -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Active Operations ({length(@executing_jobs)})
</h2>
<%= if length(@executing_jobs) == 0 do %>
<p class="text-cool-steel-600 dark:text-cool-steel-400 text-sm">
No jobs currently executing
</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">No jobs currently executing</p>
<% else %>
<div class="space-y-3">
<%= for job <- @executing_jobs do %>
<div class="border border-cool-steel-200 dark:border-cool-steel-700 rounded p-3">
<div class="border border-gray-200 dark:border-gray-700 rounded p-3">
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="font-medium text-cool-steel-900 dark:text-white">
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
{job.device.name}
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{worker_name(job.worker)}
</p>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-500 mt-1">
<p class="text-xs text-gray-500 dark:text-gray-500 mt-1">
Started {ToweropsWeb.TimeHelpers.format_time_ago(job.attempted_at)}
</p>
</div>
<div class="flex items-center">
<.icon name="hero-arrow-path" class="w-4 h-4 animate-spin text-cerulean-600" />
<.icon name="hero-arrow-path" class="w-4 h-4 animate-spin text-blue-600" />
</div>
</div>
</div>
@ -98,32 +88,32 @@
<!-- Problems -->
<%= if length(@stuck_jobs) > 0 or length(@failed_jobs) > 0 do %>
<div class="bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 border border-sweet-salmon-200 dark:border-sweet-salmon-800 rounded-lg p-4">
<h2 class="text-lg font-semibold text-sweet-salmon-900 dark:text-sweet-salmon-100 mb-4">
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<h2 class="text-lg font-semibold text-red-900 dark:text-red-100 mb-4">
⚠️ Problems Detected
</h2>
<%= if length(@stuck_jobs) > 0 do %>
<div class="mb-4">
<h3 class="font-medium text-sweet-salmon-800 dark:text-sweet-salmon-200 mb-2">
<h3 class="font-medium text-red-800 dark:text-red-200 mb-2">
Stuck Jobs ({length(@stuck_jobs)})
</h3>
<div class="space-y-2">
<%= for job <- @stuck_jobs do %>
<div class="bg-white dark:bg-cool-steel-800 rounded p-3 border border-sweet-salmon-300 dark:border-sweet-salmon-700">
<div class="bg-white dark:bg-gray-800 rounded p-3 border border-red-300 dark:border-red-700">
<div class="flex items-start justify-between">
<div>
<p class="font-medium text-cool-steel-900 dark:text-white">
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
{job.device.name}
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{worker_name(job.worker)}
</p>
<p class="text-xs text-sweet-salmon-600 dark:text-sweet-salmon-400 mt-1">
<p class="text-xs text-red-600 dark:text-red-400 mt-1">
Running for {duration_in_words(job.attempted_at)}
</p>
</div>
@ -136,26 +126,26 @@
<%= if length(@failed_jobs) > 0 do %>
<div>
<h3 class="font-medium text-sweet-salmon-800 dark:text-sweet-salmon-200 mb-2">
<h3 class="font-medium text-red-800 dark:text-red-200 mb-2">
Failed Jobs ({length(@failed_jobs)})
</h3>
<div class="space-y-2">
<%= for job <- Enum.take(@failed_jobs, 5) do %>
<div class="bg-white dark:bg-cool-steel-800 rounded p-3 border border-sweet-salmon-300 dark:border-sweet-salmon-700">
<div class="bg-white dark:bg-gray-800 rounded p-3 border border-red-300 dark:border-red-700">
<div class="flex items-start justify-between">
<div>
<p class="font-medium text-cool-steel-900 dark:text-white">
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
{job.device.name}
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{worker_name(job.worker)} - Attempt {job.attempt}/{job.max_attempts}
</p>
<%= if job.errors && length(job.errors) > 0 do %>
<p class="text-xs text-sweet-salmon-600 dark:text-sweet-salmon-400 mt-1">
<p class="text-xs text-red-600 dark:text-red-400 mt-1">
{hd(job.errors)["error"] || "Unknown error"}
</p>
<% end %>
@ -170,38 +160,38 @@
<% end %>
<!-- Health Metrics -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Health Metrics")}
</h2>
<!-- Current Activity -->
<div class="mb-6">
<h3 class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-3">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("Current Activity")}
</h3>
<div class="grid grid-cols-2 gap-3">
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 rounded p-3">
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400">Executing</p>
<p class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
<p class="text-xs text-gray-600 dark:text-gray-400">Executing</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{@metrics.executing_count}
</p>
</div>
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 rounded p-3">
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400">Queued</p>
<p class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
<p class="text-xs text-gray-600 dark:text-gray-400">Queued</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{@metrics.queued_count}
</p>
</div>
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 rounded p-3">
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400">Completed (1h)</p>
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
<p class="text-xs text-gray-600 dark:text-gray-400">Completed (1h)</p>
<p class="text-2xl font-bold text-green-600 dark:text-green-400">
{@metrics.completed_last_hour}
</p>
</div>
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 rounded p-3">
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400">Failed (1h)</p>
<p class="text-2xl font-bold text-sweet-salmon-600 dark:text-sweet-salmon-400">
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
<p class="text-xs text-gray-600 dark:text-gray-400">Failed (1h)</p>
<p class="text-2xl font-bold text-red-600 dark:text-red-400">
{@metrics.failed_last_hour}
</p>
</div>
@ -210,18 +200,18 @@
<!-- Success Rates -->
<div class="mb-6">
<h3 class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-3">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("Success Rates")}
</h3>
<div class="space-y-3">
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-cool-steel-600 dark:text-cool-steel-400">Polling (1h)</span>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-600 dark:text-gray-400">Polling (1h)</span>
<span class="font-medium text-gray-900 dark:text-white">
{Float.round(@metrics.polling_success_rate_1h * 100, 1)}%
</span>
</div>
<div class="w-full bg-cool-steel-200 dark:bg-cool-steel-700 rounded-full h-2">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-green-600 h-2 rounded-full transition-all"
style={"width: #{@metrics.polling_success_rate_1h * 100}%"}
@ -232,14 +222,14 @@
<div>
<div class="flex justify-between text-xs mb-1">
<span class="text-cool-steel-600 dark:text-cool-steel-400">Discovery (1h)</span>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-600 dark:text-gray-400">Discovery (1h)</span>
<span class="font-medium text-gray-900 dark:text-white">
{Float.round(@metrics.discovery_success_rate_1h * 100, 1)}%
</span>
</div>
<div class="w-full bg-cool-steel-200 dark:bg-cool-steel-700 rounded-full h-2">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
class="bg-cerulean-600 h-2 rounded-full transition-all"
class="bg-blue-600 h-2 rounded-full transition-all"
style={"width: #{@metrics.discovery_success_rate_1h * 100}%"}
>
</div>
@ -250,13 +240,13 @@
<!-- Performance -->
<div class="mb-6">
<h3 class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-3">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("Performance")}
</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">Avg Execution Time</dt>
<dd class="font-medium text-cool-steel-900 dark:text-white">
<dt class="text-gray-600 dark:text-gray-400">Avg Execution Time</dt>
<dd class="font-medium text-gray-900 dark:text-white">
<%= if @metrics.avg_execution_time_seconds do %>
{Float.round(@metrics.avg_execution_time_seconds, 1)}s
<% else %>
@ -265,8 +255,8 @@
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">Jobs/Hour</dt>
<dd class="font-medium text-cool-steel-900 dark:text-white">
<dt class="text-gray-600 dark:text-gray-400">Jobs/Hour</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{@metrics.completed_last_hour}
</dd>
</div>
@ -275,58 +265,58 @@
<!-- Queue Depths -->
<div>
<h3 class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-3">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
{t("Queue Depths")}
</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">Pollers</dt>
<dd class="font-medium text-cool-steel-900 dark:text-white">
<dt class="text-gray-600 dark:text-gray-400">Pollers</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{@metrics.queue_depths.pollers}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">Discovery</dt>
<dd class="font-medium text-cool-steel-900 dark:text-white">
<dt class="text-gray-600 dark:text-gray-400">Discovery</dt>
<dd class="font-medium text-gray-900 dark:text-white">
{@metrics.queue_depths.discovery}
</dd>
</div>
</dl>
</div>
<div class="mt-4 pt-4 border-t border-cool-steel-200 dark:border-cool-steel-700">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-500">
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs text-gray-500 dark:text-gray-500">
{t("Updates every 10 seconds")}
</p>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Recent Activity")}
</h2>
<%= if length(@recent_events) == 0 do %>
<p class="text-cool-steel-600 dark:text-cool-steel-400 text-sm">No recent activity</p>
<p class="text-gray-600 dark:text-gray-400 text-sm">No recent activity</p>
<% else %>
<div class="space-y-2 max-h-96 overflow-y-auto">
<%= for job <- Enum.take(@recent_events, 50) do %>
<div class={"text-sm border-l-2 pl-3 py-2 #{event_border_color(job.state)}"}>
<div class="flex items-start justify-between">
<div class="flex-1">
<p class="font-medium text-cool-steel-900 dark:text-white">
<p class="font-medium text-gray-900 dark:text-white">
<%= if job.device do %>
{job.device.name}
<% else %>
Device #{get_in(job.args, ["device_id"])}
<% end %>
</p>
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-xs text-gray-600 dark:text-gray-400">
{worker_name(job.worker)} · {event_outcome(job)}
</p>
</div>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-500 whitespace-nowrap ml-2">
<span class="text-xs text-gray-500 dark:text-gray-500 whitespace-nowrap ml-2">
{ToweropsWeb.TimeHelpers.format_time_ago(
job.completed_at || job.cancelled_at || job.discarded_at || job.inserted_at
)}

View file

@ -1,12 +1,8 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
{t("All Organizations")}
</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
{t_admin("Manage organizations")}
</p>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Organizations")}</h1>
<p class="text-gray-600 dark:text-gray-400">{t_admin("Manage organizations")}</p>
</div>
<!-- Global Billing Defaults Card -->
@ -27,7 +23,7 @@
type="button"
phx-click="edit_global_defaults"
data-test="edit-global-defaults"
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
class="rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500"
>
Edit Defaults
</button>
@ -57,7 +53,7 @@
</dl>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.table id="organizations" rows={@organizations}>
<:col :let={org} label={t("Name")}>{org.name}</:col>
<:col :let={org} label={t("Slug")}>{org.slug}</:col>
@ -66,8 +62,7 @@
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
if(org.subscription_plan == "paid",
do: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
else:
"bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-700 dark:text-cool-steel-300"
else: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"
)
]}>
{org.subscription_plan}
@ -80,7 +75,7 @@
<%= if org.custom_free_device_limit || org.custom_price_per_device do %>
<div class="flex flex-wrap gap-1">
<%= if org.custom_free_device_limit do %>
<span class="inline-flex items-center rounded-full bg-cerulean-100 px-2 py-0.5 text-xs font-medium text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400">
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
{org.custom_free_device_limit} {t("free")}
</span>
<% end %>
@ -91,9 +86,7 @@
<% end %>
</div>
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-500 text-xs">
{t("Default")}
</span>
<span class="text-gray-400 dark:text-gray-500 text-xs">{t("Default")}</span>
<% end %>
</:col>
<:col :let={org} label={t("Members")}>{length(org.memberships)}</:col>
@ -129,15 +122,15 @@
<%!-- Billing Override Edit Panel --%>
<%= if @editing_org do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t_admin("Billing Overrides")} — {@editing_org.name}
</h2>
<button
id="cancel-overrides-btn"
phx-click="cancel_overrides"
class="text-sm text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
{t("Cancel")}
</button>
@ -177,7 +170,7 @@
id="clear-overrides-btn"
type="button"
phx-click="clear_overrides"
class="text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
>
{t_admin("Reset to Defaults")}
</button>
@ -219,12 +212,12 @@
type="number"
name="global_pricing[default_free_devices]"
value={@global_form.data["default_free_devices"]}
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-slate-600 dark:bg-slate-700"
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-slate-600 dark:bg-slate-700"
/>
<%= if error = @global_form.errors[:default_free_devices] do %>
<p
id="global_pricing_default_free_devices-error"
class="mt-1 text-sm text-sweet-salmon-600"
class="mt-1 text-sm text-red-600"
>
{elem(error, 0)}
</p>
@ -240,12 +233,12 @@
step="0.01"
name="global_pricing[default_price_per_device]"
value={@global_form.data["default_price_per_device"]}
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-slate-600 dark:bg-slate-700"
class="mt-1 block w-full rounded-md border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-slate-600 dark:bg-slate-700"
/>
<%= if error = @global_form.errors[:default_price_per_device] do %>
<p
id="global_pricing_default_price_per_device-error"
class="mt-1 text-sm text-sweet-salmon-600"
class="mt-1 text-sm text-red-600"
>
{elem(error, 0)}
</p>
@ -263,7 +256,7 @@
</button>
<button
type="submit"
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
class="rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500"
>
Save Changes
</button>
@ -296,7 +289,7 @@
type="button"
phx-click="confirm_pricing_update"
data-test="confirm-pricing-update"
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cerulean-500"
class="rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white hover:bg-blue-500"
>
Confirm Update
</button>

View file

@ -1,16 +1,14 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
{t("IP Access Control")}
</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("IP Access Control")}</h1>
<p class="text-gray-600 dark:text-gray-400">
{t("Manage IP allowlist and view denied IPs")}
</p>
</div>
<!-- Tabs -->
<div class="border-b border-cool-steel-200 dark:border-white/10">
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8">
<button
id="security-tab-whitelist"
@ -19,9 +17,9 @@
class={[
"border-b-2 py-4 px-1 text-sm font-medium",
if(@current_tab == "whitelist",
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
do: "border-blue-500 text-blue-600 dark:text-blue-400",
else:
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
)
]}
>
@ -35,9 +33,9 @@
class={[
"border-b-2 py-4 px-1 text-sm font-medium",
if(@current_tab == "blocked",
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
do: "border-blue-500 text-blue-600 dark:text-blue-400",
else:
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
)
]}
>
@ -50,7 +48,7 @@
<%= if @current_tab == "whitelist" do %>
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Allowed IPs and CIDR Ranges")}
</h2>
<.button id="show-whitelist-form" phx-click="show_whitelist_form">
@ -59,7 +57,7 @@
</div>
<%= if @show_whitelist_form do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<.form
for={@whitelist_form}
phx-submit="add_whitelist"
@ -92,9 +90,9 @@
</div>
<% end %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<%= if Enum.empty?(@whitelist) do %>
<div class="p-8 text-center text-cool-steel-500 dark:text-cool-steel-400">
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
{t("No allowed IPs or CIDR ranges")}
</div>
<% else %>
@ -105,7 +103,7 @@
<%= if entry.added_by do %>
{entry.added_by.email}
<% else %>
<span class="text-cool-steel-400">Unknown</span>
<span class="text-gray-400">Unknown</span>
<% end %>
</:col>
<:col :let={entry} label={t("Added")}>
@ -130,16 +128,16 @@
<%= if @current_tab == "blocked" do %>
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Denied IP Addresses")}
</h2>
<div class="flex items-center gap-2">
<label class="text-sm text-cool-steel-600 dark:text-cool-steel-400">Filter:</label>
<label class="text-sm text-gray-600 dark:text-gray-400">Filter:</label>
<select
phx-change="change_filter"
name="filter"
class="rounded-md border-cool-steel-300 dark:border-white/10 dark:bg-cool-steel-900 text-sm"
class="rounded-md border-gray-300 dark:border-white/10 dark:bg-gray-900 text-sm"
>
<option value="all" selected={@filter == "all"}>All Bans</option>
<option value="permanent" selected={@filter == "permanent"}>Permanent Only</option>
@ -148,9 +146,9 @@
</div>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<%= if Enum.empty?(@blocked_ips) do %>
<div class="p-8 text-center text-cool-steel-500 dark:text-cool-steel-400">
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
{t("No denied IPs")}
</div>
<% else %>
@ -161,13 +159,13 @@
"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium",
case block.offense_count do
1 ->
"bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
2 ->
"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
_ ->
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
end
]}>
{block.offense_count}
@ -175,11 +173,11 @@
</:col>
<:col :let={block} label={t("Status")}>
<%= if Towerops.Security.IpBlock.permanent?(block) do %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
{t("Permanent")}
</span>
<% else %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
{Towerops.Security.IpBlock.ban_duration(block)}
</span>
<% end %>
@ -188,7 +186,7 @@
{ToweropsWeb.TimeHelpers.format_time_ago(block.last_violation_at)}
</:col>
<:col :let={block} label={t("Reason")}>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 max-w-xs truncate">
<div class="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{block.reason}
</div>
</:col>
@ -199,7 +197,7 @@
data-confirm={
t("Unblock this IP address? This will remove all offense history.")
}
class="text-sm text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-800 dark:hover:text-cerulean-300"
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
{t("Unblock")}
</button>

View file

@ -1,11 +1,11 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{t("All Users")}</h1>
<p class="text-cool-steel-600 dark:text-cool-steel-400">Manage system users</p>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Users")}</h1>
<p class="text-gray-600 dark:text-gray-400">Manage system users</p>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.table id="users" rows={@users}>
<:col :let={user} label={t("Email")}>{user.email}</:col>
<:col :let={user} label={t("Superuser")}>
@ -23,7 +23,7 @@
<button
type="submit"
disabled={user.id == @current_scope.user.id}
class="text-sm text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-800 dark:hover:text-cerulean-300 disabled:text-cool-steel-400 disabled:cursor-not-allowed"
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:text-gray-400 disabled:cursor-not-allowed"
>
{t("Impersonate")}
</button>

View file

@ -3,18 +3,18 @@
<div class="mb-6">
<.link
navigate={~p"/agents/#{@agent_token.id}"}
class="text-sm font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
<span class="hero-arrow-left h-4 w-4 inline"></span> Back to agent
</.link>
</div>
<div class="bg-white dark:bg-cool-steel-800/50 shadow rounded-lg">
<div class="px-6 py-5 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<div class="bg-white dark:bg-gray-800/50 shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
{t("Edit Agent")}
</h3>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Update the agent name. The authentication token cannot be changed.")}
</p>
</div>
@ -35,7 +35,7 @@
placeholder="e.g., Datacenter Agent"
required
/>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("A descriptive name to identify this agent in your organization.")}
</p>
</div>
@ -49,9 +49,9 @@
<div
:if={@form[:allow_remote_debug].value}
class="rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-3 mt-2"
class="rounded-md bg-amber-50 dark:bg-amber-900/20 p-3 mt-2"
>
<div class="flex items-start gap-2 text-wheat-800 dark:text-wheat-200">
<div class="flex items-start gap-2 text-amber-800 dark:text-amber-200">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium">Performance Impact</p>
@ -65,7 +65,7 @@
</div>
</div>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Enable verbose logging for troubleshooting SNMP discovery and polling issues.")}
{t("Only organization members can toggle this setting.")}
</p>
@ -75,7 +75,7 @@
<div class="mt-6 flex items-center justify-end gap-x-4">
<.link
navigate={~p"/agents/#{@agent_token.id}"}
class="text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</.link>

View file

@ -34,9 +34,9 @@ defmodule ToweropsWeb.AgentLive.Helpers do
def status_badge_class(status) do
case status do
:online -> "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300"
:warning -> "bg-wheat-100 text-wheat-800 dark:bg-wheat-900 dark:text-wheat-300"
:offline -> "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900 dark:text-sweet-salmon-300"
:never -> "bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-800 dark:text-cool-steel-300"
:warning -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300"
:offline -> "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"
:never -> "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"
end
end
@ -47,9 +47,9 @@ defmodule ToweropsWeb.AgentLive.Helpers do
def status_dot_class(status) do
case status do
:online -> "bg-green-600 dark:bg-green-400 animate-pulse"
:warning -> "bg-wheat-600 dark:bg-wheat-400"
:offline -> "bg-sweet-salmon-600 dark:bg-sweet-salmon-400"
:never -> "bg-cool-steel-400 dark:bg-cool-steel-600"
:warning -> "bg-yellow-600 dark:bg-yellow-400"
:offline -> "bg-red-600 dark:bg-red-400"
:never -> "bg-gray-400 dark:bg-gray-600"
end
end

View file

@ -16,8 +16,8 @@
<!-- New Agent Form -->
<div id="new-agent-form" class="hidden mt-6">
<div class="bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Create New Agent")}
</h3>
<.form id="create-agent-form" for={@agent_form} phx-submit="create_agent">
@ -43,7 +43,7 @@
<button
type="button"
phx-click={JS.hide(to: "#new-agent-form")}
class="px-3 py-2 text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300 hover:text-cool-steel-900 dark:hover:text-white"
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</button>
@ -55,14 +55,9 @@
<%= if !@has_agents do %>
<div class="text-center py-16">
<.icon
name="hero-server"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
{t("No agents")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{t("No agents")}</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by creating your first remote agent.")}
</p>
<div class="mt-6">
@ -87,7 +82,7 @@
</span>
<% end %>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-wheat-50 dark:bg-wheat-900/30 px-2 py-0.5 text-xs font-medium text-wheat-700 dark:text-wheat-300 ring-1 ring-inset ring-wheat-600/20 dark:ring-wheat-400/30">
<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">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
@ -104,10 +99,10 @@
<:col :let={{_id, agent}} label={t("Device")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-cool-steel-900 dark:text-white">
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<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
@ -116,16 +111,16 @@
</:col>
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-cool-steel-900 dark:text-white">
<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-cool-steel-500 dark:text-cool-steel-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" />
</div>
<% end %>
<%= if agent.last_ip do %>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5 font-mono">
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
{agent.last_ip}
</div>
<% end %>
@ -133,14 +128,14 @@
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 font-mono">
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
</:col>
<:col :let={{_id, agent}} label={t("Created")}>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="text-sm text-gray-600 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
</div>
</:col>
@ -173,7 +168,7 @@
</.button>
</div>
<% else %>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">Disabled</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
<% end %>
</:action>
</.table>
@ -183,10 +178,10 @@
<%!-- Cloud Pollers Section (Superadmin Only) --%>
<%= if @is_superuser && @has_cloud_pollers do %>
<div class="mt-12">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Cloud Pollers")}
</h2>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t(
"Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins."
)}
@ -206,7 +201,7 @@
</span>
<% end %>
<%= if agent.allow_remote_debug do %>
<span class="inline-flex items-center gap-1 rounded-full bg-wheat-50 dark:bg-wheat-900/30 px-2 py-0.5 text-xs font-medium text-wheat-700 dark:text-wheat-300 ring-1 ring-inset ring-wheat-600/20 dark:ring-wheat-400/30">
<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">
<.icon name="hero-bug-ant" class="h-3 w-3" /> Debug
</span>
<% end %>
@ -223,10 +218,10 @@
<:col :let={{_id, agent}} label={t("Device")}>
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-cool-steel-900 dark:text-white">
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<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
@ -235,16 +230,16 @@
</:col>
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-cool-steel-900 dark:text-white">
<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-cool-steel-500 dark:text-cool-steel-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" />
</div>
<% end %>
<%= if agent.last_ip do %>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5 font-mono">
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono">
{agent.last_ip}
</div>
<% end %>
@ -252,14 +247,14 @@
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400 font-mono">
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
</div>
<% end %>
</:col>
<:col :let={{_id, agent}} label={t("Created")}>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="text-sm text-gray-600 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
</div>
</:col>
@ -292,7 +287,7 @@
</.button>
</div>
<% else %>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">Disabled</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Disabled</span>
<% end %>
</:action>
</.table>
@ -303,11 +298,11 @@
<%!-- Global Default Cloud Poller (Superadmin Only) --%>
<%= if @is_superuser do %>
<div class="mt-8">
<div class="bg-wheat-50 dark:bg-wheat-900/10 border border-wheat-200 dark:border-wheat-800/30 rounded-lg p-6">
<h3 class="text-lg font-semibold text-wheat-900 dark:text-wheat-200 mb-2">
<div class="bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/30 rounded-lg p-6">
<h3 class="text-lg font-semibold text-amber-900 dark:text-amber-200 mb-2">
{t("Global Default Cloud Poller")}
</h3>
<p class="text-sm text-wheat-700 dark:text-wheat-300 mb-4">
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">
{t(
"Fallback agent for organizations without a default agent configured. Devices with no assignment at any level will use this agent."
)}
@ -321,13 +316,13 @@
>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-wheat-900 dark:text-wheat-200 mb-2">
<label class="block text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Select Global Default")}
</label>
<select
name="agent_token_id"
value={@selected_global_default}
class="block w-full rounded-md border-wheat-300 dark:border-wheat-700 bg-white dark:bg-cool-steel-900 text-cool-steel-900 dark:text-white shadow-sm focus:border-wheat-500 focus:ring-wheat-500 sm:text-sm"
class="block w-full rounded-md border-amber-300 dark:border-amber-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-white shadow-sm focus:border-amber-500 focus:ring-amber-500 sm:text-sm"
>
<option value="">No global default (direct Phoenix polling)</option>
<%= for cloud_poller <- @cloud_pollers_list do %>
@ -345,18 +340,18 @@
<% selected_poller =
Enum.find(@cloud_pollers_list, &(&1.id == @global_default_cloud_poller_id)) %>
<%= if selected_poller do %>
<p class="text-xs text-wheat-600 dark:text-wheat-400">
<p class="text-xs text-amber-600 dark:text-amber-400">
<.icon name="hero-check-circle" class="h-4 w-4 inline" /> Currently using:
<span class="font-medium">{selected_poller.name}</span>
</p>
<% else %>
<p class="text-xs text-sweet-salmon-600 dark:text-sweet-salmon-400">
<p class="text-xs text-red-600 dark:text-red-400">
<.icon name="hero-exclamation-triangle" class="h-4 w-4 inline" />
{t("Warning: Selected agent no longer exists. Please choose a new agent.")}
</p>
<% end %>
<% else %>
<p class="text-xs text-wheat-600 dark:text-wheat-400">
<p class="text-xs text-amber-600 dark:text-amber-400">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
{t(
"No global default configured. Devices without assignments will use direct Phoenix cluster polling."
@ -382,7 +377,7 @@
value: %{agent_token_id: @global_default_cloud_poller_id || ""}
)
}
class="px-3 py-2 text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300 hover:text-cool-steel-900 dark:hover:text-white"
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</button>
@ -403,26 +398,26 @@
>
<div class="flex min-h-screen items-center justify-center p-4">
<div
class="fixed inset-0 bg-cool-steel-500/75 dark:bg-cool-steel-950/75 transition-opacity"
class="fixed inset-0 bg-gray-500/75 dark:bg-gray-950/75 transition-opacity"
phx-click="close_token_modal"
>
</div>
<div class="relative bg-white dark:bg-cool-steel-900 rounded-lg shadow-xl max-w-2xl w-full">
<div class="relative bg-white dark:bg-gray-900 rounded-lg shadow-xl max-w-2xl w-full">
<div class="p-6">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Agent Setup Instructions")}
</h3>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Use the token and Docker Compose configuration below to deploy this agent.")}
</p>
</div>
<button
type="button"
phx-click="close_token_modal"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
@ -430,16 +425,16 @@
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Agent Name")}
</label>
<div class="text-base font-mono text-cool-steel-900 dark:text-white">
<div class="text-base font-mono text-gray-900 dark:text-white">
{@new_token.agent_token.name}
</div>
</div>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Authentication Token")}
</label>
<div class="relative">
@ -447,7 +442,7 @@
type="text"
readonly
value={String.trim(@new_token.token)}
class="w-full font-mono text-sm bg-cool-steel-50 dark:bg-cool-steel-800 border border-cool-steel-300 dark:border-cool-steel-700 rounded px-3 py-2 pr-28 text-cool-steel-900 dark:text-white select-all"
class="w-full font-mono text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded px-3 py-2 pr-28 text-gray-900 dark:text-white select-all"
id="agent-token-input"
onclick="this.select()"
/>
@ -457,27 +452,27 @@
phx-update="ignore"
data-target="#agent-token-input"
id={"copy-token-#{@new_token.agent_token.id}"}
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-cerulean-600 hover:bg-cerulean-700 dark:bg-cerulean-500 dark:hover:bg-cerulean-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" />
<span>{t("Copy")}</span>
</button>
</div>
<p class="mt-1.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{t("Click the token to select all, or use the Copy button")}
</p>
</div>
<div class="bg-cool-steel-50 dark:bg-cool-steel-800 border border-cool-steel-200 dark:border-cool-steel-700 rounded-lg p-4">
<h4 class="text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Docker Compose Setup with Auto-Updates")}
</h4>
<p class="text-xs text-cool-steel-600 dark:text-cool-steel-400 mb-3">
<p class="text-xs text-gray-600 dark:text-gray-400 mb-3">
{t(
"This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 12 hours."
)}
</p>
<pre class="text-xs font-mono text-cool-steel-600 dark:text-cool-steel-400 overflow-x-auto"><code>services:
<pre class="text-xs font-mono text-gray-600 dark:text-gray-400 overflow-x-auto"><code>services:
towerops-agent:
image: {@agent_image}
container_name: towerops-agent
@ -510,7 +505,7 @@
phx-update="ignore"
data-target="#docker-compose-example"
id={"copy-docker-compose-#{@new_token.agent_token.id}"}
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-white bg-cool-steel-700 hover:bg-cool-steel-600 dark:bg-cool-steel-600 dark:hover:bg-cool-steel-500 rounded transition-colors"
class="inline-flex items-center px-2.5 py-1.5 text-xs font-medium text-white bg-gray-700 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-500 rounded transition-colors"
>
<.icon name="hero-clipboard" class="h-3 w-3" /> Copy to clipboard
</button>

View file

@ -116,10 +116,10 @@ defmodule ToweropsWeb.AgentLive.Show do
defp source_badge_class(source) do
case source do
:direct -> "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900 dark:text-cerulean-300"
:direct -> "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300"
:site -> "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300"
:organization -> "bg-wheat-100 text-wheat-800 dark:bg-wheat-900 dark:text-wheat-300"
:unknown -> "bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-800 dark:text-cool-steel-300"
:organization -> "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300"
:unknown -> "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300"
end
end
end

View file

@ -29,21 +29,21 @@
"Are you sure you want to restart this agent? It will disconnect and restart its container."
)
}
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-wheat-600 text-white hover:bg-wheat-700 focus:ring-wheat-500 dark:bg-wheat-500 dark:hover:bg-wheat-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" />
<span class="hidden sm:inline">Restart</span>
</button>
<.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-cerulean-600 text-white hover:bg-cerulean-700 focus:ring-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-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" />
<span class="hidden sm:inline">Edit Agent</span>
</.link>
<.link
navigate={~p"/agents"}
class="inline-flex items-center gap-2 text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300 hover:text-cool-steel-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" />
<span class="hidden sm:inline">Back to Agents</span>
@ -54,7 +54,7 @@
<!-- Tab Navigation (Superuser Only) -->
<div
:if={@current_scope.user.is_superuser}
class="mt-6 border-b border-cool-steel-200 dark:border-white/10"
class="mt-6 border-b border-gray-200 dark:border-white/10"
>
<nav class="-mb-px flex space-x-8">
<button
@ -64,9 +64,9 @@
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if(@active_tab == "overview",
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
do: "border-blue-500 text-blue-600 dark:text-blue-400",
else:
"border-transparent text-cool-steel-500 hover:text-cool-steel-700 hover:border-cool-steel-300 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
)
]}
>
@ -79,9 +79,9 @@
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if(@active_tab == "debug",
do: "border-cerulean-500 text-cerulean-600 dark:text-cerulean-400",
do: "border-blue-500 text-blue-600 dark:text-blue-400",
else:
"border-transparent text-cool-steel-500 hover:text-cool-steel-700 hover:border-cool-steel-300 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
)
]}
>
@ -96,15 +96,15 @@
"rounded-md p-4 mb-4",
if(@agent_token.allow_remote_debug,
do: "bg-green-50 dark:bg-green-900/20",
else: "bg-cool-steel-50 dark:bg-cool-steel-900/20"
else: "bg-gray-50 dark:bg-gray-900/20"
)
]}>
<p class="text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium text-gray-900 dark:text-white">
Debug Status: {if @agent_token.allow_remote_debug,
do: "Enabled ✓",
else: "Disabled"}
</p>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
<%= if @agent_token.allow_remote_debug do %>
{t("Verbose logging is enabled. All SNMP data and agent messages are being logged.")}
<% else %>
@ -116,33 +116,33 @@
</div>
<div class="space-y-4">
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("View Debug Logs")}
</h3>
<div class="rounded-md bg-cool-steel-50 dark:bg-cool-steel-900 p-4">
<p class="text-sm mb-2 font-medium text-cool-steel-900 dark:text-white">
<div class="rounded-md bg-gray-50 dark:bg-gray-900 p-4">
<p class="text-sm mb-2 font-medium text-gray-900 dark:text-white">
{t("Production (kubectl):")}
</p>
<code class="text-xs text-cool-steel-800 dark:text-cool-steel-200 block">
<code class="text-xs text-gray-800 dark:text-gray-200 block">
kubectl logs -n towerops deployment/towerops --tail=100 | grep "agent_token_id: {@agent_token.id}"
</code>
</div>
<div class="rounded-md bg-cool-steel-50 dark:bg-cool-steel-900 p-4">
<p class="text-sm mb-2 font-medium text-cool-steel-900 dark:text-white">
<div class="rounded-md bg-gray-50 dark:bg-gray-900 p-4">
<p class="text-sm mb-2 font-medium text-gray-900 dark:text-white">
{t("Development (tail):")}
</p>
<code class="text-xs text-cool-steel-800 dark:text-cool-steel-200 block">
<code class="text-xs text-gray-800 dark:text-gray-200 block">
tail -f _build/dev/lib/towerops/priv/log/dev.log | grep "{@agent_token.id}"
</code>
</div>
<div class="mt-4">
<p class="text-sm font-medium mb-2 text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium mb-2 text-gray-900 dark:text-white">
{t("What Gets Logged:")}
</p>
<ul class="text-sm text-cool-steel-600 dark:text-cool-steel-400 list-disc list-inside space-y-1">
<ul class="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
<li>Agent connection/disconnection events</li>
<li>Job submissions (device IDs and job types)</li>
<li>SNMP discovery results (full OID maps)</li>
@ -160,10 +160,10 @@
<!-- Agent Status Overview -->
<div class="mt-6 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
<!-- Status Card -->
<div class="bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">Status</p>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Status</p>
<% {status, label} = agent_status(@agent_token) %>
<div class="mt-2">
<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)}"}>
@ -172,38 +172,36 @@
</span>
</div>
</div>
<.icon name="hero-signal" class="h-8 w-8 text-cool-steel-400 dark:text-cool-steel-500" />
<.icon name="hero-signal" class="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
</div>
<!-- Device Count Card -->
<div class="bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">Device</p>
<p class="mt-2 text-3xl font-semibold text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Device</p>
<p class="mt-2 text-3xl font-semibold text-gray-900 dark:text-white">
{length(@polling_targets)}
</p>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{@direct_assignments} direct
</p>
</div>
<.icon name="hero-server" class="h-8 w-8 text-cool-steel-400 dark:text-cool-steel-500" />
<.icon name="hero-server" class="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
</div>
<!-- Last Seen Card -->
<div class="bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Last Seen
</p>
<p class="mt-2 text-lg font-semibold text-cool-steel-900 dark:text-white">
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Last Seen</p>
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-white">
<.timestamp datetime={@agent_token.last_seen_at} timezone={@timezone} />
</p>
<%= if @agent_token.last_seen_at do %>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<.timestamp
datetime={@agent_token.last_seen_at}
timezone={@timezone}
@ -212,61 +210,54 @@
</p>
<% end %>
<%= if @agent_token.last_ip do %>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400 font-mono">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 font-mono">
{@agent_token.last_ip}
</p>
<% end %>
</div>
<.icon
name="hero-clock"
class="h-8 w-8 text-cool-steel-400 dark:text-cool-steel-500 flex-shrink-0"
/>
<.icon name="hero-clock" class="h-8 w-8 text-gray-400 dark:text-gray-500 flex-shrink-0" />
</div>
</div>
<!-- Uptime Card -->
<div class="bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">Uptime</p>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Uptime</p>
<%= if @agent_token.metadata["uptime_seconds"] do %>
<p class="mt-2 text-lg font-semibold text-cool-steel-900 dark:text-white">
<p class="mt-2 text-lg font-semibold text-gray-900 dark:text-white">
{format_uptime(@agent_token.metadata["uptime_seconds"])}
</p>
<% else %>
<p class="mt-2 text-lg font-semibold text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-lg font-semibold text-gray-500 dark:text-gray-400">
{t("Unknown")}
</p>
<% end %>
</div>
<.icon name="hero-clock" class="h-8 w-8 text-cool-steel-400 dark:text-cool-steel-500" />
<.icon name="hero-clock" class="h-8 w-8 text-gray-400 dark:text-gray-500" />
</div>
</div>
</div>
<!-- Agent Details -->
<%= if @agent_token.metadata && map_size(@agent_token.metadata) > 0 do %>
<div class="mt-6 bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Agent Details")}
</h3>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<%= if @agent_token.metadata["version"] do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Version
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white font-mono">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Version</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white font-mono">
{format_agent_version(@agent_token.metadata["version"])}
</dd>
</div>
<% end %>
<%= if @agent_token.metadata["uptime_seconds"] do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Uptime
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Uptime</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{format_uptime(@agent_token.metadata["uptime_seconds"])}
</dd>
</div>
@ -276,44 +267,38 @@
<% end %>
<!-- Agent Timestamps -->
<div class="mt-6 bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("Timestamps")}
</h3>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Created
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Created</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
<.timestamp datetime={@agent_token.inserted_at} timezone={@timezone} />
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Last Updated
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Last Updated</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
<.timestamp datetime={@agent_token.updated_at} timezone={@timezone} />
</dd>
</div>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">
{t("Last Seen (Heartbeat)")}
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white">
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
<.timestamp datetime={@agent_token.last_seen_at} timezone={@timezone} />
</dd>
</div>
<%= if @agent_token.last_ip do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Last IP Address
</dt>
<dd class="mt-1 text-sm text-cool-steel-900 dark:text-white font-mono">
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Last IP Address</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white font-mono">
{@agent_token.last_ip}
</dd>
</div>
@ -322,41 +307,38 @@
</div>
<!-- Device List -->
<div class="mt-6 bg-white dark:bg-cool-steel-800/50 border border-cool-steel-200 dark:border-white/10 rounded-lg">
<div class="p-6 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
<div class="p-6 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Polling Targets")}
</h3>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Device that this agent is responsible for polling")}
</p>
</div>
<%= if @polling_targets == [] do %>
<div class="p-12 text-center">
<.icon
name="hero-server"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
/>
<h3 class="mt-4 text-sm font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">
{t("No devices assigned")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("This agent is not currently polling any device.")}
</p>
</div>
<% else %>
<div class="divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<%= for device <- @polling_targets do %>
<.link
navigate={~p"/devices/#{device.id}"}
class="block hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800 transition-colors"
class="block hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="p-6">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3">
<p class="text-sm font-medium text-cool-steel-900 dark:text-white truncate">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{device.name}
</p>
<% {source, source_label} = assignment_source(device, @agent_token.id) %>
@ -364,7 +346,7 @@
{source_label}
</span>
</div>
<div class="mt-1 flex items-center gap-4 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="mt-1 flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
<span class="flex items-center gap-1">
<.icon name="hero-globe-alt" class="h-4 w-4" />
{device.ip_address}
@ -379,7 +361,7 @@
</div>
<.icon
name="hero-chevron-right"
class="h-5 w-5 text-cool-steel-400 dark:text-cool-steel-500"
class="h-5 w-5 text-gray-400 dark:text-gray-500"
/>
</div>
</div>

View file

@ -334,12 +334,12 @@ defmodule ToweropsWeb.AlertLive.Index do
color = severity_color(alert)
case color do
"red" -> "bg-sweet-salmon-500"
"red" -> "bg-red-500"
"orange" -> "bg-orange-500"
"yellow" -> "bg-wheat-500"
"yellow" -> "bg-yellow-500"
"green" -> "bg-green-500"
"gray" -> "bg-cool-steel-300 dark:bg-cool-steel-600"
_ -> "bg-cool-steel-300"
"gray" -> "bg-gray-300 dark:bg-gray-600"
_ -> "bg-gray-300"
end
end

View file

@ -7,13 +7,13 @@
<%!-- Compact header --%>
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-3">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">{t("Alerts")}</h1>
<span class="rounded px-2 py-0.5 text-xs font-medium bg-cerulean-50 text-cerulean-700 dark:bg-cerulean-900/20 dark:text-cerulean-400">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("Alerts")}</h1>
<span class="rounded px-2 py-0.5 text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400">
{t("Experimental")}
</span>
</div>
</div>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
{t("Monitor and triage network alerts, grouped by site with subscriber impact.")}
</p>
@ -23,17 +23,16 @@
patch={~p"/alerts?filter=unresolved&sort=#{@sort_by}"}
class={[
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "unresolved" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "unresolved" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "unresolved" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Unresolved")}
<span class={[
"rounded-full px-1.5 py-0.5 text-xs",
@filter == "unresolved" && "bg-white/20 dark:bg-cool-steel-900/30",
@filter != "unresolved" && "bg-cool-steel-200 dark:bg-cool-steel-700"
@filter == "unresolved" && "bg-white/20 dark:bg-gray-900/30",
@filter != "unresolved" && "bg-gray-200 dark:bg-gray-700"
]}>
{@counts.unresolved}
</span>
@ -42,16 +41,16 @@
patch={~p"/alerts?filter=critical&sort=#{@sort_by}"}
class={[
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "critical" && "bg-sweet-salmon-600 text-white",
@filter == "critical" && "bg-red-600 text-white",
@filter != "critical" &&
"bg-sweet-salmon-50 text-sweet-salmon-700 hover:bg-sweet-salmon-100 dark:bg-sweet-salmon-900/20 dark:text-sweet-salmon-400 dark:hover:bg-sweet-salmon-900/40"
"bg-red-50 text-red-700 hover:bg-red-100 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/40"
]}
>
{t("Critical")}
<span class={[
"rounded-full px-1.5 py-0.5 text-xs",
@filter == "critical" && "bg-white/20",
@filter != "critical" && "bg-sweet-salmon-100 dark:bg-sweet-salmon-900/40"
@filter != "critical" && "bg-red-100 dark:bg-red-900/40"
]}>
{@counts.critical}
</span>
@ -60,16 +59,16 @@
patch={~p"/alerts?filter=all&sort=#{@sort_by}"}
class={[
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "all" && "bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "all" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "all" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("All")}
<span class={[
"rounded-full px-1.5 py-0.5 text-xs",
@filter == "all" && "bg-white/20 dark:bg-cool-steel-900/30",
@filter != "all" && "bg-cool-steel-200 dark:bg-cool-steel-700"
@filter == "all" && "bg-white/20 dark:bg-gray-900/30",
@filter != "all" && "bg-gray-200 dark:bg-gray-700"
]}>
{@counts.all}
</span>
@ -78,34 +77,33 @@
patch={~p"/alerts?filter=resolved&sort=#{@sort_by}"}
class={[
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "resolved" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "resolved" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "resolved" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Resolved")}
<span class={[
"rounded-full px-1.5 py-0.5 text-xs",
@filter == "resolved" && "bg-white/20 dark:bg-cool-steel-900/30",
@filter != "resolved" && "bg-cool-steel-200 dark:bg-cool-steel-700"
@filter == "resolved" && "bg-white/20 dark:bg-gray-900/30",
@filter != "resolved" && "bg-gray-200 dark:bg-gray-700"
]}>
{@counts.resolved}
</span>
</.link>
<span class="text-cool-steel-300 dark:text-cool-steel-600 mx-1">|</span>
<span class="text-gray-300 dark:text-gray-600 mx-1">|</span>
<%!-- Sort controls --%>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">{t("Sort")}:</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{t("Sort")}:</span>
<.link
patch={~p"/alerts?filter=#{@filter}&sort=severity"}
class={[
"text-xs px-2 py-1 rounded transition-colors",
@sort_by == "severity" &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300 font-medium",
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 font-medium",
@sort_by != "severity" &&
"text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
]}
>
{t("Severity")}
@ -115,9 +113,9 @@
class={[
"text-xs px-2 py-1 rounded transition-colors",
@sort_by == "age" &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300 font-medium",
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 font-medium",
@sort_by != "age" &&
"text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
]}
>
{t("Oldest")}
@ -127,9 +125,9 @@
class={[
"text-xs px-2 py-1 rounded transition-colors",
@sort_by == "impact" &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300 font-medium",
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 font-medium",
@sort_by != "impact" &&
"text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
"text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
]}
>
{t("Impact")}
@ -139,9 +137,9 @@
<%!-- Bulk actions bar --%>
<div
:if={MapSet.size(@selected_alerts) > 0}
class="flex items-center gap-3 mb-3 px-3 py-2 rounded-lg bg-cerulean-50 dark:bg-cerulean-900/20 border border-cerulean-200 dark:border-cerulean-800"
class="flex items-center gap-3 mb-3 px-3 py-2 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
>
<span class="text-xs font-medium text-cerulean-800 dark:text-cerulean-300">
<span class="text-xs font-medium text-blue-800 dark:text-blue-300">
{MapSet.size(@selected_alerts)} {t("selected")}
</span>
<.button type="button" phx-click="bulk_acknowledge" variant="primary">
@ -153,7 +151,7 @@
<button
type="button"
phx-click="select_none"
class="text-xs text-cerulean-600 dark:text-cerulean-400 hover:underline ml-auto"
class="text-xs text-blue-600 dark:text-blue-400 hover:underline ml-auto"
>
{t("Clear selection")}
</button>
@ -171,7 +169,7 @@
<h3 class="text-lg font-semibold text-green-700 dark:text-green-400">
{t("All Clear!")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 max-w-sm">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 max-w-sm">
<%= case @filter do %>
<% "unresolved" -> %>
{t("No active alerts. Your network is looking good!")}
@ -191,13 +189,13 @@
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
<table class="min-w-full">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th scope="col" class="py-2 pl-4 pr-2 w-8">
<button
type="button"
phx-click="select_all"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title={t("Select all")}
>
<.icon name="hero-check-circle" class="h-4 w-4" />
@ -206,51 +204,51 @@
<th scope="col" class="w-1 px-0 py-2"></th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Device")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Site")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Message")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Duration")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Impact")}
</th>
<th
scope="col"
class="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5">
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
<%= for alert <- @alerts do %>
<tr
class={[
"hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/30 transition-colors cursor-pointer",
"hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors cursor-pointer",
MapSet.member?(@selected_alerts, alert.id) &&
"bg-cerulean-50/50 dark:bg-cerulean-900/10",
"bg-blue-50/50 dark:bg-blue-900/10",
rem(Enum.find_index(@alerts, &(&1.id == alert.id)) || 0, 2) == 1 &&
"bg-cool-steel-50/40 dark:bg-cool-steel-800/20"
"bg-gray-50/40 dark:bg-gray-800/20"
]}
phx-click="toggle_alert"
phx-value-id={alert.id}
@ -259,10 +257,9 @@
<td class="py-2 pl-4 pr-2" phx-click="toggle_select" phx-value-id={alert.id}>
<div class={[
"h-4 w-4 rounded border flex items-center justify-center transition-colors",
MapSet.member?(@selected_alerts, alert.id) &&
"bg-cerulean-500 border-cerulean-500",
MapSet.member?(@selected_alerts, alert.id) && "bg-blue-500 border-blue-500",
!MapSet.member?(@selected_alerts, alert.id) &&
"border-cool-steel-300 dark:border-cool-steel-600"
"border-gray-300 dark:border-gray-600"
]}>
<.icon
:if={MapSet.member?(@selected_alerts, alert.id)}
@ -283,9 +280,9 @@
<span class={[
"inline-flex items-center gap-1 text-xs font-medium px-1.5 py-0.5 rounded",
alert.alert_type == "device_down" && is_nil(alert.resolved_at) &&
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/40 dark:text-sweet-salmon-300",
"bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300",
alert.alert_type == "device_down" && alert.resolved_at &&
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400",
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
alert.alert_type == "device_up" &&
"bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300"
]}>
@ -297,33 +294,33 @@
</span>
<.link
navigate={~p"/devices/#{alert.device.id}"}
class="text-sm font-medium text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{alert.device.name}
</.link>
<span class="text-xs font-mono text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{alert.device.ip_address}
</span>
</div>
</td>
<%!-- Site --%>
<td class="px-3 py-2 whitespace-nowrap text-xs text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-3 py-2 whitespace-nowrap text-xs text-gray-600 dark:text-gray-400">
<%= if alert.device.site do %>
<.link
navigate={~p"/sites/#{alert.device.site.id}"}
class="hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="hover:text-blue-600 dark:hover:text-blue-400"
>
{alert.device.site.name}
</.link>
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-600">—</span>
<span class="text-gray-400 dark:text-gray-600">—</span>
<% end %>
</td>
<%!-- Message --%>
<td
class="px-3 py-2 text-xs text-cool-steel-600 dark:text-cool-steel-400 max-w-xs truncate"
class="px-3 py-2 text-xs text-gray-600 dark:text-gray-400 max-w-xs truncate"
title={alert.message}
>
{alert.message || "—"}
@ -334,10 +331,10 @@
<span class={[
"text-xs font-mono",
severity_color(alert) == "red" &&
"text-sweet-salmon-600 dark:text-sweet-salmon-400 font-medium",
"text-red-600 dark:text-red-400 font-medium",
severity_color(alert) == "orange" && "text-orange-600 dark:text-orange-400",
severity_color(alert) not in ["red", "orange"] &&
"text-cool-steel-600 dark:text-cool-steel-400"
"text-gray-600 dark:text-gray-400"
]}>
{duration_text(alert)}
</span>
@ -346,12 +343,12 @@
<%!-- Impact --%>
<td class="px-3 py-2 whitespace-nowrap">
<%= if impact = impact_text(alert, @site_subscribers) do %>
<span class="inline-flex items-center gap-1 text-xs text-wheat-700 dark:text-wheat-400">
<span class="inline-flex items-center gap-1 text-xs text-amber-700 dark:text-amber-400">
<.icon name="hero-users" class="h-3 w-3" />
{impact}
</span>
<% else %>
<span class="text-xs text-cool-steel-300 dark:text-cool-steel-600">—</span>
<span class="text-xs text-gray-300 dark:text-gray-600">—</span>
<% end %>
</td>
@ -359,12 +356,12 @@
<td class="px-3 py-2 whitespace-nowrap text-right">
<div class="flex items-center justify-end gap-1">
<%= if alert.acknowledged_at && is_nil(alert.resolved_at) do %>
<span class="text-xs text-cerulean-600 dark:text-cerulean-400 px-1.5 py-0.5 rounded bg-cerulean-50 dark:bg-cerulean-900/20">
<span class="text-xs text-blue-600 dark:text-blue-400 px-1.5 py-0.5 rounded bg-blue-50 dark:bg-blue-900/20">
Ack'd
</span>
<% end %>
<%= if alert.resolved_at do %>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400 px-1.5 py-0.5 rounded bg-cool-steel-100 dark:bg-cool-steel-800">
<span class="text-xs text-gray-500 dark:text-gray-400 px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800">
{t("Resolved")}
</span>
<% end %>
@ -373,7 +370,7 @@
type="button"
phx-click="acknowledge"
phx-value-id={alert.id}
class="text-xs px-2 py-1 rounded bg-cerulean-100 text-cerulean-700 hover:bg-cerulean-200 dark:bg-cerulean-900/30 dark:text-cerulean-300 dark:hover:bg-cerulean-900/50 transition-colors"
class="text-xs px-2 py-1 rounded bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/50 transition-colors"
>
Ack
</button>
@ -384,7 +381,7 @@
phx-click="resolve"
phx-value-id={alert.id}
data-confirm={t("Resolve this alert?")}
class="text-xs px-2 py-1 rounded bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700 transition-colors"
class="text-xs px-2 py-1 rounded bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors"
>
Resolve
</button>
@ -395,22 +392,22 @@
<%!-- Expanded alert detail --%>
<%= if @expanded_alert == alert.id do %>
<tr class="bg-cool-steel-50 dark:bg-cool-steel-800/40">
<tr class="bg-gray-50 dark:bg-gray-800/40">
<td colspan="8" class="px-4 py-3">
<div class="flex gap-8 text-xs">
<%!-- Timeline --%>
<div class="flex-1">
<h4 class="font-semibold text-cool-steel-700 dark:text-cool-steel-300 mb-2 uppercase tracking-wider">
<h4 class="font-semibold text-gray-700 dark:text-gray-300 mb-2 uppercase tracking-wider">
{t("Timeline")}
</h4>
<div class="space-y-1.5">
<div class="flex items-center gap-2">
<span class="inline-block h-2 w-2 rounded-full bg-sweet-salmon-500 flex-shrink-0">
<span class="inline-block h-2 w-2 rounded-full bg-red-500 flex-shrink-0">
</span>
<span class="text-cool-steel-500 dark:text-cool-steel-400 w-20 flex-shrink-0">
<span class="text-gray-500 dark:text-gray-400 w-20 flex-shrink-0">
{t("Triggered")}
</span>
<span class="font-mono text-cool-steel-900 dark:text-white">
<span class="font-mono text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_iso8601(
alert.triggered_at,
@timezone
@ -419,19 +416,19 @@
</div>
<%= if alert.acknowledged_at do %>
<div class="flex items-center gap-2">
<span class="inline-block h-2 w-2 rounded-full bg-cerulean-500 flex-shrink-0">
<span class="inline-block h-2 w-2 rounded-full bg-blue-500 flex-shrink-0">
</span>
<span class="text-cool-steel-500 dark:text-cool-steel-400 w-20 flex-shrink-0">
<span class="text-gray-500 dark:text-gray-400 w-20 flex-shrink-0">
{t("Ack'd")}
</span>
<span class="font-mono text-cool-steel-900 dark:text-white">
<span class="font-mono text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_iso8601(
alert.acknowledged_at,
@timezone
)}
</span>
<%= if alert.acknowledged_by do %>
<span class="text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-500 dark:text-gray-400">
by {alert.acknowledged_by.email}
</span>
<% end %>
@ -441,10 +438,10 @@
<div class="flex items-center gap-2">
<span class="inline-block h-2 w-2 rounded-full bg-green-500 flex-shrink-0">
</span>
<span class="text-cool-steel-500 dark:text-cool-steel-400 w-20 flex-shrink-0">
<span class="text-gray-500 dark:text-gray-400 w-20 flex-shrink-0">
{t("Resolved")}
</span>
<span class="font-mono text-cool-steel-900 dark:text-white">
<span class="font-mono text-gray-900 dark:text-white">
{ToweropsWeb.TimeHelpers.format_iso8601(
alert.resolved_at,
@timezone
@ -458,13 +455,13 @@
<%!-- Impact details --%>
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
<div>
<h4 class="font-semibold text-cool-steel-700 dark:text-cool-steel-300 mb-2 uppercase tracking-wider">
<h4 class="font-semibold text-gray-700 dark:text-gray-300 mb-2 uppercase tracking-wider">
{t("Impact")}
</h4>
<div class="space-y-1">
<div class="flex items-center gap-2">
<.icon name="hero-users" class="h-3.5 w-3.5 text-wheat-500" />
<span class="text-cool-steel-900 dark:text-white font-medium">
<.icon name="hero-users" class="h-3.5 w-3.5 text-amber-500" />
<span class="text-gray-900 dark:text-white font-medium">
{format_number(alert.gaiia_impact["total_subscribers"])} subscribers affected
</span>
</div>
@ -473,9 +470,9 @@
<div class="flex items-center gap-2">
<.icon
name="hero-currency-dollar"
class="h-3.5 w-3.5 text-wheat-500"
class="h-3.5 w-3.5 text-amber-500"
/>
<span class="text-cool-steel-900 dark:text-white font-medium">
<span class="text-gray-900 dark:text-white font-medium">
${alert.gaiia_impact["total_mrr"]}/mo at risk
</span>
</div>
@ -487,12 +484,10 @@
<%!-- Message --%>
<%= if alert.message do %>
<div class="flex-1">
<h4 class="font-semibold text-cool-steel-700 dark:text-cool-steel-300 mb-2 uppercase tracking-wider">
<h4 class="font-semibold text-gray-700 dark:text-gray-300 mb-2 uppercase tracking-wider">
{t("Details")}
</h4>
<p class="text-cool-steel-600 dark:text-cool-steel-400">
{alert.message}
</p>
<p class="text-gray-600 dark:text-gray-400">{alert.message}</p>
</div>
<% end %>
</div>

View file

@ -9,12 +9,12 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
def render(assigns) do
~H"""
<div class="relative z-50">
<div class="fixed inset-0 bg-cool-steel-500 dark:bg-cool-steel-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
</div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-cool-steel-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<.form
for={@form}
id="check-form"
@ -22,16 +22,16 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
phx-change="validate"
phx-submit="save"
>
<div class="bg-white dark:bg-cool-steel-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="flex items-start justify-between mb-4">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{if @action == :edit, do: "Edit Service Check", else: "Add Service Check"}
</h3>
<button
type="button"
phx-click="close"
phx-target={@myself}
class="text-cool-steel-400 hover:text-cool-steel-500 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
@ -40,7 +40,7 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
<div class="space-y-4">
<!-- Check Type Selection -->
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Check Type
</label>
<.input
@ -79,7 +79,7 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
<% "ssl" -> %>
{render_ssl_fields(assigns)}
<% _ -> %>
<div class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="text-sm text-gray-500 dark:text-gray-400">
Select a check type to configure
</div>
<% end %>
@ -100,7 +100,7 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
</div>
</div>
<div class="bg-cool-steel-50 dark:bg-cool-steel-900/50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-3">
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-3">
<.button
type="submit"
phx-disable-with={if @action == :edit, do: "Saving...", else: "Creating..."}
@ -111,7 +111,7 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
type="button"
phx-click="close"
phx-target={@myself}
class="text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
Cancel
</button>
@ -155,8 +155,8 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
<.input field={@form[:follow_redirects]} type="checkbox" label="Follow Redirects" />
</div>
<details class="border border-cool-steel-200 dark:border-cool-steel-700 rounded-lg p-3">
<summary class="cursor-pointer text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300">
<details class="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
<summary class="cursor-pointer text-sm font-medium text-gray-700 dark:text-gray-300">
Advanced Options
</summary>
<div class="mt-3 space-y-3">
@ -264,7 +264,7 @@ defmodule ToweropsWeb.CheckLive.FormComponent do
label="Warning Days"
placeholder="30"
/>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Alert when certificate expires within this many days
</p>
</div>

View file

@ -80,12 +80,9 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do
>
</div>
<div class="flex min-h-full items-start justify-center p-4 pt-[15vh]">
<div class="relative w-full max-w-lg transform rounded-xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-cool-steel-800 dark:ring-white/10">
<div class="flex items-center gap-3 border-b border-cool-steel-200 px-4 dark:border-white/10">
<.icon
name="hero-magnifying-glass"
class="h-5 w-5 text-cool-steel-400 dark:text-cool-steel-500"
/>
<div class="relative w-full max-w-lg transform rounded-xl bg-white shadow-2xl ring-1 ring-black/5 dark:bg-gray-800 dark:ring-white/10">
<div class="flex items-center gap-3 border-b border-gray-200 px-4 dark:border-white/10">
<.icon name="hero-magnifying-glass" class="h-5 w-5 text-gray-400 dark:text-gray-500" />
<input
type="text"
id="global-search-input"
@ -96,23 +93,23 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do
phx-debounce="200"
phx-target={@myself}
autocomplete="off"
class="flex-1 border-0 bg-transparent py-3.5 text-sm text-cool-steel-900 placeholder:text-cool-steel-400 focus:ring-0 dark:text-white dark:placeholder:text-cool-steel-500"
class="flex-1 border-0 bg-transparent py-3.5 text-sm text-gray-900 placeholder:text-gray-400 focus:ring-0 dark:text-white dark:placeholder:text-gray-500"
/>
<kbd class="hidden sm:inline-flex items-center rounded border border-cool-steel-300 px-1.5 py-0.5 text-xs text-cool-steel-400 dark:border-cool-steel-600 dark:text-cool-steel-500">
<kbd class="hidden sm:inline-flex items-center rounded border border-gray-300 px-1.5 py-0.5 text-xs text-gray-400 dark:border-gray-600 dark:text-gray-500">
esc
</kbd>
</div>
<div class="max-h-80 overflow-y-auto px-2 py-2">
<%= if @query != "" and map_size(@results) == 0 do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
No results found.
</p>
<% end %>
<%= for {category, items} <- @results do %>
<div class="mb-2">
<p class="px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<p class="px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{category_label(category)}
</p>
<%= for {item, idx} <- Enum.with_index(items) do %>
@ -122,10 +119,9 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do
class={[
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm cursor-pointer",
if(global_idx == @selected_index,
do:
"bg-cerulean-50 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-300",
do: "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300",
else:
"text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-white/5"
"text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5"
)
]}
>
@ -145,13 +141,13 @@ defmodule ToweropsWeb.Live.Components.GlobalSearchComponent do
<% end %>
<%= if @query == "" do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
Start typing to search...
</p>
<% end %>
</div>
<div class="flex items-center justify-between border-t border-cool-steel-200 px-4 py-2 text-xs text-cool-steel-400 dark:border-white/10 dark:text-cool-steel-500">
<div class="flex items-center justify-between border-t border-gray-200 px-4 py-2 text-xs text-gray-400 dark:border-white/10 dark:text-gray-500">
<div class="flex gap-2">
<span><kbd class="font-semibold"></kbd> navigate</span>
<span><kbd class="font-semibold"></kbd> open</span>

View file

@ -287,7 +287,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def change_size_class(size) when is_integer(size) and size > 20,
do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def change_size_class(_), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def change_size_class(_), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
end

View file

@ -18,9 +18,9 @@
)}
</:subtitle>
<:actions>
<span class="inline-flex items-center px-3 py-1.5 rounded-md border border-cerulean-200 dark:border-cerulean-900/50 bg-cerulean-50 dark:bg-cerulean-900/20 text-sm">
<span class="text-cerulean-700 dark:text-cerulean-300 font-medium mr-2">EIRP:</span>
<span class="text-cerulean-900 dark:text-cerulean-100 font-mono">
<span class="inline-flex items-center px-3 py-1.5 rounded-md border border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20 text-sm">
<span class="text-blue-700 dark:text-blue-300 font-medium mr-2">EIRP:</span>
<span class="text-blue-900 dark:text-blue-100 font-mono">
{Float.round(@computed_eirp, 1)} dBm
</span>
</span>
@ -37,7 +37,7 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<%!-- Identity --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Identity")}
</h3>
@ -68,7 +68,7 @@
/>
<p
:if={@form[:site_id].value not in [nil, ""] and @devices == []}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400"
class="text-xs text-gray-500 dark:text-gray-400"
>
{t("This site has no devices yet — coverage will not be attached to a specific device.")}
</p>
@ -86,20 +86,20 @@
<%!-- Radio location: draggable marker on a Leaflet map. --%>
<div class="space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Radio location")}
</h3>
<button
:if={radio_overridden?(@form)}
type="button"
phx-click="use_site_location"
class="text-xs text-cerulean-600 hover:underline dark:text-cerulean-400"
class="text-xs text-blue-600 hover:underline dark:text-blue-400"
>
{t("Use site location")}
</button>
</div>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs text-gray-500 dark:text-gray-400">
{t("Drag the marker to where the radio is mounted. Defaults to the site's coordinates.")}
</p>
@ -107,7 +107,7 @@
id="coverage-location-picker"
phx-hook="CoverageLocationPicker"
phx-update="ignore"
class="h-72 w-full rounded border border-cool-steel-200 dark:border-white/10 bg-cool-steel-100 dark:bg-cool-steel-800"
class="h-72 w-full rounded border border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-gray-800"
data-marker-lat={radio_marker_lat(@form, @sites)}
data-marker-lon={radio_marker_lon(@form, @sites)}
>
@ -115,13 +115,10 @@
<div
:if={radio_marker_lat(@form, @sites)}
class="text-xs font-mono text-cool-steel-600 dark:text-cool-steel-400"
class="text-xs font-mono text-gray-600 dark:text-gray-400"
>
{radio_marker_lat(@form, @sites)}, {radio_marker_lon(@form, @sites)}
<span
:if={radio_overridden?(@form)}
class="ml-2 text-cerulean-600 dark:text-cerulean-400"
>
<span :if={radio_overridden?(@form)} class="ml-2 text-blue-600 dark:text-blue-400">
({t("override")})
</span>
</div>
@ -132,7 +129,7 @@
<%!-- Radio --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Radio")}
</h3>
@ -169,7 +166,7 @@
<%!-- Mounting --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Mounting")}
</h3>
@ -205,14 +202,14 @@
<%!-- Foliage --%>
<div class="space-y-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Environment")}
</h3>
<div>
<label
for="coverage_foliage_tuning"
class="block text-sm font-medium text-cool-steel-900 dark:text-cool-steel-100 mb-1"
class="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-1"
>
{t("Foliage tuning")}: {Phoenix.HTML.Form.input_value(@form, :foliage_tuning) || 0}
</label>
@ -226,7 +223,7 @@
step="1"
class="w-full"
/>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-1">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{t("Higher values reduce predicted signal in wooded areas.")}
</p>
</div>

View file

@ -67,17 +67,15 @@ defmodule ToweropsWeb.CoverageLive.Index do
end
@doc false
def status_badge_class("draft"),
do: "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"
def status_badge_class("draft"), do: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
def status_badge_class("queued"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300"
def status_badge_class("queued"), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
def status_badge_class("computing"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-300"
def status_badge_class("computing"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
def status_badge_class("ready"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"
def status_badge_class("failed"),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-300"
def status_badge_class("failed"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"
def status_badge_class(_), do: "bg-cool-steel-100 text-cool-steel-700"
def status_badge_class(_), do: "bg-gray-100 text-gray-700"
end

View file

@ -23,16 +23,13 @@
<div class="flex items-center justify-center py-16">
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
<div class="items-center text-center">
<div class="rounded-full bg-cerulean-100 dark:bg-cerulean-900/40 p-4 mb-2">
<.icon
name="hero-signal"
class="h-12 w-12 text-cerulean-500 dark:text-cerulean-400"
/>
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Add a site first")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{t(
"Coverage predictions are computed from a site's location. Add a site, then return here to plan its coverage."
)}
@ -49,16 +46,13 @@
<div class="flex items-center justify-center py-16">
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
<div class="items-center text-center">
<div class="rounded-full bg-cerulean-100 dark:bg-cerulean-900/40 p-4 mb-2">
<.icon
name="hero-signal"
class="h-12 w-12 text-cerulean-500 dark:text-cerulean-400"
/>
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("No coverages yet")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{t("Create your first coverage to predict where a tower's antenna reaches.")}
</p>
<div class="mt-4 flex gap-2">
@ -70,65 +64,65 @@
</div>
</div>
<% true -> %>
<div class="mt-6 overflow-x-auto rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="mt-6 overflow-x-auto rounded-lg border border-gray-200 dark:border-white/10">
<table
id="coverages-table"
class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10"
class="min-w-full divide-y divide-gray-200 dark:divide-white/10"
>
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/80">
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Name")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Site")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Antenna")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Freq (GHz)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("TX (dBm)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Range (mi)")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Status")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-cool-steel-200 dark:bg-cool-steel-900 dark:divide-white/10">
<tbody class="bg-white divide-y divide-gray-200 dark:bg-gray-900 dark:divide-white/10">
<%= for coverage <- @coverages do %>
<tr id={"coverage-#{coverage.id}"}>
<td class="px-4 py-2 text-sm">
<.link
navigate={~p"/coverage/#{coverage.id}"}
class="font-medium text-cerulean-600 hover:underline dark:text-cerulean-400"
class="font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{coverage.name}
</.link>
</td>
<td class="px-4 py-2 text-sm text-cool-steel-700 dark:text-cool-steel-300">
<td class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">
<.link navigate={~p"/sites/#{coverage.site_id}"} class="hover:underline">
{coverage.site && coverage.site.name}
</.link>
</td>
<td class="px-4 py-2 text-sm text-cool-steel-700 dark:text-cool-steel-300">
<td class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300">
<span class="font-mono text-xs">{coverage.antenna_slug}</span>
</td>
<td class="px-4 py-2 text-sm text-right text-cool-steel-700 dark:text-cool-steel-300 font-mono">
<td class="px-4 py-2 text-sm text-right text-gray-700 dark:text-gray-300 font-mono">
{Float.round(coverage.frequency_mhz / 1000.0, 3)}
</td>
<td class="px-4 py-2 text-sm text-right text-cool-steel-700 dark:text-cool-steel-300">
<td class="px-4 py-2 text-sm text-right text-gray-700 dark:text-gray-300">
{coverage.tx_power_dbm}
</td>
<td class="px-4 py-2 text-sm text-right text-cool-steel-700 dark:text-cool-steel-300 font-mono">
<td class="px-4 py-2 text-sm text-right text-gray-700 dark:text-gray-300 font-mono">
{Float.round(coverage.radius_m / 1609.344, 2)}
</td>
<td class="px-4 py-2 text-sm">
@ -143,7 +137,7 @@
<div class="flex justify-end gap-2">
<.link
navigate={~p"/coverage/#{coverage.id}/edit"}
class="text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<.icon name="hero-pencil-square" class="h-4 w-4" />
</.link>
@ -151,7 +145,7 @@
type="button"
phx-click="recompute"
phx-value-id={coverage.id}
class="text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
title={t("Recompute")}
>
<.icon name="hero-arrow-path" class="h-4 w-4" />
@ -161,7 +155,7 @@
phx-click="delete"
phx-value-id={coverage.id}
data-confirm={t("Delete this coverage?")}
class="text-sweet-salmon-600 hover:text-sweet-salmon-800 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
title={t("Delete")}
>
<.icon name="hero-trash" class="h-4 w-4" />

View file

@ -26,13 +26,13 @@
<div class="mt-6 flex items-center justify-center py-16">
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
<div class="items-center text-center">
<div class="rounded-full bg-cerulean-100 dark:bg-cerulean-900/40 p-4 mb-2">
<.icon name="hero-signal" class="h-12 w-12 text-cerulean-500 dark:text-cerulean-400" />
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
<.icon name="hero-signal" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("No ready coverages")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
{t(
"Coverage maps appear here once the background compute finishes. Create or wait for a coverage to finish."
)}
@ -50,7 +50,7 @@
id="coverage-multi-map"
phx-hook="MultiCoverageMap"
phx-update="ignore"
class="relative mt-6 h-[78vh] w-full rounded-lg border border-cool-steel-200 dark:border-white/10 bg-cool-steel-100 dark:bg-cool-steel-800 overflow-hidden"
class="relative mt-6 h-[78vh] w-full rounded-lg border border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-gray-800 overflow-hidden"
data-coverages={Jason.encode!(coverages_payload(@coverages))}
data-enabled={Jason.encode!(@enabled_ids)}
>
@ -61,15 +61,15 @@
class="pointer-events-none fixed top-32 left-1/2 -translate-x-[640px] z-[1100]"
id="coverage-height-control"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg p-3 flex flex-col items-center gap-2 w-24">
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg p-3 flex flex-col items-center gap-2 w-24">
<%!-- LOS vs NLOS mode toggle. The label and reference frame
of the height slider switch with the mode. --%>
<div class="flex w-full text-[10px] font-medium border border-cool-steel-200 dark:border-white/10 rounded overflow-hidden">
<div class="flex w-full text-[10px] font-medium border border-gray-200 dark:border-white/10 rounded overflow-hidden">
<button
type="button"
id="coverage-mode-los"
data-mode="los"
class="flex-1 px-1 py-1 text-cool-steel-900 dark:text-white bg-cerulean-100 dark:bg-cerulean-900/40"
class="flex-1 px-1 py-1 text-gray-900 dark:text-white bg-blue-100 dark:bg-blue-900/40"
>
LOS
</button>
@ -77,14 +77,14 @@
type="button"
id="coverage-mode-nlos"
data-mode="nlos"
class="flex-1 px-1 py-1 text-cool-steel-700 dark:text-cool-steel-300 hover:bg-cool-steel-100 dark:hover:bg-cool-steel-800"
class="flex-1 px-1 py-1 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
>
NLOS
</button>
</div>
<span
id="coverage-mode-label"
class="text-[10px] font-medium text-cool-steel-700 dark:text-cool-steel-300 text-center leading-tight"
class="text-[10px] font-medium text-gray-700 dark:text-gray-300 text-center leading-tight"
>
{t("Height above clutter")}
</span>
@ -107,21 +107,21 @@
class="pointer-events-none fixed top-32 right-6 z-[1100] w-72 max-h-[60vh] overflow-y-auto"
id="coverage-tower-list"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">{t("Towers")}</h3>
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Towers")}</h3>
<div class="flex gap-1.5">
<button
type="button"
phx-click="show_all"
class="text-xs px-1.5 py-0.5 rounded hover:bg-cool-steel-100 dark:hover:bg-cool-steel-800 text-cool-steel-700 dark:text-cool-steel-300"
class="text-xs px-1.5 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"
>
{t("Show All")}
</button>
<button
type="button"
phx-click="hide_all"
class="text-xs px-1.5 py-0.5 rounded hover:bg-cool-steel-100 dark:hover:bg-cool-steel-800 text-cool-steel-700 dark:text-cool-steel-300"
class="text-xs px-1.5 py-0.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"
>
{t("Hide All")}
</button>
@ -135,10 +135,10 @@
phx-click="toggle_coverage"
phx-value-id={c.id}
class={[
"w-full text-left flex items-center gap-2 px-3 py-1.5 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800",
"w-full text-left flex items-center gap-2 px-3 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-800",
if(c.id in @enabled_ids,
do: "text-cool-steel-900 dark:text-white font-medium",
else: "text-cool-steel-400 dark:text-cool-steel-500"
do: "text-gray-900 dark:text-white font-medium",
else: "text-gray-400 dark:text-gray-500"
)
]}
>
@ -146,12 +146,12 @@
"inline-block h-2.5 w-2.5 rounded-full",
if(c.id in @enabled_ids,
do: "bg-green-500",
else: "bg-cool-steel-300 dark:bg-cool-steel-600"
else: "bg-gray-300 dark:bg-gray-600"
)
]}>
</span>
<span class="text-sm truncate flex-1">{c.name}</span>
<span class="text-[10px] text-cool-steel-500 dark:text-cool-steel-400 truncate">
<span class="text-[10px] text-gray-500 dark:text-gray-400 truncate">
{c.site && c.site.name}
</span>
</button>
@ -163,11 +163,11 @@
<%!-- Opacity slider + legend (bottom-center, overlay above map) --%>
<div class="pointer-events-none fixed bottom-6 left-1/2 -translate-x-1/2 z-[1100] w-[640px] max-w-[90vw]">
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg p-3 space-y-2">
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg p-3 space-y-2">
<div class="flex items-center gap-3">
<label
for="coverage-opacity-slider"
class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 w-16"
class="text-xs font-medium text-gray-700 dark:text-gray-300 w-16"
>
{t("Opacity")}
</label>
@ -185,7 +185,7 @@
<div class="flex items-center gap-3">
<label
for="coverage-rssi-slider"
class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 w-16"
class="text-xs font-medium text-gray-700 dark:text-gray-300 w-16"
>
{t("Min RSSI")}
</label>
@ -200,7 +200,7 @@
/>
<span id="coverage-rssi-label" class="text-xs font-mono w-16 text-right">-100 dBm</span>
</div>
<div class="flex items-center gap-1 text-[10px] font-mono text-cool-steel-700 dark:text-cool-steel-300">
<div class="flex items-center gap-1 text-[10px] font-mono text-gray-700 dark:text-gray-300">
<span class="w-3 h-3 rounded inline-block" style="background:rgb(0,200,0)"></span>
<span>≥-50</span>
<span class="w-3 h-3 rounded inline-block ml-2" style="background:rgb(200,230,0)">
@ -224,10 +224,10 @@
class="pointer-events-none fixed top-32 left-6 z-[1100] w-[420px] max-w-[90vw] max-h-[60vh] overflow-y-auto"
id="coverage-probe-panel"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-cool-steel-200 dark:border-white/10">
<div class="flex items-center gap-2 text-xs font-mono text-cool-steel-700 dark:text-cool-steel-300">
<.icon name="hero-map-pin" class="h-4 w-4 text-cerulean-500" />
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
<div class="flex items-center gap-2 text-xs font-mono text-gray-700 dark:text-gray-300">
<.icon name="hero-map-pin" class="h-4 w-4 text-blue-500" />
<span>
{:erlang.float_to_binary(@probe.lat * 1.0, decimals: 5)}, {:erlang.float_to_binary(
@probe.lon * 1.0,
@ -238,13 +238,13 @@
<button
type="button"
phx-click="close_probe"
class="text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400"
class="text-gray-500 hover:text-gray-700 dark:text-gray-400"
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
</div>
<div class="px-3 py-2 border-b border-cool-steel-200 dark:border-white/10 flex items-center gap-3">
<span class="text-xs text-cool-steel-700 dark:text-cool-steel-300">{t("Units:")}</span>
<div class="px-3 py-2 border-b border-gray-200 dark:border-white/10 flex items-center gap-3">
<span class="text-xs text-gray-700 dark:text-gray-300">{t("Units:")}</span>
<label class="flex items-center gap-1 text-xs cursor-pointer">
<input
type="radio"
@ -265,45 +265,45 @@
</label>
</div>
<table class="min-w-full text-xs">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-3 py-1.5 text-left font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-left font-medium text-gray-700 dark:text-gray-300">
{t("Tower")}
</th>
<th class="px-3 py-1.5 text-right font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-right font-medium text-gray-700 dark:text-gray-300">
{t("Distance")}
</th>
<th class="px-3 py-1.5 text-right font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-right font-medium text-gray-700 dark:text-gray-300">
RSSI
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-white/10">
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for row <- @probe.rows do %>
<tr>
<td class="px-3 py-1.5 font-medium text-cool-steel-900 dark:text-cool-steel-100 truncate">
<td class="px-3 py-1.5 font-medium text-gray-900 dark:text-gray-100 truncate">
{row.coverage.name}
</td>
<td class="px-3 py-1.5 text-right font-mono text-cool-steel-700 dark:text-cool-steel-300">
<td class="px-3 py-1.5 text-right font-mono text-gray-700 dark:text-gray-300">
{fmt_distance(row.distance_m, @probe_units)}
</td>
<td class={[
"px-3 py-1.5 text-right font-mono",
cond do
row.rssi == :no_coverage ->
"text-cool-steel-400"
"text-gray-400"
is_number(row.rssi) and row.rssi >= -75 ->
"text-green-600 dark:text-green-400 font-bold"
is_number(row.rssi) and row.rssi >= -85 ->
"text-wheat-600 dark:text-wheat-400"
"text-yellow-600 dark:text-yellow-400"
is_number(row.rssi) ->
"text-sweet-salmon-600 dark:text-sweet-salmon-400"
"text-red-600 dark:text-red-400"
true ->
"text-cool-steel-400"
"text-gray-400"
end
]}>
{fmt_rssi(row.rssi)}

View file

@ -120,19 +120,17 @@ defmodule ToweropsWeb.CoverageLive.Show do
defdelegate fmt_rssi(v), to: CoverageMapView
@doc false
def status_badge_class("draft"),
do: "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"
def status_badge_class("draft"), do: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
def status_badge_class("queued"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300"
def status_badge_class("queued"), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
def status_badge_class("computing"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-300"
def status_badge_class("computing"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
def status_badge_class("ready"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"
def status_badge_class("failed"),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-300"
def status_badge_class("failed"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"
def status_badge_class(_), do: "bg-cool-steel-100 text-cool-steel-700"
def status_badge_class(_), do: "bg-gray-100 text-gray-700"
@doc false
def coverage_lat(coverage) do
@ -168,8 +166,8 @@ defmodule ToweropsWeb.CoverageLive.Show do
def param_row(assigns) do
~H"""
<div class="flex justify-between gap-3">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">{@label}</dt>
<dd class="text-cool-steel-900 dark:text-cool-steel-100 font-medium text-right">{@value}</dd>
<dt class="text-gray-600 dark:text-gray-400">{@label}</dt>
<dd class="text-gray-900 dark:text-gray-100 font-medium text-right">{@value}</dd>
</div>
"""
end

View file

@ -12,10 +12,10 @@
%{label: t("Coverage"), navigate: ~p"/coverage"},
%{label: @coverage.name}
]} />
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white truncate">
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">
{@coverage.name}
</h1>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
<.icon name="hero-map-pin" class="h-4 w-4 inline" />
<.link navigate={~p"/sites/#{@coverage.site_id}"} class="hover:underline">
{@coverage.site && @coverage.site.name}
@ -53,25 +53,25 @@
<%!-- Status banners (queued / computing / failed). --%>
<%= if @coverage.status == "computing" do %>
<div class="mb-4 rounded-lg border border-wheat-200 bg-wheat-50 dark:border-wheat-900/50 dark:bg-wheat-900/20 p-4">
<div class="mb-4 rounded-lg border border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20 p-4">
<div class="flex items-start gap-3">
<.icon
name="hero-arrow-path"
class="h-5 w-5 text-wheat-700 dark:text-wheat-400 animate-spin shrink-0"
class="h-5 w-5 text-yellow-700 dark:text-yellow-400 animate-spin shrink-0"
/>
<div class="flex-1">
<p class="text-sm font-medium text-wheat-900 dark:text-wheat-200">
<p class="text-sm font-medium text-yellow-900 dark:text-yellow-200">
{t("Computing coverage…")}
</p>
<div class="mt-1 flex items-center gap-3">
<div class="flex-1 h-2 bg-wheat-200 dark:bg-wheat-900 rounded-full overflow-hidden">
<div class="flex-1 h-2 bg-yellow-200 dark:bg-yellow-900 rounded-full overflow-hidden">
<div
class="h-full bg-wheat-600 dark:bg-wheat-500 transition-all"
class="h-full bg-yellow-600 dark:bg-yellow-500 transition-all"
style={"width: #{@coverage.progress_pct}%"}
>
</div>
</div>
<span class="text-sm text-wheat-800 dark:text-wheat-300 font-mono shrink-0">
<span class="text-sm text-yellow-800 dark:text-yellow-300 font-mono shrink-0">
{@coverage.progress_pct}%
</span>
</div>
@ -81,20 +81,17 @@
<% end %>
<%= if @coverage.status == "failed" do %>
<div class="mb-4 rounded-lg border border-sweet-salmon-200 bg-sweet-salmon-50 dark:border-sweet-salmon-900/50 dark:bg-sweet-salmon-900/20 p-4">
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-900/20 p-4">
<div class="flex items-start gap-3">
<.icon
name="hero-exclamation-triangle"
class="h-5 w-5 text-sweet-salmon-700 dark:text-sweet-salmon-400 flex-shrink-0 mt-0.5"
class="h-5 w-5 text-red-700 dark:text-red-400 flex-shrink-0 mt-0.5"
/>
<div>
<p class="text-sm font-medium text-sweet-salmon-900 dark:text-sweet-salmon-200">
<p class="text-sm font-medium text-red-900 dark:text-red-200">
{t("Compute failed")}
</p>
<p
:if={@coverage.error_message}
class="mt-1 text-sm text-sweet-salmon-800 dark:text-sweet-salmon-300"
>
<p :if={@coverage.error_message} class="mt-1 text-sm text-red-800 dark:text-red-300">
{@coverage.error_message}
</p>
</div>
@ -103,8 +100,8 @@
<% end %>
<%= if @coverage.status == "queued" do %>
<div class="mb-4 rounded-lg border border-cerulean-200 bg-cerulean-50 dark:border-cerulean-900/50 dark:bg-cerulean-900/20 p-4">
<p class="text-sm text-cerulean-900 dark:text-cerulean-200">
<div class="mb-4 rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-900/50 dark:bg-blue-900/20 p-4">
<p class="text-sm text-blue-900 dark:text-blue-200">
<.icon name="hero-clock" class="h-4 w-4 inline" />
{t("Coverage is queued. Compute will start shortly.")}
</p>
@ -118,7 +115,7 @@
id="coverage-multi-map"
phx-hook="MultiCoverageMap"
phx-update="ignore"
class="relative h-[78vh] w-full rounded-lg border border-cool-steel-200 dark:border-white/10 bg-cool-steel-100 dark:bg-cool-steel-800 overflow-hidden"
class="relative h-[78vh] w-full rounded-lg border border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-gray-800 overflow-hidden"
data-coverages={Jason.encode!(coverages_payload([@coverage]))}
data-enabled={Jason.encode!([@coverage.id])}
>
@ -129,13 +126,13 @@
class="pointer-events-none fixed top-32 left-6 z-[1100]"
id="coverage-height-control"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg p-3 flex flex-col items-center gap-2 w-24">
<div class="flex w-full text-[10px] font-medium border border-cool-steel-200 dark:border-white/10 rounded overflow-hidden">
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg p-3 flex flex-col items-center gap-2 w-24">
<div class="flex w-full text-[10px] font-medium border border-gray-200 dark:border-white/10 rounded overflow-hidden">
<button
type="button"
id="coverage-mode-los"
data-mode="los"
class="flex-1 px-1 py-1 text-cool-steel-900 dark:text-white bg-cerulean-100 dark:bg-cerulean-900/40"
class="flex-1 px-1 py-1 text-gray-900 dark:text-white bg-blue-100 dark:bg-blue-900/40"
>
LOS
</button>
@ -143,14 +140,14 @@
type="button"
id="coverage-mode-nlos"
data-mode="nlos"
class="flex-1 px-1 py-1 text-cool-steel-700 dark:text-cool-steel-300 hover:bg-cool-steel-100 dark:hover:bg-cool-steel-800"
class="flex-1 px-1 py-1 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800"
>
NLOS
</button>
</div>
<span
id="coverage-mode-label"
class="text-[10px] font-medium text-cool-steel-700 dark:text-cool-steel-300 text-center leading-tight"
class="text-[10px] font-medium text-gray-700 dark:text-gray-300 text-center leading-tight"
>
{t("Height above clutter")}
</span>
@ -174,15 +171,15 @@
class="pointer-events-none fixed top-32 right-6 z-[1100] w-80 max-h-[70vh] overflow-y-auto"
id="coverage-params-panel"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Parameters")}
</h3>
<button
type="button"
phx-click="toggle_params"
class="text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400"
class="text-gray-500 hover:text-gray-700 dark:text-gray-400"
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
@ -236,7 +233,7 @@
type="button"
phx-click="delete"
data-confirm={t("Delete this coverage?")}
class="mt-3 w-full text-sm text-sweet-salmon-600 hover:text-sweet-salmon-800 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="mt-3 w-full text-sm text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
<.icon name="hero-trash" class="h-4 w-4 inline" /> {t("Delete coverage")}
</button>
@ -247,11 +244,11 @@
<%!-- Opacity + RSSI filter + legend (bottom-center). --%>
<div class="pointer-events-none fixed bottom-6 left-1/2 -translate-x-1/2 z-[1100] w-[640px] max-w-[90vw]">
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg p-3 space-y-2">
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg p-3 space-y-2">
<div class="flex items-center gap-3">
<label
for="coverage-opacity-slider"
class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 w-16"
class="text-xs font-medium text-gray-700 dark:text-gray-300 w-16"
>
{t("Opacity")}
</label>
@ -268,7 +265,7 @@
<div class="flex items-center gap-3">
<label
for="coverage-rssi-slider"
class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 w-16"
class="text-xs font-medium text-gray-700 dark:text-gray-300 w-16"
>
{t("Min RSSI")}
</label>
@ -285,7 +282,7 @@
-100 dBm
</span>
</div>
<div class="flex items-center gap-1 text-[10px] font-mono text-cool-steel-700 dark:text-cool-steel-300">
<div class="flex items-center gap-1 text-[10px] font-mono text-gray-700 dark:text-gray-300">
<span class="w-3 h-3 rounded inline-block" style="background:rgb(0,200,0)"></span>
<span>≥-50</span>
<span class="w-3 h-3 rounded inline-block ml-2" style="background:rgb(200,230,0)">
@ -310,10 +307,10 @@
class="pointer-events-none fixed top-32 left-32 z-[1100] w-[360px] max-w-[90vw] max-h-[60vh] overflow-y-auto"
id="coverage-probe-panel"
>
<div class="pointer-events-auto rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white/95 dark:bg-cool-steel-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-cool-steel-200 dark:border-white/10">
<div class="flex items-center gap-2 text-xs font-mono text-cool-steel-700 dark:text-cool-steel-300">
<.icon name="hero-map-pin" class="h-4 w-4 text-cerulean-500" />
<div class="pointer-events-auto rounded-lg border border-gray-200 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur shadow-lg">
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 dark:border-white/10">
<div class="flex items-center gap-2 text-xs font-mono text-gray-700 dark:text-gray-300">
<.icon name="hero-map-pin" class="h-4 w-4 text-blue-500" />
<span>
{:erlang.float_to_binary(@probe.lat * 1.0, decimals: 5)}, {:erlang.float_to_binary(
@probe.lon * 1.0,
@ -324,15 +321,13 @@
<button
type="button"
phx-click="close_probe"
class="text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400"
class="text-gray-500 hover:text-gray-700 dark:text-gray-400"
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
</div>
<div class="px-3 py-2 border-b border-cool-steel-200 dark:border-white/10 flex items-center gap-3">
<span class="text-xs text-cool-steel-700 dark:text-cool-steel-300">
{t("Units:")}
</span>
<div class="px-3 py-2 border-b border-gray-200 dark:border-white/10 flex items-center gap-3">
<span class="text-xs text-gray-700 dark:text-gray-300">{t("Units:")}</span>
<label class="flex items-center gap-1 text-xs cursor-pointer">
<input
type="radio"
@ -353,45 +348,45 @@
</label>
</div>
<table class="min-w-full text-xs">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th class="px-3 py-1.5 text-left font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-left font-medium text-gray-700 dark:text-gray-300">
{t("Tower")}
</th>
<th class="px-3 py-1.5 text-right font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-right font-medium text-gray-700 dark:text-gray-300">
{t("Distance")}
</th>
<th class="px-3 py-1.5 text-right font-medium text-cool-steel-700 dark:text-cool-steel-300">
<th class="px-3 py-1.5 text-right font-medium text-gray-700 dark:text-gray-300">
RSSI
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-white/10">
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for row <- @probe.rows do %>
<tr>
<td class="px-3 py-1.5 font-medium text-cool-steel-900 dark:text-cool-steel-100 truncate">
<td class="px-3 py-1.5 font-medium text-gray-900 dark:text-gray-100 truncate">
{row.coverage.name}
</td>
<td class="px-3 py-1.5 text-right font-mono text-cool-steel-700 dark:text-cool-steel-300">
<td class="px-3 py-1.5 text-right font-mono text-gray-700 dark:text-gray-300">
{fmt_distance(row.distance_m, @probe_units)}
</td>
<td class={[
"px-3 py-1.5 text-right font-mono",
cond do
row.rssi == :no_coverage ->
"text-cool-steel-400"
"text-gray-400"
is_number(row.rssi) and row.rssi >= -75 ->
"text-green-600 dark:text-green-400 font-bold"
is_number(row.rssi) and row.rssi >= -85 ->
"text-wheat-600 dark:text-wheat-400"
"text-yellow-600 dark:text-yellow-400"
is_number(row.rssi) ->
"text-sweet-salmon-600 dark:text-sweet-salmon-400"
"text-red-600 dark:text-red-400"
true ->
"text-cool-steel-400"
"text-gray-400"
end
]}>
{fmt_rssi(row.rssi)}
@ -404,10 +399,10 @@
</div>
<% end %>
<% true -> %>
<div class="h-[78vh] w-full rounded-lg border border-cool-steel-200 dark:border-white/10 flex items-center justify-center bg-cool-steel-50 dark:bg-cool-steel-900 text-center px-6">
<div class="h-[78vh] w-full rounded-lg border border-gray-200 dark:border-white/10 flex items-center justify-center bg-gray-50 dark:bg-gray-900 text-center px-6">
<div>
<.icon name="hero-signal-slash" class="h-12 w-12 mx-auto text-cool-steel-400 mb-2" />
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<.icon name="hero-signal-slash" class="h-12 w-12 mx-auto text-gray-400 mb-2" />
<p class="text-sm text-gray-600 dark:text-gray-400">
{t("No heatmap available yet. Run compute to generate the coverage prediction.")}
</p>
</div>

View file

@ -258,22 +258,19 @@ defmodule ToweropsWeb.DashboardLive do
defp favicon_status(_devices_down, _alert_count), do: "green"
defp health_score_color(score) when score > 80, do: "text-green-600 dark:text-green-400"
defp health_score_color(score) when score > 50, do: "text-wheat-600 dark:text-wheat-400"
defp health_score_color(_score), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
defp health_score_color(score) when score > 50, do: "text-yellow-600 dark:text-yellow-400"
defp health_score_color(_score), do: "text-red-600 dark:text-red-400"
defp source_classes("preseem"), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
defp source_classes("snmp"), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
defp source_classes("gaiia"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
defp source_classes("system"),
do: "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-700 dark:text-cool-steel-300"
defp source_classes(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
defp source_classes("gaiia"), do: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
defp source_classes("system"), do: "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"
defp source_classes(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
defp config_change_impact_color(change) do
cond do
change.change_size > 50 -> "bg-sweet-salmon-500"
change.change_size > 20 -> "bg-wheat-500"
change.change_size > 50 -> "bg-red-500"
change.change_size > 20 -> "bg-yellow-500"
true -> "bg-green-500"
end
end
@ -322,15 +319,15 @@ defmodule ToweropsWeb.DashboardLive do
amount = decimal_to_float(mrr)
cond do
amount >= 1000 -> "border-l-4 border-sweet-salmon-500 bg-sweet-salmon-50 dark:bg-sweet-salmon-950/30"
amount >= 1000 -> "border-l-4 border-red-500 bg-red-50 dark:bg-red-950/30"
amount >= 100 -> "border-l-4 border-orange-500 bg-orange-50 dark:bg-orange-950/30"
amount > 0 -> "border-l-4 border-wheat-500 bg-wheat-50 dark:bg-wheat-950/30"
true -> "border-l-4 border-cool-steel-300 bg-cool-steel-50 dark:bg-cool-steel-800/50"
amount > 0 -> "border-l-4 border-yellow-500 bg-yellow-50 dark:bg-yellow-950/30"
true -> "border-l-4 border-gray-300 bg-gray-50 dark:bg-gray-800/50"
end
end
defp site_health_dot(:red), do: "bg-sweet-salmon-500"
defp site_health_dot(:yellow), do: "bg-wheat-500"
defp site_health_dot(:red), do: "bg-red-500"
defp site_health_dot(:yellow), do: "bg-yellow-500"
defp site_health_dot(_), do: "bg-green-500"
defp decimal_to_float(%Decimal{} = d), do: Decimal.to_float(d)
@ -342,8 +339,8 @@ defmodule ToweropsWeb.DashboardLive do
defp mrr_at_risk_positive?(_), do: false
defp uptime_color(pct) when pct >= 99.0, do: "text-green-600 dark:text-green-400"
defp uptime_color(pct) when pct >= 95.0, do: "text-wheat-600 dark:text-wheat-400"
defp uptime_color(_pct), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
defp uptime_color(pct) when pct >= 95.0, do: "text-yellow-600 dark:text-yellow-400"
defp uptime_color(_pct), do: "text-red-600 dark:text-red-400"
defp format_qoe(score), do: :erlang.float_to_binary(score / 1, decimals: 1)

View file

@ -17,14 +17,14 @@
═══════════════════════════════════════════════ --%>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<h1 class="text-lg font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-lg font-bold text-gray-900 dark:text-white">
{@current_scope.organization.name}
</h1>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400 font-mono">
<span class="text-xs text-gray-400 dark:text-gray-500 dark:text-gray-400 font-mono">
NOC
</span>
</div>
<div class="flex items-center gap-1.5 text-xs text-cool-steel-400 dark:text-cool-steel-500 font-mono">
<div class="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75">
</span>
@ -40,70 +40,61 @@
<%!-- Empty State --%>
<%= if assigns[:summary] && @device_count == 0 do %>
<div class="mt-12 rounded-xl border-2 border-dashed border-cool-steel-300 bg-cool-steel-50/50 p-16 text-center dark:border-white/20 dark:bg-cool-steel-800/30">
<.icon
name="hero-light-bulb"
class="mx-auto h-14 w-14 text-cerulean-500 dark:text-cerulean-400"
/>
<h3 class="mt-6 text-xl font-semibold text-cool-steel-900 dark:text-white">
<div class="mt-12 rounded-xl border-2 border-dashed border-gray-300 bg-gray-50/50 p-16 text-center dark:border-white/20 dark:bg-gray-800/30">
<.icon name="hero-light-bulb" class="mx-auto h-14 w-14 text-blue-500 dark:text-blue-400" />
<h3 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
{t("Let's get started!")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400 max-w-md mx-auto">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t("Set up your network monitoring in a few steps.")}
</p>
<div class="mt-10 max-w-lg mx-auto">
<div class="space-y-5 text-left">
<%= if @current_scope.organization.use_sites do %>
<div class="flex gap-4 items-start">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-cerulean-100 text-cerulean-600 font-semibold text-sm dark:bg-cerulean-900 dark:text-cerulean-300">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm dark:bg-blue-900 dark:text-blue-300">
1
</div>
<div>
<h4 class="font-medium text-cool-steel-900 dark:text-white">
{t("Create a Site")}
</h4>
<p class="mt-0.5 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<h4 class="font-medium text-gray-900 dark:text-white">{t("Create a Site")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("Sites represent your physical locations")}
</p>
</div>
</div>
<div class="flex gap-4 items-start">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-cool-steel-100 text-cool-steel-500 font-semibold text-sm dark:bg-cool-steel-700 dark:text-cool-steel-400">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-500 font-semibold text-sm dark:bg-gray-700 dark:text-gray-400">
2
</div>
<div>
<h4 class="font-medium text-cool-steel-900 dark:text-white">
{t("Add Devices")}
</h4>
<p class="mt-0.5 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<h4 class="font-medium text-gray-900 dark:text-white">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("Add routers, switches, and servers to your sites")}
</p>
</div>
</div>
<% else %>
<div class="flex gap-4 items-start">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-cerulean-100 text-cerulean-600 font-semibold text-sm dark:bg-cerulean-900 dark:text-cerulean-300">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm dark:bg-blue-900 dark:text-blue-300">
1
</div>
<div>
<h4 class="font-medium text-cool-steel-900 dark:text-white">
{t("Add Devices")}
</h4>
<p class="mt-0.5 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<h4 class="font-medium text-gray-900 dark:text-white">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("Add network devices to monitor")}
</p>
</div>
</div>
<% end %>
<div class="flex gap-4 items-start">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-cool-steel-100 text-cool-steel-500 font-semibold text-sm dark:bg-cool-steel-700 dark:text-cool-steel-400">
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-500 font-semibold text-sm dark:bg-gray-700 dark:text-gray-400">
{if @current_scope.organization.use_sites, do: "3", else: "2"}
</div>
<div>
<h4 class="font-medium text-cool-steel-900 dark:text-white">
<h4 class="font-medium text-gray-900 dark:text-white">
{t("Monitor & Receive Alerts")}
</h4>
<p class="mt-0.5 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("TowerOps automatically monitors your devices and alerts you of issues")}
</p>
</div>
@ -128,33 +119,30 @@
<%= if assigns[:setup_checklist] && !@setup_checklist.all_done && @device_count > 0 do %>
<div
id="setup-checklist"
class="mt-4 rounded-lg border border-cerulean-200 bg-cerulean-50/50 dark:border-cerulean-800/50 dark:bg-cerulean-900/10 p-4"
class="mt-4 rounded-lg border border-blue-200 bg-blue-50/50 dark:border-blue-800/50 dark:bg-blue-900/10 p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<.icon
name="hero-rocket-launch"
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400"
/>
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-rocket-launch" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Setup Progress")}
</h3>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
{@setup_checklist.completed}/{@setup_checklist.total}
</span>
</div>
<button
phx-click={JS.hide(to: "#setup-checklist")}
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title={t("Dismiss")}
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
</div>
<%!-- Progress bar --%>
<div class="w-full bg-cool-steel-200 dark:bg-cool-steel-700 rounded-full h-1.5 mb-3">
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5 mb-3">
<div
class="bg-cerulean-600 dark:bg-cerulean-400 h-1.5 rounded-full transition-all duration-500"
class="bg-blue-600 dark:bg-blue-400 h-1.5 rounded-full transition-all duration-500"
style={"width: #{@setup_checklist.completed / @setup_checklist.total * 100}%"}
>
</div>
@ -166,22 +154,22 @@
class={[
"flex items-center gap-2.5 rounded-md px-3 py-2 text-sm transition-colors",
if(step.done,
do: "text-cool-steel-400 dark:text-cool-steel-500",
do: "text-gray-400 dark:text-gray-500",
else:
"text-cool-steel-700 dark:text-cool-steel-300 hover:bg-cerulean-100/50 dark:hover:bg-cerulean-900/20"
"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 %>
<div class="h-5 w-5 rounded-full border-2 border-cool-steel-300 dark:border-cool-steel-600 shrink-0" />
<div class="h-5 w-5 rounded-full border-2 border-gray-300 dark:border-gray-600 shrink-0" />
<% end %>
<span class={if step.done, do: "line-through"}>
{step.label}
</span>
<%= if step[:optional] do %>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500">optional</span>
<span class="text-xs text-gray-400 dark:text-gray-500">optional</span>
<% end %>
</.link>
<% end %>
@ -193,17 +181,17 @@
<%!-- ═══════════════════════════════════════════════
CONDENSED STATUS BAR — single dense line
═══════════════════════════════════════════════ --%>
<div class="mt-4 grid grid-cols-2 gap-0.5 rounded-md border border-cool-steel-200 bg-cool-steel-50 px-1 py-1 dark:border-white/10 dark:bg-cool-steel-800/80 font-mono text-xs sm:flex sm:flex-wrap sm:items-center sm:gap-1">
<div class="mt-4 grid grid-cols-2 gap-0.5 rounded-md border border-gray-200 bg-gray-50 px-1 py-1 dark:border-white/10 dark:bg-gray-800/80 font-mono text-xs sm:flex sm:flex-wrap sm:items-center sm:gap-1">
<%!-- Health --%>
<div class={[
"flex items-center gap-1.5 rounded px-2.5 py-1.5",
cond do
@summary.health_score > 80 -> "bg-green-100/80 dark:bg-green-900/30"
@summary.health_score > 50 -> "bg-wheat-100/80 dark:bg-wheat-900/30"
true -> "bg-sweet-salmon-100/80 dark:bg-sweet-salmon-900/30"
@summary.health_score > 50 -> "bg-yellow-100/80 dark:bg-yellow-900/30"
true -> "bg-red-100/80 dark:bg-red-900/30"
end
]}>
<span class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium">
<span class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium">
Health
</span>
<span class={[
@ -224,11 +212,11 @@
</span>
</div>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<%!-- Uptime --%>
<div class="flex items-center gap-1.5 px-2.5 py-1.5">
<span class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium">
<span class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium">
Up
</span>
<span class={["font-bold text-sm", uptime_color(@uptime_percentage)]}>
@ -236,29 +224,29 @@
do: "100",
else: :erlang.float_to_binary(@uptime_percentage, decimals: 1)}%
</span>
<span class="text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
{@device_up}/{@device_count}
</span>
<%= if @device_down > 0 do %>
<span class="text-sweet-salmon-500 font-bold">{@device_down}↓</span>
<span class="text-red-500 font-bold">{@device_down}↓</span>
<% end %>
</div>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<%!-- Alerts --%>
<.link
navigate={~p"/alerts"}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-cool-steel-200/50 dark:hover:bg-white/5 transition"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-gray-200/50 dark:hover:bg-white/5 transition"
>
<span class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium">
<span class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium">
Alerts
</span>
<%= if @total_alert_count > 0 do %>
<span class="font-bold text-sm text-sweet-salmon-600 dark:text-sweet-salmon-400">
<span class="font-bold text-sm text-red-600 dark:text-red-400">
{@total_alert_count}
</span>
<span class="text-sweet-salmon-500">
<span class="text-red-500">
{String.duplicate("●", min(@total_alert_count, 5))}
</span>
<% else %>
@ -268,36 +256,34 @@
</.link>
<%= if @current_scope.organization.use_sites && @sites_count > 0 do %>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<.link
navigate={~p"/sites"}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-cool-steel-200/50 dark:hover:bg-white/5 transition"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-gray-200/50 dark:hover:bg-white/5 transition"
>
<span class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium">
<span class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium">
Sites
</span>
<span class="font-bold text-sm text-cool-steel-900 dark:text-white">
{@sites_count}
</span>
<span class="font-bold text-sm text-gray-900 dark:text-white">{@sites_count}</span>
</.link>
<% end %>
<%= if @has_subscribers do %>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<div class="flex items-center gap-1.5 px-2.5 py-1.5">
<span
class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium"
class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium"
title="Subscribers"
>
Subs
</span>
<span class="font-bold text-sm text-cool-steel-900 dark:text-white">
<span class="font-bold text-sm text-gray-900 dark:text-white">
{format_number(@summary.subscribers.total)}
</span>
<%!-- Financial data: role-gated --%>
<%= if @can_view_financials do %>
<span
class="text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400"
class="text-gray-400 dark:text-gray-500 dark:text-gray-400"
title="Monthly Recurring Revenue"
>
{format_mrr(@summary.subscribers.total_mrr)}/mo
@ -307,16 +293,16 @@
<%!-- Financial data: role-gated --%>
<%= if @can_view_financials and mrr_at_risk_positive?(@impact_summary.mrr_at_risk) do %>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<div class="flex items-center gap-1.5 px-2.5 py-1.5 bg-sweet-salmon-100/80 dark:bg-sweet-salmon-900/30 rounded">
<span class="text-sweet-salmon-500 dark:text-sweet-salmon-400 text-[10px] uppercase font-sans font-medium">
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<div class="flex items-center gap-1.5 px-2.5 py-1.5 bg-red-100/80 dark:bg-red-900/30 rounded">
<span class="text-red-500 dark:text-red-400 text-[10px] uppercase font-sans font-medium">
At Risk
</span>
<span class="font-bold text-sm text-sweet-salmon-600 dark:text-sweet-salmon-400">
<span class="font-bold text-sm text-red-600 dark:text-red-400">
{format_mrr(@impact_summary.mrr_at_risk)}
</span>
<%= if @impact_summary.subscribers_affected > 0 do %>
<span class="text-sweet-salmon-500 dark:text-sweet-salmon-400">
<span class="text-red-500 dark:text-red-400">
{format_number(@impact_summary.subscribers_affected)} subs
</span>
<% end %>
@ -325,21 +311,21 @@
<% end %>
<%!-- Insights count --%>
<span class="hidden text-cool-steel-300 dark:text-cool-steel-600 sm:inline">│</span>
<span class="hidden text-gray-300 dark:text-gray-600 sm:inline">│</span>
<.link
navigate={~p"/insights"}
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-cool-steel-200/50 dark:hover:bg-white/5 transition"
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded hover:bg-gray-200/50 dark:hover:bg-white/5 transition"
>
<span class="text-cool-steel-500 dark:text-cool-steel-400 text-[10px] uppercase font-sans font-medium">
<span class="text-gray-500 dark:text-gray-400 text-[10px] uppercase font-sans font-medium">
Insights
</span>
<span class="font-bold text-sm text-cool-steel-900 dark:text-white">
<span class="font-bold text-sm text-gray-900 dark:text-white">
{@summary.insights.critical + @summary.insights.warning + @summary.insights.info}
</span>
<span :if={@summary.insights.critical > 0} class="text-sweet-salmon-500 text-[10px]">
<span :if={@summary.insights.critical > 0} class="text-red-500 text-[10px]">
{@summary.insights.critical}c
</span>
<span :if={@summary.insights.warning > 0} class="text-wheat-500 text-[10px]">
<span :if={@summary.insights.warning > 0} class="text-yellow-500 text-[10px]">
{@summary.insights.warning}w
</span>
</.link>
@ -351,19 +337,19 @@
<%= if @active_incidents != [] || @active_alerts != [] do %>
<div class="mt-5">
<div class="flex items-center gap-2 mb-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-sweet-salmon-600 dark:text-sweet-salmon-400">
<h2 class="text-xs font-semibold uppercase tracking-wider text-red-600 dark:text-red-400">
{t("Active Issues")}
</h2>
<span class="font-mono text-xs text-sweet-salmon-500 dark:text-sweet-salmon-400">
<span class="font-mono text-xs text-red-500 dark:text-red-400">
{length(@active_incidents) + @deduped_alert_count}
</span>
</div>
<%!-- Incidents (highest priority) --%>
<%= if @active_incidents != [] do %>
<div class="overflow-hidden rounded-md border border-sweet-salmon-200 dark:border-sweet-salmon-800/50 mb-2">
<div class="overflow-hidden rounded-md border border-red-200 dark:border-red-800/50 mb-2">
<table class="min-w-full">
<tbody class="divide-y divide-sweet-salmon-100 dark:divide-sweet-salmon-900/30">
<tbody class="divide-y divide-red-100 dark:divide-red-900/30">
<tr
:for={incident <- Enum.take(@active_incidents, 10)}
class={[
@ -380,13 +366,13 @@
"w-1 h-full min-h-[2.5rem]",
if @can_view_financials do
cond do
decimal_to_float(incident.mrr_at_risk) >= 1000 -> "bg-sweet-salmon-500"
decimal_to_float(incident.mrr_at_risk) >= 1000 -> "bg-red-500"
decimal_to_float(incident.mrr_at_risk) >= 100 -> "bg-orange-500"
decimal_to_float(incident.mrr_at_risk) > 0 -> "bg-wheat-500"
true -> "bg-cool-steel-400"
decimal_to_float(incident.mrr_at_risk) > 0 -> "bg-yellow-500"
true -> "bg-gray-400"
end
else
"bg-cool-steel-400"
"bg-gray-400"
end
]} />
</td>
@ -394,17 +380,17 @@
<div class="flex items-center gap-2">
<.icon
name="hero-exclamation-triangle"
class="h-3.5 w-3.5 text-sweet-salmon-500 flex-shrink-0"
class="h-3.5 w-3.5 text-red-500 flex-shrink-0"
/>
<.link
navigate={~p"/devices/#{incident.device_id}"}
class="text-sm font-semibold text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="text-sm font-semibold text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
{incident.device_name}
</.link>
<span
:if={incident.site_name}
class="text-[10px] text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400"
class="text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400"
>
@ {incident.site_name}
</span>
@ -412,15 +398,12 @@
</td>
<td class="px-3 py-1.5 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-3 font-mono text-xs">
<span
class="text-sweet-salmon-600 dark:text-sweet-salmon-400 font-semibold"
title="Duration"
>
<span class="text-red-600 dark:text-red-400 font-semibold" title="Duration">
{format_duration(incident.duration_seconds)}
</span>
<%= if incident.subscribers_affected > 0 do %>
<span
class="text-wheat-600 dark:text-wheat-400"
class="text-amber-600 dark:text-amber-400"
title="Subscribers affected"
>
{format_number(incident.subscribers_affected)}s
@ -428,7 +411,7 @@
<% end %>
<%!-- Financial data: role-gated --%>
<%= if @can_view_financials and mrr_at_risk_positive?(incident.mrr_at_risk) do %>
<span class="text-wheat-600 dark:text-wheat-400" title="MRR at risk">
<span class="text-amber-600 dark:text-amber-400" title="MRR at risk">
{format_mrr(incident.mrr_at_risk)}
</span>
<% end %>
@ -442,31 +425,31 @@
<%!-- Alerts table --%>
<%= if @active_alerts != [] do %>
<div class="overflow-x-auto rounded-md border border-cool-steel-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">
<thead class="bg-cool-steel-50/80 dark:bg-cool-steel-800/80">
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
<tr>
<th class="w-1 p-0"></th>
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 w-12"></th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-800/50">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-800/50">
<tr
:for={alert <- @active_alerts}
id={"alert-#{alert.id}"}
class="hover:bg-cool-steel-50 dark:hover:bg-white/5 group"
class="hover:bg-gray-50 dark:hover:bg-white/5 group"
>
<td class="w-1 p-0">
<div class={[
@ -474,14 +457,14 @@
cond do
alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] &&
alert.gaiia_impact["total_subscribers"] > 10 ->
"bg-sweet-salmon-500"
"bg-red-500"
alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] &&
alert.gaiia_impact["total_subscribers"] > 0 ->
"bg-orange-500"
true ->
"bg-wheat-500"
"bg-yellow-500"
end
]} />
</td>
@ -489,30 +472,30 @@
<div class="flex items-center gap-1.5 flex-wrap">
<.link
navigate={~p"/devices/#{alert.device.id}"}
class="text-sm font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
{alert.device.name}
</.link>
<span
:if={alert.device.ip_address}
class="hidden sm:inline font-mono text-[11px] text-cool-steel-400 dark:text-cool-steel-500"
class="hidden sm:inline font-mono text-[11px] text-gray-400 dark:text-gray-500"
>
{alert.device.ip_address}
</span>
</div>
<span
:if={alert.device.site}
class="text-[10px] text-cool-steel-400 dark:text-cool-steel-500"
class="text-[10px] text-gray-400 dark:text-gray-500"
>
{alert.device.site.name}
</span>
</td>
<td class="px-3 py-1.5 text-xs text-cool-steel-600 dark:text-cool-steel-400 max-w-[120px] sm: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}
</td>
<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 %>
<span class="font-mono text-xs text-wheat-600 dark:text-wheat-400">
<span class="font-mono text-xs text-amber-600 dark:text-amber-400">
{format_subscriber_count(alert.gaiia_impact["total_subscribers"])}
</span>
<% end %>
@ -521,7 +504,7 @@
<.timestamp
datetime={alert.triggered_at}
timezone={@timezone}
class="font-mono text-[11px] text-cool-steel-400 dark:text-cool-steel-500"
class="font-mono text-[11px] text-gray-400 dark:text-gray-500"
/>
</td>
<td class="hidden sm:table-cell px-3 py-1.5 text-right">
@ -530,12 +513,12 @@
type="button"
phx-click="acknowledge_alert"
phx-value-id={alert.id}
class="rounded px-1.5 py-0.5 text-[10px] font-medium text-cool-steel-500 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:ring-cool-steel-600 dark:hover:bg-cool-steel-700 dark:hover:text-cool-steel-200 opacity-0 group-hover:opacity-100 transition"
class="rounded px-1.5 py-0.5 text-[10px] font-medium text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:ring-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200 opacity-0 group-hover:opacity-100 transition"
>
ACK
</button>
<% else %>
<span class="text-[10px] text-cerulean-500 dark:text-cerulean-400 font-mono">
<span class="text-[10px] text-blue-500 dark:text-blue-400 font-mono">
ACK
</span>
<% end %>
@ -544,10 +527,10 @@
</tbody>
</table>
<%= if @total_alert_count > 20 do %>
<div class="border-t border-cool-steel-100 dark:border-white/5 px-3 py-1.5 bg-cool-steel-50/50 dark:bg-cool-steel-800/30">
<div class="border-t border-gray-100 dark:border-white/5 px-3 py-1.5 bg-gray-50/50 dark:bg-gray-800/30">
<.link
navigate={~p"/alerts"}
class="text-[10px] font-medium text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-[10px] font-medium text-blue-600 dark:text-blue-400 hover:underline"
>
+{@total_alert_count - 20} more alerts →
</.link>
@ -564,55 +547,52 @@
<%= if @current_scope.organization.use_sites && @site_impact_summaries != [] do %>
<div class="mt-5">
<div class="flex items-center justify-between mb-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Site Health")}
</h2>
<.link
navigate={~p"/sites"}
class="text-[10px] font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400"
class="text-[10px] font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
all sites →
</.link>
</div>
<div class="overflow-x-auto rounded-md border border-cool-steel-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">
<thead class="bg-cool-steel-50/80 dark:bg-cool-steel-800/80">
<thead class="bg-gray-50/80 dark:bg-gray-800/80">
<tr>
<th class="px-3 py-1.5 text-left text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
<th class="hidden sm:table-cell px-3 py-1.5 text-right text-[10px] font-medium uppercase tracking-wider text-cool-steel-400 dark:text-cool-steel-500">
<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")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-800/50">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-800/50">
<tr
:for={site <- @site_impact_summaries}
class={[
"hover:bg-cool-steel-50 dark:hover:bg-white/5",
if(site.devices_down > 0,
do: "bg-sweet-salmon-50/50 dark:bg-sweet-salmon-950/10",
else: ""
)
"hover:bg-gray-50 dark:hover:bg-white/5",
if(site.devices_down > 0, do: "bg-red-50/50 dark:bg-red-950/10", else: "")
]}
>
<td class="px-3 py-1.5">
<.link
navigate={~p"/sites/#{site.site_id}"}
class="flex items-center gap-1.5 text-sm font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="flex items-center gap-1.5 text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
<span class={[
"h-2 w-2 rounded-full flex-shrink-0",
@ -622,47 +602,47 @@
{site.name}
</.link>
</td>
<td class="px-3 py-1.5 text-right font-mono text-xs text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-3 py-1.5 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
{site.device_count}
</td>
<td class="px-3 py-1.5 text-right font-mono text-xs">
<%= if site.devices_down > 0 do %>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400 font-bold">
<span class="text-red-600 dark:text-red-400 font-bold">
{site.devices_down}
</span>
<% else %>
<span class="text-cool-steel-300 dark:text-cool-steel-600">0</span>
<span class="text-gray-300 dark:text-gray-600">0</span>
<% end %>
</td>
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs">
<%= if site.avg_qoe do %>
<span class={
cond do
site.avg_qoe < 2.0 -> "text-sweet-salmon-600 dark:text-sweet-salmon-400"
site.avg_qoe < 4.0 -> "text-wheat-600 dark:text-wheat-400"
site.avg_qoe < 2.0 -> "text-red-600 dark:text-red-400"
site.avg_qoe < 4.0 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-green-600 dark:text-green-400"
end
}>
{format_qoe(site.avg_qoe)}
</span>
<% else %>
<span class="text-cool-steel-300 dark:text-cool-steel-600">—</span>
<span class="text-gray-300 dark:text-gray-600">—</span>
<% end %>
</td>
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs text-cool-steel-600 dark:text-cool-steel-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 %>
{site.subscribers}
<% else %>
<span class="text-cool-steel-300 dark:text-cool-steel-600">—</span>
<span class="text-gray-300 dark:text-gray-600">—</span>
<% end %>
</td>
<td class="hidden sm:table-cell px-3 py-1.5 text-right font-mono text-xs">
<%= if site.subscribers_affected > 0 do %>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400 font-bold">
<span class="text-red-600 dark:text-red-400 font-bold">
{format_number(site.subscribers_affected)}
</span>
<% else %>
<span class="text-cool-steel-300 dark:text-cool-steel-600">—</span>
<span class="text-gray-300 dark:text-gray-600">—</span>
<% end %>
</td>
</tr>
@ -679,7 +659,7 @@
<div class="mt-5">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Insights")}
</h2>
<div class="flex items-center gap-0.5">
@ -688,8 +668,8 @@
class={[
"rounded px-1.5 py-0.5 text-[10px] font-medium transition",
if(is_nil(@insight_source),
do: "bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
else: "text-cool-steel-400 hover:bg-cool-steel-100 dark:hover:bg-white/10"
do: "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
else: "text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10"
)
]}
>
@ -701,8 +681,8 @@
class={[
"rounded px-1.5 py-0.5 text-[10px] font-medium transition",
if(@insight_source == source,
do: "bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
else: "text-cool-steel-400 hover:bg-cool-steel-100 dark:hover:bg-white/10"
do: "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
else: "text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10"
)
]}
>
@ -712,35 +692,35 @@
</div>
<.link
navigate={~p"/insights"}
class="text-[10px] font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400"
class="text-[10px] font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
all insights →
</.link>
</div>
<div class="overflow-hidden rounded-md border border-cool-steel-200 dark:border-white/10 divide-y divide-cool-steel-100 dark:divide-white/5">
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<div
:for={insight <- Enum.take(@insights, 6)}
id={"insight-#{insight.id}"}
class="flex items-center justify-between gap-2 px-3 py-2 bg-white dark:bg-cool-steel-800/50 hover:bg-cool-steel-50 dark:hover:bg-white/5 group"
class="flex items-center justify-between gap-2 px-3 py-2 bg-white dark:bg-gray-800/50 hover:bg-gray-50 dark:hover:bg-white/5 group"
>
<div class="flex items-center gap-2 min-w-0 flex-1">
<%!-- Urgency indicator --%>
<span class={[
"w-1.5 h-1.5 rounded-full flex-shrink-0",
case insight.urgency do
"critical" -> "bg-sweet-salmon-500"
"warning" -> "bg-wheat-500"
"info" -> "bg-cerulean-500"
_ -> "bg-cool-steel-400"
"critical" -> "bg-red-500"
"warning" -> "bg-yellow-500"
"info" -> "bg-blue-500"
_ -> "bg-gray-400"
end
]} />
<span class={[
"text-[10px] font-medium uppercase w-8 flex-shrink-0",
case insight.urgency do
"critical" -> "text-sweet-salmon-600 dark:text-sweet-salmon-400"
"warning" -> "text-wheat-600 dark:text-wheat-400"
"info" -> "text-cerulean-600 dark:text-cerulean-400"
_ -> "text-cool-steel-500 dark:text-cool-steel-400"
"critical" -> "text-red-600 dark:text-red-400"
"warning" -> "text-yellow-600 dark:text-yellow-400"
"info" -> "text-blue-600 dark:text-blue-400"
_ -> "text-gray-500 dark:text-gray-400"
end
]}>
{case insight.urgency do
@ -758,7 +738,7 @@
</span>
<.link
navigate={~p"/insights?source=#{insight.source}"}
class="text-sm text-cool-steel-900 dark:text-white hover:text-cerulean-600 dark:hover:text-cerulean-400 truncate"
class="text-sm text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 truncate"
>
{insight.title}
</.link>
@ -766,7 +746,7 @@
<div class="flex items-center gap-1 flex-shrink-0">
<.link
navigate={~p"/insights?source=#{insight.source}"}
class="rounded px-1.5 py-0.5 text-[10px] font-medium text-cerulean-600 dark:text-cerulean-400 hover:bg-cerulean-50 dark:hover:bg-cerulean-900/20 opacity-0 group-hover:opacity-100 transition"
class="rounded px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 opacity-0 group-hover:opacity-100 transition"
>
View
</.link>
@ -774,7 +754,7 @@
type="button"
phx-click="dismiss_insight"
phx-value-id={insight.id}
class="rounded p-0.5 text-cool-steel-300 hover:text-cool-steel-500 dark:text-cool-steel-400 dark:text-cool-steel-600 dark:hover:text-cool-steel-400 opacity-0 group-hover:opacity-100 transition"
class="rounded p-0.5 text-gray-300 hover:text-gray-500 dark:text-gray-400 dark:text-gray-600 dark:hover:text-gray-400 opacity-0 group-hover:opacity-100 transition"
title="Dismiss"
>
<.icon name="hero-x-mark" class="h-3 w-3" />
@ -791,41 +771,39 @@
<div class="mt-5 grid gap-4 lg:grid-cols-2">
<%!-- Network Pulse --%>
<div>
<h2 class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-2">
{t("Network Pulse")}
</h2>
<div class="rounded-md border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-800/50 divide-y divide-cool-steel-100 dark:divide-white/5">
<div class="rounded-md border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-800/50 divide-y divide-gray-100 dark:divide-white/5">
<div class="flex items-center justify-between px-3 py-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
Devices monitored
</span>
<span class="font-mono text-sm font-bold text-cool-steel-900 dark:text-white">
<span class="text-xs text-gray-500 dark:text-gray-400">Devices monitored</span>
<span class="font-mono text-sm font-bold text-gray-900 dark:text-white">
{@device_count}
</span>
</div>
<div class="flex items-center justify-between px-3 py-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">Online</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Online</span>
<span class="font-mono text-sm text-green-600 dark:text-green-400">{@device_up}</span>
</div>
<div class="flex items-center justify-between px-3 py-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">Down</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Down</span>
<span class={[
"font-mono text-sm",
if(@device_down > 0,
do: "text-sweet-salmon-600 dark:text-sweet-salmon-400 font-bold",
else: "text-cool-steel-400"
do: "text-red-600 dark:text-red-400 font-bold",
else: "text-gray-400"
)
]}>
{@device_down}
</span>
</div>
<div class="flex items-center justify-between px-3 py-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">Unknown</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Unknown</span>
<span class={[
"font-mono text-sm",
if(@device_unknown > 0,
do: "text-wheat-600 dark:text-wheat-400",
else: "text-cool-steel-400"
do: "text-yellow-600 dark:text-yellow-400",
else: "text-gray-400"
)
]}>
{@device_unknown}
@ -833,14 +811,12 @@
</div>
<%!-- Config changes row hidden — feature temporarily disabled --%>
<div class="flex items-center justify-between px-3 py-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
Active incidents
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Active incidents</span>
<span class={[
"font-mono text-sm",
if(length(@active_incidents) > 0,
do: "text-sweet-salmon-600 dark:text-sweet-salmon-400 font-bold",
else: "text-cool-steel-400"
do: "text-red-600 dark:text-red-400 font-bold",
else: "text-gray-400"
)
]}>
{length(@active_incidents)}
@ -852,45 +828,45 @@
<%!-- Config Changes — hidden, feature temporarily disabled --%>
<div class="hidden">
<div class="flex items-center justify-between mb-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
Config Changes
<span class="font-normal text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<span class="font-normal text-gray-400 dark:text-gray-500 dark:text-gray-400">
(7d)
</span>
</h2>
<.link
navigate={~p"/insights"}
class="text-[10px] font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400"
class="text-[10px] font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
all →
</.link>
</div>
<%= if Enum.any?(@recent_config_changes) do %>
<div class="overflow-hidden rounded-md border border-cool-steel-200 dark:border-white/10 divide-y divide-cool-steel-100 dark:divide-white/5">
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<%= for change <- @recent_config_changes do %>
<.link
navigate={~p"/devices/#{change.device_id}/config-timeline"}
class="flex items-center gap-2 px-3 py-1.5 bg-white hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:hover:bg-white/5 transition-colors"
class="flex items-center gap-2 px-3 py-1.5 bg-white hover:bg-gray-50 dark:bg-gray-800/50 dark:hover:bg-white/5 transition-colors"
>
<span class={"w-1.5 h-1.5 rounded-full flex-shrink-0 #{config_change_impact_color(change)}"} />
<span class="text-xs font-medium text-cool-steel-900 dark:text-white truncate">
<span class="text-xs font-medium text-gray-900 dark:text-white truncate">
{if change.device, do: change.device.name, else: "Unknown"}
</span>
<span class="font-mono text-[10px] text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400 flex-shrink-0">
<span class="font-mono text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400 flex-shrink-0">
{change.change_size}L
</span>
<span class="text-[10px] text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400 truncate">
<span class="text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400 truncate">
{Enum.join(Enum.take(change.sections_changed, 2), ", ")}
</span>
<span class="ml-auto flex-shrink-0 font-mono text-[10px] text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<span class="ml-auto flex-shrink-0 font-mono text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400">
{format_short_datetime(change.changed_at, @timezone)}
</span>
</.link>
<% end %>
</div>
<% else %>
<div class="rounded-md border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-800/50 px-3 py-6 text-center">
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-md border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-800/50 px-3 py-6 text-center">
<span class="text-xs text-gray-400 dark:text-gray-500 dark:text-gray-400">
No config changes in 7 days
</span>
</div>
@ -904,26 +880,26 @@
<%= if assigns[:recent_activity] && @recent_activity != [] do %>
<div class="mt-5">
<div class="flex items-center justify-between mb-2">
<h2 class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<h2 class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Recent Activity")}
</h2>
<.link
navigate={~p"/activity"}
class="text-[10px] font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400"
class="text-[10px] font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
all activity →
</.link>
</div>
<div class="overflow-hidden rounded-md border border-cool-steel-200 dark:border-white/10 divide-y divide-cool-steel-100 dark:divide-white/5">
<div class="overflow-hidden rounded-md border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<div
:for={activity <- @recent_activity}
class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-cool-steel-800/50"
class="flex items-center gap-2 px-3 py-1.5 bg-white dark:bg-gray-800/50"
>
<span class="h-1 w-1 rounded-full bg-cerulean-400 dark:bg-cerulean-500 flex-shrink-0" />
<p class="text-xs text-cool-steel-700 dark:text-cool-steel-300 min-w-0 flex-1 truncate">
<span class="h-1 w-1 rounded-full bg-blue-400 dark:bg-blue-500 flex-shrink-0" />
<p class="text-xs text-gray-700 dark:text-gray-300 min-w-0 flex-1 truncate">
{activity.summary}
</p>
<span class="flex-shrink-0 font-mono text-[10px] text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<span class="flex-shrink-0 font-mono text-[10px] text-gray-400 dark:text-gray-500 dark:text-gray-400">
{format_short_datetime(activity.timestamp, @timezone)}
</span>
</div>
@ -934,30 +910,30 @@
<%!-- ═══════════════════════════════════════════════
FOOTER — Command palette hint
═══════════════════════════════════════════════ --%>
<div class="mt-6 flex items-center justify-center border-t border-cool-steel-100 dark:border-white/5 pt-4 pb-2">
<span class="text-[10px] text-cool-steel-300 dark:text-cool-steel-600">
<kbd class="px-1 py-0.5 rounded border border-cool-steel-200 dark:border-cool-steel-700 font-mono text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-6 flex items-center justify-center border-t border-gray-100 dark:border-white/5 pt-4 pb-2">
<span class="text-[10px] text-gray-300 dark:text-gray-600">
<kbd class="px-1 py-0.5 rounded border border-gray-200 dark:border-gray-700 font-mono text-gray-400 dark:text-gray-500 dark:text-gray-400">
⌘K
</kbd>
<span class="ml-1">to search</span>
<span class="mx-2">·</span>
<.link
navigate={~p"/devices/new"}
class="hover:text-cool-steel-500 dark:text-cool-steel-400 dark:hover:text-cool-steel-400"
class="hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-400"
>
+ device
</.link>
<span class="mx-2">·</span>
<.link
navigate={~p"/trace"}
class="hover:text-cool-steel-500 dark:text-cool-steel-400 dark:hover:text-cool-steel-400"
class="hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-400"
>
trace
</.link>
<span class="mx-2">·</span>
<.link
navigate={~p"/network-map"}
class="hover:text-cool-steel-500 dark:text-cool-steel-400 dark:hover:text-cool-steel-400"
class="hover:text-gray-500 dark:text-gray-400 dark:hover:text-gray-400"
>
map
</.link>

View file

@ -4,7 +4,7 @@
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div phx-hook="ScrollToTop" id="scroll-container" phx-update="ignore">
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<div class="mb-4">
<.link
navigate={
@ -12,7 +12,7 @@
do: ~p"/devices/#{@device.id}",
else: ~p"/devices"
}
class="inline-flex items-center gap-1 text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{if @live_action == :edit, do: "Back to Device", else: "Back to Device List"}
@ -21,10 +21,10 @@
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{@page_title}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{if @live_action == :new,
do: "Add new device to monitor",
else: "Update device details"}
@ -42,19 +42,19 @@
<% badge_class =
cond do
@current_scope.user.is_superuser ->
"bg-cerulean-50 text-cerulean-800 dark:bg-cerulean-900/20 dark:text-cerulean-400"
"bg-blue-50 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
@device_quota.at_limit ->
"bg-sweet-salmon-50 text-sweet-salmon-800 border-sweet-salmon-200 dark:bg-sweet-salmon-900/20 dark:text-sweet-salmon-400 dark:border-sweet-salmon-800"
"bg-red-50 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"
percent >= 90 ->
"bg-orange-50 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800"
percent >= 75 ->
"bg-wheat-50 text-wheat-800 border-wheat-200 dark:bg-wheat-900/20 dark:text-wheat-400 dark:border-wheat-800"
"bg-yellow-50 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800"
@device_quota.limit == :unlimited ->
"bg-cerulean-50 text-cerulean-800 dark:bg-cerulean-900/20 dark:text-cerulean-400"
"bg-blue-50 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
true ->
"bg-green-50 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
@ -96,14 +96,14 @@
<% end %>
<.form for={@form} id="device-form" phx-change="validate" phx-submit="save">
<div class="divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Basic Information -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Basic Information")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Core device identification and location details.")}
</p>
</div>
@ -129,15 +129,15 @@
<div
:if={@duplicate_device}
class="rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-3 mt-2"
class="rounded-md bg-amber-50 dark:bg-amber-900/20 p-3 mt-2"
>
<div class="flex items-center gap-2 text-wheat-800 dark:text-wheat-200">
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 flex-shrink-0" />
<p class="text-sm">
{t("A device with this IP address already exists:")}
<.link
navigate={~p"/devices/#{@duplicate_device.id}"}
class="font-medium underline hover:text-wheat-900 dark:hover:text-wheat-100"
class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100"
>
{@duplicate_device.name || @duplicate_device.ip_address}
</.link>
@ -147,9 +147,9 @@
<div
:if={@non_routable_ip_error}
class="rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 p-3 mt-2"
class="rounded-md bg-red-50 dark:bg-red-900/20 p-3 mt-2"
>
<div class="flex items-start gap-2 text-sweet-salmon-800 dark:text-sweet-salmon-200">
<div class="flex items-start gap-2 text-red-800 dark:text-red-200">
<.icon name="hero-exclamation-circle" class="h-5 w-5 flex-shrink-0 mt-0.5" />
<div class="text-sm">
<p class="font-medium">Non-routable IP address detected</p>
@ -176,7 +176,7 @@
prompt="No site (ungrouped)"
options={Enum.map(@available_sites, &{&1.name, &1.id})}
/>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Optionally group this device into a site (office, datacenter, etc.)")}
</p>
</div>
@ -201,7 +201,7 @@
{"Other", "other"}
]}
/>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"Select the type of device. This helps with network visualization and organization."
)}
@ -214,10 +214,10 @@
<!-- Monitoring Configuration -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Monitoring Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Configure how and how often this device should be monitored.")}
</p>
</div>
@ -263,35 +263,35 @@
<%= if @live_action == :edit and Map.has_key?(assigns, :agent_source) do %>
<%= case @agent_source do %>
<% :device -> %>
<p class="mt-2 text-sm text-wheat-600 dark:text-wheat-400 flex items-center gap-1">
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>{t("Overriding")}</strong>
site/organization defaults - this device is explicitly assigned
</p>
<% :site -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" />
Inherited from site: <strong>{@effective_agent_name}</strong>
</p>
<% :organization -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization: <strong>{@effective_agent_name}</strong>
</p>
<% :global -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-globe-alt" class="h-4 w-4" />
{t("Using global default cloud poller:")}
<strong>{@effective_agent_name}</strong>
</p>
<% :none -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-cloud" class="h-4 w-4" />
{t("No agent assigned - cloud polling")}
</p>
<% end %>
<% else %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t(
"Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults."
)}
@ -309,7 +309,7 @@
prompt={t("Inherit from organization default")}
options={Enum.map(@escalation_policies, &{&1.name, &1.id})}
/>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"Select an escalation policy for alert notifications. Leave empty to use the organization default."
)}
@ -323,10 +323,10 @@
<!-- SNMP Configuration -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Choose monitoring mode and configure SNMP settings for detailed device information."
)}
@ -337,7 +337,7 @@
<div class="space-y-6">
<!-- Monitoring Mode Tabs -->
<div>
<div class="border-b border-cool-steel-200 dark:border-white/10">
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8" aria-label={t("Monitoring mode")}>
<button
id="monitoring-mode-snmp-and-icmp"
@ -347,9 +347,9 @@
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
@monitoring_mode == "snmp_and_icmp" &&
"border-cerulean-500 text-cerulean-600 dark:border-cerulean-400 dark:text-cerulean-400",
"border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400",
@monitoring_mode != "snmp_and_icmp" &&
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
{t("SNMP & ICMP")}
@ -362,16 +362,16 @@
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
@monitoring_mode == "icmp_only" &&
"border-cerulean-500 text-cerulean-600 dark:border-cerulean-400 dark:text-cerulean-400",
"border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400",
@monitoring_mode != "icmp_only" &&
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
{t("ICMP Only")}
</button>
</nav>
</div>
<div class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<%= if @monitoring_mode == "snmp_and_icmp" do %>
<p>
{t(
@ -415,12 +415,12 @@
<%= if @live_action == :edit and Map.has_key?(assigns, :snmp_config_source) do %>
<%= case @snmp_config_source do %>
<% :device -> %>
<p class="mt-2 text-sm text-wheat-600 dark:text-wheat-400 flex items-center gap-1">
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>{t("Overriding")}</strong> site/organization SNMP defaults
</p>
<% :site -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office" class="h-4 w-4" />
Inherited from site (v{@effective_snmp_version}, community: {String.slice(
@effective_snmp_community,
@ -431,7 +431,7 @@
)})
</p>
<% :organization -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Inherited from organization (v{@effective_snmp_version}, community: {String.slice(
@effective_snmp_community,
@ -442,7 +442,7 @@
)})
</p>
<% :default -> %>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-information-circle" class="h-4 w-4" />
{t(
"Using default (v2c, no community set - configure at organization or site level)"
@ -589,16 +589,16 @@
<%= if @snmp_test_result do %>
<%= cond do %>
<% Map.get(@snmp_test_result, :testing) -> %>
<div class="mt-3 rounded-md bg-cerulean-50 p-4 dark:bg-cerulean-900/20">
<div class="mt-3 rounded-md bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex items-center">
<div class="shrink-0">
<.icon
name="hero-arrow-path"
class="h-5 w-5 text-cerulean-400 animate-spin"
class="h-5 w-5 text-blue-400 animate-spin"
/>
</div>
<div class="ml-3">
<p class="text-sm text-cerulean-700 dark:text-cerulean-400">
<p class="text-sm text-blue-700 dark:text-blue-400">
{t("Testing SNMP credentials...")}
</p>
</div>
@ -621,16 +621,16 @@
</div>
</div>
<% true -> %>
<div class="mt-3 rounded-md bg-sweet-salmon-50 p-4 dark:bg-sweet-salmon-900/20">
<div class="mt-3 rounded-md bg-red-50 p-4 dark:bg-red-900/20">
<div class="flex items-start">
<div class="shrink-0">
<.icon name="hero-x-circle" class="h-5 w-5 text-sweet-salmon-400" />
<.icon name="hero-x-circle" class="h-5 w-5 text-red-400" />
</div>
<div class="ml-3">
<p class="text-sm font-medium text-sweet-salmon-800 dark:text-sweet-salmon-400">
<p class="text-sm font-medium text-red-800 dark:text-red-400">
{t("Connection failed")}
</p>
<p class="mt-1 text-sm text-sweet-salmon-700 dark:text-sweet-salmon-300">
<p class="mt-1 text-sm text-red-700 dark:text-red-300">
{Map.get(@snmp_test_result, :message)}
</p>
</div>
@ -653,16 +653,16 @@
phx-update="ignore"
>
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Configure MikroTik RouterOS API access. Works alongside SNMP for enhanced device management. SSH is used for device backups."
)}
</p>
<div class="mt-3 rounded-md bg-cerulean-50 dark:bg-cerulean-900/20 p-3">
<p class="text-xs text-cerulean-700 dark:text-cerulean-300">
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
@ -673,8 +673,8 @@
<div class="md:col-span-2">
<div class="space-y-6">
<%= if Map.has_key?(assigns, :mikrotik_config_source) and @mikrotik_config_source != :device do %>
<div class="rounded-md bg-cerulean-50 dark:bg-cerulean-900/20 p-4">
<p class="text-sm text-cerulean-700 dark:text-cerulean-300">
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-4">
<p class="text-sm text-blue-700 dark:text-blue-300">
<strong>Inheriting credentials from {@mikrotik_config_source}.</strong>
Username: <span class="font-mono">{@effective_mikrotik_username}</span>
</p>
@ -727,8 +727,8 @@
<%= if @form[:mikrotik_use_ssl].value == false do %>
<div class="col-span-full">
<div class="rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 p-4">
<p class="text-sm text-sweet-salmon-700 dark:text-sweet-salmon-300">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>🚨 Critical Security Warning:</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network."
@ -750,7 +750,7 @@
</div>
<!-- Save/Cancel Footer -->
<div class="flex items-center justify-between gap-x-6 border-t border-cool-steel-200 px-4 py-4 sm:px-8 dark:border-white/10">
<div class="flex items-center justify-between gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-8 dark:border-white/10">
<.button navigate={~p"/devices"}>{t("Cancel")}</.button>
<.button
phx-disable-with={t("Saving...")}
@ -764,19 +764,19 @@
<!-- Danger Zone -->
<%= if @live_action == :edit do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-cool-steel-200 dark:border-white/10">
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-sweet-salmon-600 dark:text-sweet-salmon-500">
<h2 class="text-base/7 font-semibold text-red-600 dark:text-red-500">
{t("Danger Zone")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Permanently delete this device and all its data.")}
</p>
</div>
<div class="md:col-span-2">
<div class="rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 p-4 border border-sweet-salmon-200 dark:border-sweet-salmon-800">
<p class="text-sm text-sweet-salmon-800 dark:text-sweet-salmon-200 mb-4">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200 mb-4">
{t(
"Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted."
)}

View file

@ -251,18 +251,15 @@ defmodule ToweropsWeb.DeviceLive.Helpers.Formatters do
Returns Tailwind CSS classes for capacity source badges.
"""
@spec capacity_source_class(String.t()) :: String.t()
def capacity_source_class("manual"),
do: "bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/50 dark:text-cerulean-300"
def capacity_source_class("manual"), do: "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
def capacity_source_class("sensor"), do: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300"
def capacity_source_class("if_speed"),
do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300"
def capacity_source_class("if_speed"), do: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
def capacity_source_class("peak"), do: "bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300"
def capacity_source_class(_),
do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300"
def capacity_source_class(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
@doc """
Returns background color class for utilization percentage.
@ -272,8 +269,8 @@ defmodule ToweropsWeb.DeviceLive.Helpers.Formatters do
- < 70%: green
"""
@spec utilization_color(number()) :: String.t()
def utilization_color(pct) when pct >= 90, do: "bg-sweet-salmon-500"
def utilization_color(pct) when pct >= 70, do: "bg-wheat-500"
def utilization_color(pct) when pct >= 90, do: "bg-red-500"
def utilization_color(pct) when pct >= 70, do: "bg-yellow-500"
def utilization_color(_pct), do: "bg-green-500"
@doc """
@ -284,7 +281,7 @@ defmodule ToweropsWeb.DeviceLive.Helpers.Formatters do
- < 70%: green
"""
@spec utilization_text_color(number()) :: String.t()
def utilization_text_color(pct) when pct >= 90, do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def utilization_text_color(pct) when pct >= 70, do: "text-wheat-600 dark:text-wheat-400"
def utilization_text_color(pct) when pct >= 90, do: "text-red-600 dark:text-red-400"
def utilization_text_color(pct) when pct >= 70, do: "text-yellow-600 dark:text-yellow-400"
def utilization_text_color(_pct), do: "text-green-600 dark:text-green-400"
end

View file

@ -508,12 +508,12 @@ defmodule ToweropsWeb.DeviceLive.Index do
# Health indicator helpers
defp health_dot_color(nil), do: "bg-cool-steel-400"
defp health_dot_color(nil), do: "bg-gray-400"
defp health_dot_color(%{worst_status: 0}), do: "bg-green-500"
defp health_dot_color(%{worst_status: 1}), do: "bg-wheat-500"
defp health_dot_color(%{worst_status: 2}), do: "bg-sweet-salmon-500"
defp health_dot_color(%{worst_status: 3}), do: "bg-cool-steel-400"
defp health_dot_color(_), do: "bg-cool-steel-400"
defp health_dot_color(%{worst_status: 1}), do: "bg-yellow-500"
defp health_dot_color(%{worst_status: 2}), do: "bg-red-500"
defp health_dot_color(%{worst_status: 3}), do: "bg-gray-400"
defp health_dot_color(_), do: "bg-gray-400"
defp health_dot_title(nil), do: "No check data"
defp health_dot_title(%{worst_status: 0}), do: "All checks passing"
@ -522,7 +522,7 @@ defmodule ToweropsWeb.DeviceLive.Index do
defp health_dot_title(%{worst_status: 3}), do: "Unknown"
defp health_dot_title(_), do: "No check data"
defp last_seen_text(nil), do: {"Never", "text-cool-steel-400 dark:text-cool-steel-600"}
defp last_seen_text(nil), do: {"Never", "text-gray-400 dark:text-gray-600"}
defp last_seen_text(datetime) do
diff_seconds = DateTime.diff(DateTime.utc_now(), datetime, :second)
@ -531,15 +531,15 @@ defmodule ToweropsWeb.DeviceLive.Index do
diff_seconds < 0 -> {"just now", "text-green-600 dark:text-green-400"}
diff_seconds < 60 -> {"#{diff_seconds}s ago", "text-green-600 dark:text-green-400"}
diff_seconds < 600 -> {"#{div(diff_seconds, 60)}m ago", "text-green-600 dark:text-green-400"}
diff_seconds < 3600 -> {"#{div(diff_seconds, 60)}m ago", "text-wheat-600 dark:text-wheat-400"}
diff_seconds < 86_400 -> {"#{div(diff_seconds, 3600)}h ago", "text-sweet-salmon-600 dark:text-sweet-salmon-400"}
true -> {"#{div(diff_seconds, 86_400)}d ago", "text-sweet-salmon-600 dark:text-sweet-salmon-400"}
diff_seconds < 3600 -> {"#{div(diff_seconds, 60)}m ago", "text-yellow-600 dark:text-yellow-400"}
diff_seconds < 86_400 -> {"#{div(diff_seconds, 3600)}h ago", "text-red-600 dark:text-red-400"}
true -> {"#{div(diff_seconds, 86_400)}d ago", "text-red-600 dark:text-red-400"}
end
end
defp response_time_badge(nil), do: {"-", "text-cool-steel-400"}
defp response_time_badge(nil), do: {"-", "text-gray-400"}
defp response_time_badge(%{response_time_ms: nil}), do: {"-", "text-cool-steel-400"}
defp response_time_badge(%{response_time_ms: nil}), do: {"-", "text-gray-400"}
defp response_time_badge(%{response_time_ms: ms}) do
formatted =
@ -552,8 +552,8 @@ defmodule ToweropsWeb.DeviceLive.Index do
color =
cond do
ms < 20 -> "text-green-600 dark:text-green-400"
ms <= 100 -> "text-wheat-600 dark:text-wheat-400"
true -> "text-sweet-salmon-600 dark:text-sweet-salmon-400"
ms <= 100 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-red-600 dark:text-red-400"
end
{formatted, color}
@ -567,14 +567,14 @@ defmodule ToweropsWeb.DeviceLive.Index do
min(100, round(ms / 2))
end
defp response_time_bar_color(nil), do: "bg-cool-steel-300"
defp response_time_bar_color(%{response_time_ms: nil}), do: "bg-cool-steel-300"
defp response_time_bar_color(nil), do: "bg-gray-300"
defp response_time_bar_color(%{response_time_ms: nil}), do: "bg-gray-300"
defp response_time_bar_color(%{response_time_ms: ms}) do
cond do
ms < 20 -> "bg-green-500"
ms <= 100 -> "bg-wheat-500"
true -> "bg-sweet-salmon-500"
ms <= 100 -> "bg-yellow-500"
true -> "bg-red-500"
end
end

View file

@ -5,12 +5,10 @@
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<%!-- Header: title + actions, then tabs below on mobile --%>
<div class="mb-4 border-b border-cool-steel-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 justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white truncate">
{@page_title}
</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white truncate">{@page_title}</h1>
<% percent =
if @device_quota.limit != :unlimited and @device_quota.limit > 0 do
(@device_quota.current / @device_quota.limit * 100) |> trunc()
@ -20,16 +18,16 @@
<% badge_class =
cond do
@current_scope.user.is_superuser ->
"bg-cerulean-50 text-cerulean-700 dark:bg-cerulean-900/20 dark:text-cerulean-400"
"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 ->
"bg-sweet-salmon-50 text-sweet-salmon-700 dark:bg-sweet-salmon-900/20 dark:text-sweet-salmon-400"
"bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400"
percent >= 90 ->
"bg-orange-50 text-orange-700 dark:bg-orange-900/20 dark:text-orange-400"
@device_quota.limit == :unlimited ->
"bg-cerulean-50 text-cerulean-700 dark:bg-cerulean-900/20 dark:text-cerulean-400"
"bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400"
true ->
"bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
@ -100,9 +98,9 @@
class={[
"px-2.5 py-1 rounded-md text-sm font-medium transition-colors",
if @active_tab == "existing" do
"bg-cool-steel-100 text-cool-steel-900 dark:bg-cool-steel-800 dark:text-white"
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
else
"text-cool-steel-500 hover:text-cool-steel-700 hover:bg-cool-steel-50 dark:text-cool-steel-400 dark:hover:text-cool-steel-300 dark:hover:bg-cool-steel-800/50"
"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
]}
>
@ -113,16 +111,16 @@
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-cool-steel-100 text-cool-steel-900 dark:bg-cool-steel-800 dark:text-white"
"bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-white"
else
"text-cool-steel-500 hover:text-cool-steel-700 hover:bg-cool-steel-50 dark:text-cool-steel-400 dark:hover:text-cool-steel-300 dark:hover:bg-cool-steel-800/50"
"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-cerulean-100 text-cerulean-800 dark:bg-cerulean-900 dark:text-cerulean-200"
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>
@ -133,9 +131,9 @@
<%= case @active_tab do %>
<% "existing" -> %>
<%= if @sites_enabled && !@has_sites && @has_devices do %>
<div class="rounded-md bg-cerulean-50 dark:bg-cerulean-900/20 p-3 mb-4 flex items-center gap-3">
<.icon name="hero-information-circle" class="h-5 w-5 text-cerulean-400 flex-shrink-0" />
<p class="text-sm text-cerulean-800 dark:text-cerulean-200 flex-1">
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 mb-4 flex items-center gap-3">
<.icon name="hero-information-circle" class="h-5 w-5 text-blue-400 flex-shrink-0" />
<p class="text-sm text-blue-800 dark:text-blue-200 flex-1">
{t("Organize your devices with sites")} —
<.link navigate={~p"/sites/new"} class="font-medium underline">
{t("Create a site")}
@ -148,12 +146,12 @@
<div class="text-center py-16">
<.icon
name="hero-server"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 dark:text-gray-400"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No devices")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by adding your first device.")}
</p>
<div class="mt-6">
@ -164,14 +162,14 @@
</div>
<% else %>
<%!-- Sticky Status Bar + Search --%>
<div class="sticky top-0 z-10 bg-white dark:bg-cool-steel-900 pb-3 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-ml-4 lg:-mr-8 lg:pl-4 lg:pr-8 border-b border-cool-steel-100 dark:border-white/5">
<div class="sticky top-0 z-10 bg-white dark:bg-gray-900 pb-3 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-ml-4 lg:-mr-8 lg:pl-4 lg:pr-8 border-b border-gray-100 dark:border-white/5">
<div class="flex items-center gap-3 pt-2">
<%!-- Search --%>
<form phx-change="search" class="flex-1 max-w-sm">
<div class="relative">
<.icon
name="hero-magnifying-glass"
class="absolute left-2.5 top-2 h-4 w-4 text-cool-steel-400"
class="absolute left-2.5 top-2 h-4 w-4 text-gray-400"
/>
<input
type="text"
@ -179,7 +177,7 @@
value={@search_query}
placeholder={t("Filter by name or IP...")}
phx-debounce="150"
class="block w-full rounded-md border-0 py-1.5 pl-9 pr-3 text-sm bg-cool-steel-50 dark:bg-cool-steel-800 text-cool-steel-900 dark:text-white ring-1 ring-inset ring-cool-steel-300 dark:ring-white/10 placeholder:text-cool-steel-400 focus:ring-2 focus:ring-cerulean-500 font-mono"
class="block w-full rounded-md border-0 py-1.5 pl-9 pr-3 text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-white/10 placeholder:text-gray-400 focus:ring-2 focus:ring-blue-500 font-mono"
autocomplete="off"
spellcheck="false"
/>
@ -188,9 +186,7 @@
<%!-- Status summary + filter buttons --%>
<div class="flex items-center gap-2 text-xs font-medium">
<span class="text-cool-steel-500 dark:text-cool-steel-400 font-mono">
{@total_devices}
</span>
<span class="text-gray-500 dark:text-gray-400 font-mono">{@total_devices}</span>
<button
type="button"
phx-click="filter_status"
@ -214,12 +210,12 @@
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
@status_filter == "down" &&
"bg-sweet-salmon-100 dark:bg-sweet-salmon-900/40 text-sweet-salmon-800 dark:text-sweet-salmon-300 ring-1 ring-sweet-salmon-300 dark:ring-sweet-salmon-700",
"bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 ring-1 ring-red-300 dark:ring-red-700",
@status_filter != "down" &&
"text-sweet-salmon-700 dark:text-sweet-salmon-400 hover:bg-sweet-salmon-50 dark:hover:bg-sweet-salmon-900/20"
"text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20"
]}
>
<span class="inline-block h-2 w-2 rounded-full bg-sweet-salmon-500"></span>
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
{@total_down}
</button>
<button
@ -230,12 +226,12 @@
class={[
"inline-flex items-center gap-1 px-2 py-1 rounded-md transition-colors",
@status_filter == "unknown" &&
"bg-cool-steel-200 dark:bg-cool-steel-700 text-cool-steel-800 dark:text-cool-steel-200 ring-1 ring-cool-steel-400 dark:ring-cool-steel-600",
"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 ring-1 ring-gray-400 dark:ring-gray-600",
@status_filter != "unknown" &&
"text-cool-steel-500 dark:text-cool-steel-400 hover:bg-cool-steel-100 dark:hover:bg-cool-steel-800"
"text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
]}
>
<span class="inline-block h-2 w-2 rounded-full bg-cool-steel-400"></span>
<span class="inline-block h-2 w-2 rounded-full bg-gray-400"></span>
{@total_unknown}
</button>
<button
@ -243,7 +239,7 @@
type="button"
phx-click="filter_status"
phx-value-status="all"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300 ml-1"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 ml-1"
title={t("Clear filter")}
>
<.icon name="hero-x-mark" class="h-4 w-4" />
@ -262,7 +258,7 @@
id="device-list"
phx-update="ignore"
>
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th :if={@reorder_mode} scope="col" class="py-2 pl-4 pr-2 w-8 sm:pl-3">
<span class="sr-only">Drag</span>
@ -270,7 +266,7 @@
<th
scope="col"
class={[
"py-2 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400",
"py-2 pr-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400",
@reorder_mode && "pl-2",
!@reorder_mode && "pl-4 sm:pl-3"
]}
@ -279,31 +275,31 @@
</th>
<th
scope="col"
class="hidden sm:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th
scope="col"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Status")}
</th>
<th
scope="col"
class="hidden md:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th
scope="col"
class="hidden lg:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th
scope="col"
class="hidden lg:table-cell px-3 py-2 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-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")}
</th>
@ -311,20 +307,20 @@
</thead>
<tbody
id="device-rows"
class="divide-y divide-cool-steel-100 dark:divide-white/5"
class="divide-y divide-gray-100 dark:divide-white/5"
>
<%= for row <- @device_rows do %>
<%= if row.type == :site_header do %>
<tr
id={row.id}
class="bg-cool-steel-50/80 dark:bg-cool-steel-800/30"
class="bg-gray-50/80 dark:bg-gray-800/30"
data-site-id={if row.site, do: row.site.id, else: "no-site"}
draggable={if @reorder_mode && row.site, do: "true", else: "false"}
>
<td :if={@reorder_mode && row.site} class="py-1.5 pl-4 pr-2 sm:pl-3">
<button
type="button"
class="drag-handle cursor-grab active:cursor-grabbing text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Drag to reorder site"
>
<.icon name="hero-bars-3" class="h-4 w-4" />
@ -340,17 +336,17 @@
<%= if row.site do %>
<.link
navigate={~p"/sites/#{row.site.id}"}
class="font-semibold text-cool-steel-900 dark:text-white hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="font-semibold text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400"
>
{row.site.name}
</.link>
<% else %>
<span class="font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<span class="font-semibold text-gray-700 dark:text-gray-300">
{if @sites_enabled, do: "Unassigned", else: "All Devices"}
</span>
<% end %>
<span class="text-cool-steel-400 dark:text-cool-steel-600">·</span>
<span class="text-cool-steel-500 dark:text-cool-steel-400 font-mono">
<span class="text-gray-400 dark:text-gray-600">·</span>
<span class="text-gray-500 dark:text-gray-400 font-mono">
{row.stats.total}
</span>
<span
@ -363,14 +359,14 @@
</span>
<span
:if={row.stats.down > 0}
class="text-sweet-salmon-600 dark:text-sweet-salmon-400 font-medium"
class="text-red-600 dark:text-red-400 font-medium"
>
<span class="inline-block h-1.5 w-1.5 rounded-full bg-sweet-salmon-500">
<span class="inline-block h-1.5 w-1.5 rounded-full bg-red-500">
</span>
{row.stats.down}
</span>
<%= if row.site && @site_subscribers[row.site.id] do %>
<span class="text-wheat-600 dark:text-wheat-400">
<span class="text-amber-600 dark:text-amber-400">
<.icon name="hero-users" class="inline h-3 w-3" />
{@site_subscribers[row.site.id].account_count}
</span>
@ -378,7 +374,7 @@
</div>
<span
:if={row.site && row.site.location}
class="text-xs text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400"
class="text-xs text-gray-400 dark:text-gray-500 dark:text-gray-400"
>
<.icon name="hero-map-pin" class="inline h-3 w-3" /> {row.site.location}
</span>
@ -389,9 +385,8 @@
<tr
id={row.id}
class={[
"device-row hover:bg-cerulean-50/50 dark:hover:bg-cerulean-900/10 transition-colors cursor-pointer",
rem(row.device_index, 2) == 1 &&
"bg-cool-steel-50/40 dark:bg-cool-steel-800/20"
"device-row hover:bg-blue-50/50 dark:hover:bg-blue-900/10 transition-colors cursor-pointer",
rem(row.device_index, 2) == 1 && "bg-gray-50/40 dark:bg-gray-800/20"
]}
title={device_row_title(row.device)}
data-device-id={row.device.id}
@ -408,13 +403,13 @@
>
<button
type="button"
class="drag-handle cursor-grab active:cursor-grabbing text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="drag-handle cursor-grab active:cursor-grabbing text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
title="Drag to reorder device"
>
<.icon name="hero-bars-3" class="h-4 w-4" />
</button>
</td>
<td class="p-0 text-sm font-medium whitespace-nowrap text-cool-steel-900 dark:text-white">
<td class="p-0 text-sm font-medium whitespace-nowrap text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
@ -435,7 +430,7 @@
<.link
navigate={~p"/devices/#{row.device.id}"}
class={[
"block px-3 text-inherit no-underline font-mono text-xs text-cool-steel-600 dark:text-cool-steel-300",
"block px-3 text-inherit no-underline font-mono text-xs text-gray-600 dark:text-gray-300",
"py-2"
]}
>
@ -454,16 +449,15 @@
<span class={[
"inline-block h-2 w-2 rounded-full",
row.device.status == :up && "bg-green-500",
row.device.status == :down && "bg-sweet-salmon-500",
row.device.status == :unknown && "bg-cool-steel-400"
row.device.status == :down && "bg-red-500",
row.device.status == :unknown && "bg-gray-400"
]} />
<span class={[
"text-xs font-mono font-medium",
row.device.status == :up && "text-green-700 dark:text-green-400",
row.device.status == :down &&
"text-sweet-salmon-700 dark:text-sweet-salmon-400",
row.device.status == :down && "text-red-700 dark:text-red-400",
row.device.status == :unknown &&
"text-cool-steel-500 dark:text-cool-steel-400"
"text-gray-500 dark:text-gray-400"
]}>
{row.device.status |> to_string() |> String.upcase()}
</span>
@ -496,7 +490,7 @@
<% bar_w = response_time_bar_width(row.response) %>
<div
:if={bar_w > 0}
class="w-16 h-1.5 bg-cool-steel-100 dark:bg-cool-steel-700 rounded-full overflow-hidden"
class="w-16 h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden"
>
<div
class={"h-full rounded-full #{response_time_bar_color(row.response)}"}
@ -515,14 +509,12 @@
]}
>
<%= if sub_text = format_subscriber_count(row.subscribers) do %>
<span class="inline-flex items-center gap-1 text-xs text-cerulean-700 dark:text-cerulean-300">
<span class="inline-flex items-center gap-1 text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-users" class="h-3 w-3" />
{sub_text}
</span>
<% else %>
<span class="text-xs text-cool-steel-300 dark:text-cool-steel-600">
</span>
<span class="text-xs text-gray-300 dark:text-gray-600">—</span>
<% end %>
</.link>
</td>
@ -540,17 +532,17 @@
<div class="text-center py-16">
<.icon
name="hero-magnifying-glass"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500 dark:text-cool-steel-400"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500 dark:text-gray-400"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No discovered devices")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Devices will appear here as they are discovered via LLDP/CDP.")}
</p>
</div>
<% else %>
<div class="mb-4 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Showing {(@pagination.page - 1) * @pagination.per_page + 1}-{min(
@pagination.page * @pagination.per_page,
@pagination.total_count
@ -565,16 +557,16 @@
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
MAC
</span>
<span class="ml-2 font-mono text-sm text-cool-steel-900 dark:text-white">
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
<% :ip -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900 dark:text-cerulean-200">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
IP
</span>
<span class="ml-2 font-mono text-sm text-cool-steel-900 dark:text-white">
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
@ -583,56 +575,52 @@
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
HOST
</span>
<span class="ml-2 text-sm text-cool-steel-900 dark:text-white">
<span class="ml-2 text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
</span>
</div>
<% _ -> %>
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">Unknown</span>
<span class="text-sm text-gray-500 dark:text-gray-400">Unknown</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Hostname")}>
<%= if discovered.hostname do %>
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{discovered.hostname}
</span>
<% else %>
<span class="text-sm text-cool-steel-400">-</span>
<span class="text-sm text-gray-400">-</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Type")}>
<div class="flex items-center gap-2">
<.icon
name="hero-server"
class="h-4 w-4 text-cool-steel-500 dark:text-cool-steel-400"
class="h-4 w-4 text-gray-500 dark:text-gray-400"
/>
<span class="text-sm text-cool-steel-900 dark:text-white">
<span class="text-sm text-gray-900 dark:text-white">
{device_type_label(discovered.device_type)}
</span>
</div>
</:col>
<:col :let={discovered} label={t("Manufacturer")}>
<%= if discovered.manufacturer do %>
<span class="text-sm text-cool-steel-900 dark:text-white">
{discovered.manufacturer}
</span>
<span class="text-sm text-gray-900 dark:text-white">{discovered.manufacturer}</span>
<% else %>
<span class="text-sm text-cool-steel-400">-</span>
<span class="text-sm text-gray-400">-</span>
<% end %>
</:col>
<:col :let={discovered} label={t("Discovered By")}>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="text-sm text-gray-600 dark:text-gray-400">
<%= if length(discovered.discovered_by) == 1 do %>
<% [first] = discovered.discovered_by %>
<.link
navigate={~p"/devices/#{first.device_id}"}
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{first.device_name}
</.link>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
({first.interface})
</span>
<span class="text-xs text-gray-500 dark:text-gray-400"> ({first.interface})</span>
<% else %>
<span>{length(discovered.discovered_by)} devices</span>
<% end %>
@ -641,7 +629,7 @@
<:col :let={discovered} label={t("Last Seen")}>
<.timestamp
datetime={discovered.last_seen}
class="text-sm text-cool-steel-600 dark:text-cool-steel-400"
class="text-sm text-gray-600 dark:text-gray-400"
/>
</:col>
<:col :let={discovered} label={t("Actions")}>
@ -656,25 +644,25 @@
</.table>
<%= if @pagination.total_pages > 1 do %>
<div class="mt-6 flex items-center justify-between border-t border-cool-steel-200 dark:border-white/10 pt-6">
<div class="mt-6 flex items-center justify-between border-t border-gray-200 dark:border-white/10 pt-6">
<div class="flex flex-1 justify-between sm:hidden">
<.link
:if={@pagination.page > 1}
patch={~p"/devices?tab=discovered&page=#{@pagination.page - 1}"}
class="relative inline-flex items-center rounded-md border border-cool-steel-300 bg-white px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Previous")}
</.link>
<.link
:if={@pagination.page < @pagination.total_pages}
patch={~p"/devices?tab=discovered&page=#{@pagination.page + 1}"}
class="relative ml-3 inline-flex items-center rounded-md border border-cool-steel-300 bg-white px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Next")}
</.link>
</div>
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<p class="text-sm text-cool-steel-700 dark:text-cool-steel-400">
<p class="text-sm text-gray-700 dark:text-gray-400">
<span class="font-medium">{(@pagination.page - 1) * @pagination.per_page + 1}</span><span class="font-medium">{min(@pagination.page * @pagination.per_page, @pagination.total_count)}</span> of
<span class="font-medium">{@pagination.total_count}</span>
</p>
@ -685,24 +673,24 @@
<.link
:if={@pagination.page > 1}
patch={~p"/devices?tab=discovered&page=#{@pagination.page - 1}"}
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-cool-steel-400 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:ring-white/10 dark:hover:bg-cool-steel-800"
class="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<.icon name="hero-chevron-left" class="h-5 w-5" />
</.link>
<%= for page_num <- pagination_range(@pagination.page, @pagination.total_pages) do %>
<%= if page_num == :ellipsis do %>
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-cool-steel-700 ring-1 ring-inset ring-cool-steel-300 dark:text-cool-steel-400 dark:ring-white/10">
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300 dark:text-gray-400 dark:ring-white/10">
...
</span>
<% else %>
<.link
patch={~p"/devices?tab=discovered&page=#{page_num}"}
class={[
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-cool-steel-300 dark:ring-white/10",
"relative inline-flex items-center px-4 py-2 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-white/10",
if @pagination.page == page_num do
"z-10 bg-cerulean-600 text-white"
"z-10 bg-blue-600 text-white"
else
"text-cool-steel-900 hover:bg-cool-steel-50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"text-gray-900 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
end
]}
>
@ -713,7 +701,7 @@
<.link
:if={@pagination.page < @pagination.total_pages}
patch={~p"/devices?tab=discovered&page=#{@pagination.page + 1}"}
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-cool-steel-400 ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:ring-white/10 dark:hover:bg-cool-steel-800"
class="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:ring-white/10 dark:hover:bg-gray-800"
>
<.icon name="hero-chevron-right" class="h-5 w-5" />
</.link>

View file

@ -632,8 +632,8 @@ defmodule ToweropsWeb.DeviceLive.Show do
# Additional template-used formatting functions (not yet in Formatters module)
@doc false
def utilization_text_color(pct) when pct >= 90, do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def utilization_text_color(pct) when pct >= 70, do: "text-wheat-600 dark:text-wheat-400"
def utilization_text_color(pct) when pct >= 90, do: "text-red-600 dark:text-red-400"
def utilization_text_color(pct) when pct >= 70, do: "text-yellow-600 dark:text-yellow-400"
def utilization_text_color(_pct), do: "text-green-600 dark:text-green-400"
@doc false
@ -1023,7 +1023,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
assigns = %{}
~H"""
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
WARNING
</span>
"""
@ -1033,7 +1033,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
assigns = %{}
~H"""
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
CRITICAL
</span>
"""
@ -1043,7 +1043,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
assigns = %{}
~H"""
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-900/30 dark:text-cool-steel-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
UNKNOWN
</span>
"""
@ -1053,7 +1053,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
assigns = %{}
~H"""
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-900/30 dark:text-cool-steel-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
PENDING
</span>
"""
@ -1115,8 +1115,8 @@ defmodule ToweropsWeb.DeviceLive.Show do
defp config_change_dot_color(event) do
cond do
event.change_size > 50 -> "bg-sweet-salmon-500"
event.change_size > 20 -> "bg-wheat-500"
event.change_size > 50 -> "bg-red-500"
event.change_size > 20 -> "bg-yellow-500"
true -> "bg-green-500"
end
end

File diff suppressed because it is too large Load diff

View file

@ -7,11 +7,11 @@
<div class="flex items-center gap-3 mb-6">
<.link
navigate={~p"/schedules?tab=escalation-policies"}
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-arrow-left" class="h-5 w-5" />
</.link>
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">{@page_title}</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@page_title}</h1>
</div>
<div class="max-w-2xl">
@ -34,13 +34,13 @@
]}
/>
</div>
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 flex items-center gap-3">
<.icon name="hero-arrow-path" class="h-5 w-5 text-cool-steel-500" />
<span class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
<div class="rounded-lg border border-gray-200 dark:border-white/10 p-4 flex items-center gap-3">
<.icon name="hero-arrow-path" class="h-5 w-5 text-gray-500" />
<span class="text-sm text-gray-700 dark:text-gray-300">
{t("If no one acknowledges, repeat this policy")}
</span>
<.input field={@form[:repeat_count]} type="number" min="1" max="10" class="w-20" />
<span class="text-sm text-cool-steel-700 dark:text-cool-steel-300">{t("time(s).")}</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("time(s).")}</span>
</div>
<div class="flex items-center gap-3">
<.button type="submit" phx-disable-with={t("Saving...")}>
@ -48,7 +48,7 @@
</.button>
<.link
navigate={~p"/schedules?tab=escalation-policies"}
class="text-sm text-cool-steel-600 dark:text-cool-steel-400 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</.link>

View file

@ -8,17 +8,15 @@
<div class="flex items-center gap-3">
<.link
navigate={~p"/schedules"}
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-arrow-left" class="h-5 w-5" />
</.link>
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">
{t("Escalation Policies")}
</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("Escalation Policies")}</h1>
</div>
<.link
navigate={~p"/schedules/escalation-policies/new"}
class="inline-flex items-center gap-2 rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Policy")}
@ -30,39 +28,39 @@
<div class="text-center">
<.icon
name="hero-arrow-trending-up"
class="h-12 w-12 text-cool-steel-300 dark:text-cool-steel-600 mx-auto mb-4"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No escalation policies")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Create an escalation policy to define how alerts are escalated to on-call users.")}
</p>
</div>
</div>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Name")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Levels")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-900">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
<%= for policy <- @policies do %>
<tr
class="hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/30 transition-colors cursor-pointer"
class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors cursor-pointer"
phx-click={JS.navigate(~p"/schedules/escalation-policies/#{policy.id}")}
>
<td class="px-4 py-3 text-sm font-medium text-cool-steel-900 dark:text-white">
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
{policy.name}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{length(policy.rules)}
</td>
</tr>

View file

@ -8,16 +8,16 @@
<div class="flex items-center gap-3">
<.link
navigate={~p"/schedules?tab=escalation-policies"}
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-arrow-left" class="h-5 w-5" />
</.link>
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">{@policy.name}</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@policy.name}</h1>
</div>
<div class="flex items-center gap-2">
<.link
navigate={~p"/schedules/escalation-policies/#{@policy.id}/edit"}
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 border border-cool-steel-300 dark:border-cool-steel-600 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-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" />
{t("Edit")}
@ -25,7 +25,7 @@
<button
phx-click="delete"
data-confirm={t("Are you sure you want to delete this escalation policy?")}
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm font-medium text-sweet-salmon-600 dark:text-sweet-salmon-400 border border-cool-steel-300 dark:border-cool-steel-600 hover:bg-sweet-salmon-50 dark:hover:bg-sweet-salmon-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" />
{t("Delete")}
@ -33,23 +33,23 @@
</div>
</div>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
{t("Send On-Call Handoff Notifications:")}
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{handoff_mode_label(@policy.handoff_notifications_mode)}
</span>
</p>
<%= if @policy.description do %>
<p class="mb-6 text-sm text-cool-steel-600 dark:text-cool-steel-400">{@policy.description}</p>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">{@policy.description}</p>
<% end %>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<%!-- LEFT: Levels timeline --%>
<div class="lg:col-span-2 space-y-3">
<%= if Enum.empty?(@policy.rules) do %>
<div class="rounded-lg border border-dashed border-cool-steel-300 dark:border-cool-steel-600 p-6 text-center">
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-6 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No levels configured. Add a level to define escalation steps.")}
</p>
</div>
@ -66,12 +66,12 @@
<div
data-id={rule.id}
draggable="true"
class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4 cursor-move"
class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 cursor-move"
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<.icon name="hero-bars-3" class="h-4 w-4 text-cool-steel-400" />
<h3 class="text-base font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-bars-3" class="h-4 w-4 text-gray-400" />
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
{t("Level %{n}", n: idx + 1)}
</h3>
</div>
@ -80,7 +80,7 @@
phx-click="move_rule"
phx-value-id={rule.id}
phx-value-direction="up"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300 disabled:opacity-30 disabled:hover:text-cool-steel-400"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
disabled={idx == 0}
title={t("Move up")}
>
@ -90,7 +90,7 @@
phx-click="move_rule"
phx-value-id={rule.id}
phx-value-direction="down"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300 disabled:opacity-30 disabled:hover:text-cool-steel-400"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
disabled={idx == rule_count - 1}
title={t("Move down")}
>
@ -100,7 +100,7 @@
phx-click="delete_rule"
phx-value-id={rule.id}
data-confirm={t("Delete this level?")}
class="text-cool-steel-400 hover:text-sweet-salmon-500 dark:hover:text-sweet-salmon-400 transition-colors ml-1"
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors ml-1"
title={t("Delete")}
>
<.icon name="hero-trash" class="h-4 w-4" />
@ -108,9 +108,9 @@
</div>
</div>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-3">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
{t("Notify the following users immediately and escalate after")}
<span class="font-semibold text-cool-steel-900 dark:text-white">
<span class="font-semibold text-gray-900 dark:text-white">
{rule.timeout_minutes}
</span>
{t("minutes.")}
@ -119,13 +119,13 @@
<%!-- Targets --%>
<div>
<%= if Enum.empty?(rule.targets) do %>
<p class="text-xs text-cool-steel-400 dark:text-cool-steel-500 mb-2">
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">
{t("No targets yet")}
</p>
<% else %>
<div class="flex flex-wrap gap-2 mb-2">
<%= for target <- rule.targets do %>
<span class="inline-flex items-center gap-1 rounded-full bg-cool-steel-100 dark:bg-cool-steel-800 pl-2.5 pr-1 py-0.5 text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300">
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-800 pl-2.5 pr-1 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">
<%= case target.target_type do %>
<% "user" -> %>
<.icon name="hero-user" class="h-3 w-3 mr-0.5" />
@ -139,7 +139,7 @@
<button
phx-click="delete_target"
phx-value-id={target.id}
class="ml-0.5 rounded-full p-0.5 hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700 transition-colors"
class="ml-0.5 rounded-full p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<.icon name="hero-x-mark" class="h-3 w-3" />
</button>
@ -154,14 +154,14 @@
<select
name="target_type"
id={"target-type-#{rule.id}"}
class="block rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-xs py-1.5"
class="block rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1.5"
>
<option value="user">{t("User")}</option>
<option value="schedule">{t("Schedule")}</option>
</select>
<select
name="target_id"
class="block w-full max-w-xs rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-xs py-1.5"
class="block w-full max-w-xs rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1.5"
>
<option value="">{t("Select...")}</option>
<optgroup label={t("Users")}>
@ -177,7 +177,7 @@
</select>
<button
type="submit"
class="inline-flex items-center rounded-md bg-cool-steel-100 dark:bg-cool-steel-800 px-2.5 py-1.5 text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700 transition-colors"
class="inline-flex items-center rounded-md bg-gray-100 dark:bg-gray-800 px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<.icon name="hero-plus" class="h-3 w-3 mr-1" />
{t("Add")}
@ -191,11 +191,11 @@
<%!-- Add Level button + form --%>
<%= if @show_add_rule do %>
<div class="rounded-lg border border-cerulean-200 dark:border-cerulean-800 bg-cerulean-50/50 dark:bg-cerulean-900/10 p-4">
<div class="rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-4">
<form phx-submit="save_rule">
<div class="flex items-end gap-4 mb-4">
<div class="flex-1">
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Escalate after (minutes)")}
</label>
<input
@ -204,7 +204,7 @@
value="30"
min="1"
max="1440"
class="block w-full rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-sm"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm"
/>
</div>
</div>
@ -215,7 +215,7 @@
<button
type="button"
phx-click="toggle_add_rule"
class="text-sm text-cool-steel-600 dark:text-cool-steel-400 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</button>
@ -225,16 +225,16 @@
<% else %>
<button
phx-click="toggle_add_rule"
class="w-full rounded-lg border border-dashed border-cool-steel-300 dark:border-cool-steel-600 p-3 text-sm text-cool-steel-500 dark:text-cool-steel-400 hover:border-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200 transition-colors"
class="w-full rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-3 text-sm text-gray-500 dark:text-gray-400 hover:border-gray-400 hover:text-gray-700 dark:hover:text-gray-200 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4 inline mr-1" /> {t("Add Level")}
</button>
<% end %>
<%!-- REPEAT footer --%>
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-cool-steel-50 dark:bg-cool-steel-800/40 p-4 flex items-center gap-3">
<.icon name="hero-arrow-path" class="h-5 w-5 text-cool-steel-500" />
<p class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-800/40 p-4 flex items-center gap-3">
<.icon name="hero-arrow-path" class="h-5 w-5 text-gray-500" />
<p class="text-sm text-gray-700 dark:text-gray-300">
{t("If no one acknowledges, repeat this policy")}
<span class="font-semibold">{@policy.repeat_count}</span>
{t("time(s).")}
@ -244,12 +244,12 @@
<%!-- RIGHT: Services sidebar --%>
<aside class="lg:col-span-1">
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white mb-3">
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
{t("Services (%{count})", count: length(@services))}
</h3>
<%= if @services == [] do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No devices use this policy yet.")}
</p>
<% else %>
@ -258,7 +258,7 @@
<li>
<.link
navigate={~p"/devices/#{device.id}"}
class="text-sm text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
{device.name}
</.link>

View file

@ -10,21 +10,19 @@
<div class="flex items-center gap-4">
<.link
navigate={~p"/devices/#{@device_id}"}
class="text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-5 w-5" />
</.link>
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{@chart_title}</h1>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">{@device.name}</p>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@chart_title}</h1>
<p class="text-sm text-gray-600 dark:text-gray-400">{@device.name}</p>
</div>
</div>
</div>
<!-- Date Range Selector -->
<div class="flex items-center gap-2 mb-6">
<span class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300">
Time Range:
</span>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Time Range:</span>
<div class="flex gap-1">
<%= for {label, value} <- [{"Live", "live"}, {"1 Hour", "1h"}, {"6 Hours", "6h"}, {"12 Hours", "12h"}, {"24 Hours", "24h"}, {"7 Days", "7d"}, {"30 Days", "30d"}] do %>
<button
@ -40,9 +38,9 @@
end
else
if @range == value do
"bg-cerulean-500 text-white border-cerulean-500"
"bg-blue-500 text-white border-blue-500"
else
"bg-white dark:bg-cool-steel-800/50 text-cool-steel-700 dark:text-cool-steel-300 border-cool-steel-300 dark:border-white/10 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800"
"bg-white dark:bg-gray-800/50 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800"
end
end
]}
@ -54,7 +52,7 @@
</div>
<!-- Live Mode Indicator -->
<%= if @is_live_mode do %>
<div class="flex items-center gap-2 text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 mb-4">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75">
</span>
@ -65,7 +63,7 @@
<% end %>
<!-- Large Chart -->
<%= if @chart_data do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="p-6">
<div
id="detail-chart"
@ -82,16 +80,16 @@
<canvas></canvas>
</div>
<%= if @max_value && @min_value do %>
<div class="mt-4 pt-4 border-t border-cool-steel-200 dark:border-white/10 flex justify-center gap-8 text-sm">
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-white/10 flex justify-center gap-8 text-sm">
<div class="flex items-center gap-2">
<span class="font-medium text-cool-steel-700 dark:text-cool-steel-300">Max:</span>
<span class="text-cool-steel-900 dark:text-white font-semibold">
<span class="font-medium text-gray-700 dark:text-gray-300">Max:</span>
<span class="text-gray-900 dark:text-white font-semibold">
{format_value(@max_value, @unit)}
</span>
</div>
<div class="flex items-center gap-2">
<span class="font-medium text-cool-steel-700 dark:text-cool-steel-300">Min:</span>
<span class="text-cool-steel-900 dark:text-white font-semibold">
<span class="font-medium text-gray-700 dark:text-gray-300">Min:</span>
<span class="text-gray-900 dark:text-white font-semibold">
{format_value(@min_value, @unit)}
</span>
</div>
@ -100,13 +98,13 @@
</div>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-12">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-chart-bar" class="mx-auto h-12 w-12 text-cool-steel-400" />
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-chart-bar" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No sensor data available")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any sensors of this type.")}
</p>
</div>

File diff suppressed because it is too large Load diff

View file

@ -182,23 +182,18 @@ defmodule ToweropsWeb.InsightsLive.Index do
end
@doc false
def urgency_classes("critical"),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def urgency_classes("warning"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def urgency_classes("info"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def urgency_classes(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
def urgency_classes("critical"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
def urgency_classes("warning"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def urgency_classes("info"), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
def urgency_classes(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
@doc false
def source_classes("preseem"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def source_classes("preseem"), do: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-400"
def source_classes("gaiia"), do: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400"
def source_classes("snmp"), do: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
def source_classes("system"),
do: "bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-700 dark:text-cool-steel-300"
def source_classes("system"), do: "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"
def source_classes("ai"), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
def source_classes(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
def source_classes(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
@doc """
Display label for a source. Acronyms render uppercase; everything else

View file

@ -4,12 +4,12 @@
active_page="insights"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="flex items-start justify-between gap-4 border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="flex items-start justify-between gap-4 border-b border-gray-200 pb-5 dark:border-white/5">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Network Insights")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Proactive network health observations from all sources.")}
</p>
</div>
@ -20,7 +20,7 @@
id="regenerate-insights-btn"
phx-click="regenerate_insights"
data-confirm={t("Queue insight regeneration jobs for all sources?")}
class="inline-flex items-center gap-1.5 rounded-md bg-cerulean-600 px-3 py-2 text-sm font-medium text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="inline-flex items-center gap-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4" />
{t("Regenerate insights")}
@ -29,7 +29,7 @@
</div>
<%!-- Filter bar --%>
<div class="mt-6 flex flex-wrap items-center gap-4 border-b border-cool-steel-200 pb-4 dark:border-white/10">
<div class="mt-6 flex flex-wrap items-center gap-4 border-b border-gray-200 pb-4 dark:border-white/10">
<%!-- Status filter tabs --%>
<nav class="flex space-x-4">
<.link
@ -40,10 +40,9 @@
class={[
"rounded-md px-3 py-1.5 text-sm font-medium",
if(@filter_status == "active",
do:
"bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
)
]}
>
@ -57,10 +56,9 @@
class={[
"rounded-md px-3 py-1.5 text-sm font-medium",
if(@filter_status == "dismissed",
do:
"bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
)
]}
>
@ -69,10 +67,8 @@
</nav>
<%!-- Source filter pills --%>
<div class="flex items-center gap-2 border-l border-cool-steel-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400">
Source:
</span>
<div class="flex items-center gap-2 border-l border-gray-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Source:</span>
<.link
id="filter-source-all"
patch={
@ -81,9 +77,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(is_nil(@filter_source),
do: "bg-cool-steel-200 text-cool-steel-800 dark:bg-white/20 dark:text-white",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -98,9 +93,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(@filter_source == source,
do: "bg-cool-steel-200 text-cool-steel-800 dark:bg-white/20 dark:text-white",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -109,10 +103,8 @@
</div>
<%!-- Urgency filter --%>
<div class="flex items-center gap-2 border-l border-cool-steel-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400">
Urgency:
</span>
<div class="flex items-center gap-2 border-l border-gray-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Urgency:</span>
<.link
id="filter-urgency-all"
patch={
@ -121,9 +113,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(is_nil(@filter_urgency),
do: "bg-cool-steel-200 text-cool-steel-800 dark:bg-white/20 dark:text-white",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -132,11 +123,10 @@
<.link
:for={
{urgency, classes} <- [
{"critical",
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"},
{"warning", "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"},
{"info",
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"}
{"critical", "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"},
{"warning",
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"},
{"info", "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"}
]
}
id={"filter-urgency-#{urgency}"}
@ -147,8 +137,7 @@
"rounded-md px-2 py-1 text-xs font-medium capitalize",
if(@filter_urgency == urgency,
do: classes,
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -161,16 +150,16 @@
<%= if any_selected?(@selected_ids) do %>
<div
id="bulk-actions"
class="mt-4 flex items-center gap-3 rounded-lg bg-cerulean-50 px-4 py-3 dark:bg-cerulean-900/20"
class="mt-4 flex items-center gap-3 rounded-lg bg-indigo-50 px-4 py-3 dark:bg-indigo-900/20"
>
<span class="text-sm font-medium text-cerulean-700 dark:text-cerulean-200">
<span class="text-sm font-medium text-indigo-700 dark:text-indigo-200">
{MapSet.size(@selected_ids)} selected
</span>
<button
type="button"
id="deselect-all-btn"
phx-click="deselect_all"
class="text-sm text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Deselect All")}
</button>
@ -178,7 +167,7 @@
type="button"
id="bulk-dismiss-btn"
phx-click="bulk_dismiss"
class="rounded-md bg-cerulean-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Dismiss Selected")}
</button>
@ -191,16 +180,16 @@
<div id="empty-state" class="flex items-center justify-center py-8">
<div class="card shadow-md border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 max-w-md w-full">
<div class="items-center text-center">
<div class="rounded-full bg-wheat-100 dark:bg-wheat-900/40 p-4 mb-2">
<div class="rounded-full bg-amber-100 dark:bg-amber-900/40 p-4 mb-2">
<.icon
name="hero-light-bulb-solid"
class="h-12 w-12 text-wheat-500 dark:text-wheat-400"
class="h-12 w-12 text-amber-500 dark:text-amber-400"
/>
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("No Insights Yet")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
<%= cond do %>
<% @filter_status == "dismissed" -> %>
{t("No dismissed insights.")}
@ -227,7 +216,7 @@
type="button"
id="select-all-btn"
phx-click="select_all"
class="text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
{t("Select All")}
</button>
@ -238,11 +227,11 @@
:for={insight <- @insights}
id={"insight-#{insight.id}"}
class={[
"rounded-lg border bg-white p-4 transition-shadow hover:shadow-md dark:bg-cool-steel-800/50",
"rounded-lg border bg-white p-4 transition-shadow hover:shadow-md dark:bg-gray-800/50",
if(selected?(@selected_ids, insight.id),
do:
"border-cerulean-300 ring-1 ring-cerulean-200 dark:border-cerulean-600 dark:ring-cerulean-800",
else: "border-cool-steel-200 dark:border-white/10"
"border-indigo-300 ring-1 ring-indigo-200 dark:border-indigo-600 dark:ring-indigo-800",
else: "border-gray-200 dark:border-white/10"
)
]}
>
@ -259,9 +248,8 @@
"flex h-5 w-5 items-center justify-center rounded border",
if(selected?(@selected_ids, insight.id),
do:
"border-cerulean-600 bg-cerulean-600 dark:border-cerulean-500 dark:bg-cerulean-500",
else:
"border-cool-steel-300 bg-white dark:border-cool-steel-600 dark:bg-cool-steel-700"
"border-indigo-600 bg-indigo-600 dark:border-indigo-500 dark:bg-indigo-500",
else: "border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-700"
)
]}>
<%= if selected?(@selected_ids, insight.id) do %>
@ -290,29 +278,29 @@
<%!-- AI badge — present on any LLM-enriched insight --%>
<%= if insight.llm_enriched_at do %>
<span class="inline-flex items-center rounded-full bg-cerulean-600 px-2 py-0.5 text-xs font-semibold text-white dark:bg-cerulean-500">
<span class="inline-flex items-center rounded-full bg-blue-600 px-2 py-0.5 text-xs font-semibold text-white dark:bg-blue-500">
AI
</span>
<% end %>
<%!-- Type badge --%>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2 py-0.5 text-xs font-medium text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{String.replace(insight.type, "_", " ")}
</span>
</div>
<h3 class="mt-1 text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
{insight.title}
</h3>
<%= if insight.description do %>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{insight.description}
</p>
<% end %>
<%= if insight.llm_summary do %>
<div class="mt-2 rounded-md border border-cerulean-200 bg-cerulean-50 px-3 py-2 text-sm text-cerulean-900 dark:border-cerulean-900/40 dark:bg-cerulean-950/40 dark:text-cerulean-100">
<div class="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-900 dark:border-blue-900/40 dark:bg-blue-950/40 dark:text-blue-100">
<p class="font-medium">{t("Summary")}</p>
<p>{insight.llm_summary}</p>
<%= if insight.recommended_action do %>
@ -321,7 +309,7 @@
{insight.recommended_action}
</p>
<% end %>
<p class="mt-1 text-xs text-cerulean-700 dark:text-cerulean-300">
<p class="mt-1 text-xs text-blue-700 dark:text-blue-300">
AI-generated
</p>
</div>
@ -329,8 +317,8 @@
<%!-- Affected devices list (from metadata) --%>
<%= if insight.metadata["device_names"] && length(insight.metadata["device_names"]) > 0 do %>
<div class="mt-2 rounded-md bg-cool-steel-50 p-3 dark:bg-white/5">
<p class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300">
<div class="mt-2 rounded-md bg-gray-50 p-3 dark:bg-white/5">
<p class="text-xs font-medium text-gray-700 dark:text-gray-300">
{t("Affected devices:")}
</p>
<div class="mt-1.5 flex flex-wrap gap-1.5">
@ -340,18 +328,18 @@
navigate={
~p"/devices/#{Enum.at(insight.metadata["device_ids"], idx)}"
}
class="inline-flex items-center rounded-md bg-white px-2 py-0.5 text-xs font-medium text-cerulean-700 ring-1 ring-inset ring-cerulean-700/10 hover:bg-cerulean-50 dark:bg-cerulean-900/20 dark:text-cerulean-400 dark:ring-cerulean-400/20 dark:hover:bg-cerulean-900/40"
class="inline-flex items-center rounded-md bg-white px-2 py-0.5 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 hover:bg-indigo-50 dark:bg-indigo-900/20 dark:text-indigo-400 dark:ring-indigo-400/20 dark:hover:bg-indigo-900/40"
>
{name || "Unnamed"}
</.link>
<% else %>
<span class="inline-flex items-center rounded-md bg-white px-2 py-0.5 text-xs font-medium text-cool-steel-600 ring-1 ring-inset ring-cool-steel-500/10 dark:bg-white/10 dark:text-cool-steel-300 dark:ring-white/10">
<span class="inline-flex items-center rounded-md bg-white px-2 py-0.5 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10">
{name || "Unnamed"}
</span>
<% end %>
<% end %>
<%= if insight.metadata["count"] && insight.metadata["count"] > length(insight.metadata["device_names"]) do %>
<span class="inline-flex items-center rounded-md bg-cool-steel-100 px-2 py-0.5 text-xs text-cool-steel-500 dark:bg-white/10 dark:text-cool-steel-400">
<span class="inline-flex items-center rounded-md bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-white/10 dark:text-gray-400">
+{insight.metadata["count"] - length(insight.metadata["device_names"])} more
</span>
<% end %>
@ -361,8 +349,8 @@
<%!-- Actionable recommendations --%>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "untracked" do %>
<div class="mt-3 rounded-md border border-cerulean-200 bg-cerulean-50 p-3 dark:border-cerulean-800 dark:bg-cerulean-900/20">
<p class="text-xs font-medium text-cerulean-800 dark:text-cerulean-300 mb-2">
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
{t("To fix this:")}
</p>
@ -371,7 +359,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
}
class="inline-flex items-center gap-1 rounded-md bg-cerulean-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-cerulean-700"
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-blue-700"
>
<.icon name="hero-arrows-right-left" class="h-3.5 w-3.5" />
{t("Link devices to Gaiia")}
@ -380,7 +368,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
}
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-cool-steel-700 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-cool-steel-300 dark:ring-white/10 dark:hover:bg-white/20"
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
>
<.icon name="hero-table-cells" class="h-3.5 w-3.5" />
{t("View comparison report")}
@ -390,8 +378,8 @@
<% end %>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "ghost" do %>
<div class="mt-3 rounded-md border border-wheat-200 bg-wheat-50 p-3 dark:border-wheat-800 dark:bg-wheat-900/20">
<p class="text-xs font-medium text-wheat-800 dark:text-wheat-200 mb-2">
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<p class="text-xs font-medium text-amber-800 dark:text-amber-200 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
{t("To fix this:")}
</p>
@ -400,7 +388,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
}
class="inline-flex items-center gap-1 rounded-md bg-wheat-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-wheat-700"
class="inline-flex items-center gap-1 rounded-md bg-amber-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-amber-700"
>
<.icon name="hero-trash" class="h-3.5 w-3.5" />
{t("Remove stale device links")}
@ -410,8 +398,8 @@
<% end %>
<%= if insight.source == "gaiia" && insight.metadata["finding_type"] == "mismatch" do %>
<div class="mt-3 rounded-md border border-cerulean-200 bg-cerulean-50 p-3 dark:border-cerulean-800 dark:bg-cerulean-900/20">
<p class="text-xs font-medium text-cerulean-800 dark:text-cerulean-300 mb-2">
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
{t("To fix this:")}
</p>
@ -420,7 +408,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
}
class="inline-flex items-center gap-1 rounded-md bg-cerulean-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-cerulean-700"
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-blue-700"
>
<.icon name="hero-table-cells" class="h-3.5 w-3.5" />
{t("Review conflicting data")}
@ -559,24 +547,24 @@
<% end %>
<%= if insight.type == "upstream_degradation" do %>
<div class="mt-3 rounded-md border border-cerulean-200 bg-cerulean-50 p-3 dark:border-cerulean-700 dark:bg-cerulean-950/40">
<div class="mt-3 rounded-md border border-indigo-200 bg-indigo-50 p-3 dark:border-indigo-700 dark:bg-indigo-950/40">
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col">
<span class="text-xs font-medium text-cerulean-700 dark:text-cerulean-200">
<span class="text-xs font-medium text-indigo-700 dark:text-indigo-200">
{t("Degraded APs")}
</span>
<span class="font-mono text-base font-semibold text-cerulean-900 dark:text-white">
<span class="font-mono text-base font-semibold text-indigo-900 dark:text-white">
{insight.metadata["degraded_ap_count"]} / {insight.metadata[
"total_ap_count"
]}
</span>
</div>
<span class="ml-auto rounded-md bg-cerulean-600 px-2 py-1 text-xs font-semibold text-white dark:bg-cerulean-500">
<span class="ml-auto rounded-md bg-indigo-600 px-2 py-1 text-xs font-semibold text-white dark:bg-indigo-500">
{t("Investigate upstream first")}
</span>
</div>
<%= if is_list(insight.metadata["degraded_aps"]) do %>
<ul class="mt-2 space-y-1 text-xs text-cerulean-900 dark:text-cerulean-50">
<ul class="mt-2 space-y-1 text-xs text-indigo-900 dark:text-indigo-50">
<li
:for={ap <- insight.metadata["degraded_aps"]}
class="flex items-center justify-between gap-2"
@ -585,7 +573,7 @@
<%= if ap["device_id"] do %>
<.link
navigate={~p"/devices/#{ap["device_id"]}"}
class="text-cerulean-700 underline hover:text-cerulean-900 dark:text-cerulean-200 dark:hover:text-white"
class="text-indigo-700 underline hover:text-indigo-900 dark:text-indigo-200 dark:hover:text-white"
>
{ap["name"] || "Unnamed"}
</.link>
@ -593,7 +581,7 @@
{ap["name"] || "Unnamed"}
<% end %>
</span>
<span class="font-mono text-cerulean-800 dark:text-cerulean-100">
<span class="font-mono text-indigo-800 dark:text-indigo-100">
QoE {Float.round(ap["qoe_score"] / 1.0, 1)}
</span>
</li>
@ -603,33 +591,33 @@
<% end %>
<%= if insight.type == "device_overheating" do %>
<div class="mt-3 rounded-md border border-sweet-salmon-200 bg-sweet-salmon-50 p-3 dark:border-sweet-salmon-800 dark:bg-sweet-salmon-900/20">
<div class="mt-3 rounded-md border border-red-200 bg-red-50 p-3 dark:border-red-800 dark:bg-red-900/20">
<div class="flex flex-wrap items-center gap-4 text-sm">
<div class="flex flex-col">
<span class="text-xs text-sweet-salmon-700 dark:text-sweet-salmon-200">
<span class="text-xs text-red-700 dark:text-red-200">
{t("Hottest sensor")}
</span>
<span class="font-mono text-base font-semibold text-sweet-salmon-900 dark:text-sweet-salmon-100">
<span class="font-mono text-base font-semibold text-red-900 dark:text-red-100">
{insight.metadata["temperature_c"]} °C
</span>
<%= if insight.metadata["sensor_descr"] do %>
<span class="text-xs text-sweet-salmon-700 dark:text-sweet-salmon-200">
<span class="text-xs text-red-700 dark:text-red-200">
{insight.metadata["sensor_descr"]}
</span>
<% end %>
</div>
<%= if insight.metadata["qoe_score"] do %>
<div class="flex flex-col">
<span class="text-xs text-sweet-salmon-700 dark:text-sweet-salmon-200">
<span class="text-xs text-red-700 dark:text-red-200">
{t("Linked QoE")}
</span>
<span class="font-mono text-base font-semibold text-sweet-salmon-900 dark:text-sweet-salmon-100">
<span class="font-mono text-base font-semibold text-red-900 dark:text-red-100">
{Float.round(insight.metadata["qoe_score"] / 1.0, 1)}
</span>
</div>
<% end %>
<%= if is_number(insight.metadata["temperature_c"]) and insight.metadata["temperature_c"] >= 75 do %>
<span class="ml-auto rounded-md bg-sweet-salmon-600 px-2 py-1 text-xs font-semibold text-white dark:bg-sweet-salmon-500">
<span class="ml-auto rounded-md bg-red-600 px-2 py-1 text-xs font-semibold text-white dark:bg-red-500">
{t("Above manufacturer spec")}
</span>
<% end %>
@ -638,42 +626,42 @@
<% end %>
<%= if insight.type == "own_fleet_channel_conflict" do %>
<div class="mt-3 rounded-md border border-wheat-200 bg-wheat-50 p-3 dark:border-wheat-800 dark:bg-wheat-900/20">
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<div class="flex items-center gap-3 text-sm">
<div class="flex flex-col">
<span class="text-xs text-wheat-700 dark:text-wheat-200">
<span class="text-xs text-amber-700 dark:text-amber-200">
{t("Channel")}
</span>
<span class="font-mono text-base font-semibold text-wheat-900 dark:text-wheat-100">
<span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100">
{insight.metadata["channel"]}
<%= if insight.metadata["frequency_mhz"] do %>
<span class="text-xs font-normal text-wheat-700 dark:text-wheat-200">
<span class="text-xs font-normal text-amber-700 dark:text-amber-200">
({insight.metadata["frequency_mhz"]} MHz)
</span>
<% end %>
</span>
</div>
<div class="flex flex-col">
<span class="text-xs text-wheat-700 dark:text-wheat-200">
<span class="text-xs text-amber-700 dark:text-amber-200">
{t("APs sharing")}
</span>
<span class="font-mono text-base font-semibold text-wheat-900 dark:text-wheat-100">
<span class="font-mono text-base font-semibold text-amber-900 dark:text-amber-100">
{insight.metadata["ap_count"]}
</span>
</div>
<%= if insight.metadata["has_critical_rf"] do %>
<span class="ml-auto rounded-md bg-sweet-salmon-100 px-2 py-1 text-xs font-medium text-sweet-salmon-800 dark:bg-sweet-salmon-900/40 dark:text-sweet-salmon-200">
<span class="ml-auto rounded-md bg-red-100 px-2 py-1 text-xs font-medium text-red-800 dark:bg-red-900/40 dark:text-red-200">
{t("Critical RF environment detected")}
</span>
<% end %>
</div>
<%= if is_list(insight.metadata["aps"]) and insight.metadata["aps"] != [] do %>
<div class="mt-3 overflow-x-auto">
<p class="text-xs font-medium text-wheat-800 dark:text-wheat-200">
<p class="text-xs font-medium text-amber-800 dark:text-amber-200">
{t("Conflicting APs")}
</p>
<table class="mt-1 w-full text-left text-xs text-wheat-900 dark:text-wheat-100">
<thead class="text-[11px] font-medium text-wheat-700 dark:text-wheat-200">
<table class="mt-1 w-full text-left text-xs text-amber-900 dark:text-amber-100">
<thead class="text-[11px] font-medium text-amber-700 dark:text-amber-200">
<tr>
<th class="py-1 pr-3">{t("AP")}</th>
<th class="py-1 pr-3">{t("Width")}</th>
@ -686,7 +674,7 @@
<tbody>
<tr
:for={ap <- insight.metadata["aps"]}
class="border-t border-wheat-200/50 dark:border-wheat-800/40"
class="border-t border-amber-200/50 dark:border-amber-800/40"
>
<td class="py-1 pr-3">
<%= if ap["device_id"] do %>
@ -700,7 +688,7 @@
{ap["name"] || "Unnamed"}
<% end %>
<%= if ap["rf_severity"] == "critical" do %>
<span class="ml-1 rounded bg-sweet-salmon-600 px-1 py-0.5 text-[10px] font-semibold text-white dark:bg-sweet-salmon-500">
<span class="ml-1 rounded bg-red-600 px-1 py-0.5 text-[10px] font-semibold text-white dark:bg-red-500">
crit
</span>
<% end %>
@ -790,7 +778,7 @@
</div>
<% end %>
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<%!-- Access point --%>
<%= if insight.preseem_access_point do %>
<span class="flex items-center gap-1">
@ -803,7 +791,7 @@
<%= if insight.device do %>
<.link
navigate={~p"/devices/#{insight.device.id}"}
class="flex items-center gap-1 text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="flex items-center gap-1 text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
<.icon name="hero-server" class="h-3.5 w-3.5" />
{insight.device.name || "Device"}
@ -829,7 +817,7 @@
type="button"
phx-click="dismiss"
phx-value-id={insight.id}
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-cool-steel-600 hover:bg-cool-steel-100 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-white"
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
>
{t("Dismiss")}
</button>

View file

@ -7,16 +7,16 @@
<div class="mb-6">
<.link
navigate={~p"/maintenance"}
class="inline-flex items-center gap-1 text-sm text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{t("Back to Maintenance Windows")}
</.link>
</div>
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white mb-6">{@page_title}</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white mb-6">{@page_title}</h1>
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-6">
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-6">
<.form
for={@form}
phx-change="validate"
@ -44,7 +44,7 @@
<%!-- Scope --%>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Scope")}
</label>
<div class="flex items-center gap-4 mb-3">
@ -56,11 +56,9 @@
checked={@scope_type == "org"}
phx-click="change_scope"
phx-value-scope_type="org"
class="text-cerulean-600 focus:ring-cerulean-500 dark:bg-cool-steel-800 dark:border-white/10"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
{t("Org-wide")}
</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Org-wide")}</span>
</label>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input
@ -70,11 +68,9 @@
checked={@scope_type == "site"}
phx-click="change_scope"
phx-value-scope_type="site"
class="text-cerulean-600 focus:ring-cerulean-500 dark:bg-cool-steel-800 dark:border-white/10"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
{t("Site")}
</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Site")}</span>
</label>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input
@ -84,11 +80,9 @@
checked={@scope_type == "device"}
phx-click="change_scope"
phx-value-scope_type="device"
class="text-cerulean-600 focus:ring-cerulean-500 dark:bg-cool-steel-800 dark:border-white/10"
class="text-blue-600 focus:ring-blue-500 dark:bg-gray-800 dark:border-white/10"
/>
<span class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
{t("Device")}
</span>
<span class="text-sm text-gray-700 dark:text-gray-300">{t("Device")}</span>
</label>
</div>
@ -154,16 +148,16 @@
<% end %>
<%!-- Submit --%>
<div class="flex items-center justify-end gap-3 pt-4 border-t border-cool-steel-200 dark:border-white/10">
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-white/10">
<.link
navigate={~p"/maintenance"}
class="rounded-lg px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-100 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800 transition-colors"
class="rounded-lg px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 transition-colors"
>
{t("Cancel")}
</.link>
<button
type="submit"
class="rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 transition-colors"
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
{t("Save")}
</button>

View file

@ -5,18 +5,16 @@
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="flex items-center justify-between mb-1">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">
{t("Maintenance Windows")}
</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("Maintenance Windows")}</h1>
<.link
navigate={~p"/maintenance/new"}
class="inline-flex items-center gap-2 rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Window")}
</.link>
</div>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4">
{t("Schedule planned downtime to suppress alerts during maintenance work.")}
</p>
@ -26,9 +24,9 @@
patch={~p"/maintenance?filter=all"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "all" && "bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "all" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "all" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("All")}
@ -37,10 +35,9 @@
patch={~p"/maintenance?filter=active"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "active" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "active" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "active" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Active")}
@ -49,10 +46,9 @@
patch={~p"/maintenance?filter=upcoming"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "upcoming" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "upcoming" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "upcoming" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Upcoming")}
@ -61,9 +57,9 @@
patch={~p"/maintenance?filter=past"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@filter == "past" && "bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@filter == "past" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@filter != "past" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Past")}
@ -75,51 +71,51 @@
<div class="text-center">
<.icon
name="hero-wrench-screwdriver"
class="h-12 w-12 text-cool-steel-300 dark:text-cool-steel-600 mx-auto mb-4"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No maintenance windows")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Schedule maintenance to suppress alerts during planned work.")}
</p>
</div>
</div>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Name")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Scope")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Time Range")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Status")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Created By")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-900">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
<%= for window <- @windows do %>
<tr
class="hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/30 transition-colors cursor-pointer"
class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors cursor-pointer"
phx-click={JS.navigate(~p"/maintenance/#{window.id}")}
>
<td class="px-4 py-3 text-sm font-medium text-cool-steel-900 dark:text-white">
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
{window.name}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{scope_label(window)}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400 whitespace-nowrap">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap">
{format_datetime(window.starts_at, @timezone)} — {format_datetime(
window.ends_at,
@timezone
@ -132,14 +128,14 @@
status == :active &&
"bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300",
status == :upcoming &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/40 dark:text-cerulean-300",
"bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
status == :past &&
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
]}>
{status_label(status)}
</span>
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{if window.created_by, do: window.created_by.email, else: "—"}
</td>
</tr>

View file

@ -7,7 +7,7 @@
<div class="mb-6">
<.link
navigate={~p"/maintenance"}
class="inline-flex items-center gap-1 text-sm text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-200"
class="inline-flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{t("Back to Maintenance Windows")}
@ -35,15 +35,13 @@
<div class="flex items-start justify-between mb-6">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{@window.name}</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@window.name}</h1>
<span class={[
"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium mt-2",
status == :active &&
"bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300",
status == :upcoming &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/40 dark:text-cerulean-300",
status == :past &&
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
status == :upcoming && "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
status == :past && "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
]}>
{status_label(status)}
</span>
@ -51,7 +49,7 @@
<div class="flex items-center gap-2">
<.link
navigate={~p"/maintenance/#{@window.id}/edit"}
class="inline-flex items-center gap-2 rounded-lg bg-cool-steel-100 px-4 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-gray-100 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors"
>
<.icon name="hero-pencil" class="h-4 w-4" />
{t("Edit")}
@ -60,7 +58,7 @@
type="button"
phx-click="delete"
data-confirm={t("Are you sure you want to delete this maintenance window?")}
class="inline-flex items-center gap-2 rounded-lg bg-sweet-salmon-100 px-4 py-2 text-sm font-medium text-sweet-salmon-700 hover:bg-sweet-salmon-200 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-300 dark:hover:bg-sweet-salmon-900/50 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-red-100 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/50 transition-colors"
>
<.icon name="hero-trash" class="h-4 w-4" />
{t("Delete")}
@ -68,14 +66,14 @@
</div>
</div>
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-white/10">
<%!-- Reason --%>
<%= if @window.reason do %>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Reason")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white whitespace-pre-wrap">
<dd class="text-sm text-gray-900 dark:text-white whitespace-pre-wrap">
{@window.reason}
</dd>
</div>
@ -83,18 +81,18 @@
<%!-- Scope --%>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Scope")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white">{scope_label(@window)}</dd>
<dd class="text-sm text-gray-900 dark:text-white">{scope_label(@window)}</dd>
</div>
<%!-- Time Range --%>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Time Range")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white">
<dd class="text-sm text-gray-900 dark:text-white">
{format_datetime(@window.starts_at, @timezone)} — {format_datetime(
@window.ends_at,
@timezone
@ -104,10 +102,10 @@
<%!-- Recurring --%>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Recurring")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white">
<dd class="text-sm text-gray-900 dark:text-white">
<%= if @window.recurring do %>
{t("Yes")} — {@window.recurrence_rule || t("No rule specified")}
<% else %>
@ -118,16 +116,16 @@
<%!-- Suppress Alerts --%>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Suppress Alerts")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white">
<dd class="text-sm text-gray-900 dark:text-white">
<%= if @window.suppress_alerts do %>
<span class="inline-flex items-center gap-1 text-green-700 dark:text-green-400">
<.icon name="hero-check-circle" class="h-4 w-4" /> {t("Yes")}
</span>
<% else %>
<span class="inline-flex items-center gap-1 text-cool-steel-500 dark:text-cool-steel-400">
<span class="inline-flex items-center gap-1 text-gray-500 dark:text-gray-400">
<.icon name="hero-x-circle" class="h-4 w-4" /> {t("No")}
</span>
<% end %>
@ -136,10 +134,10 @@
<%!-- Created By --%>
<div class="px-6 py-4">
<dt class="text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">
{t("Created By")}
</dt>
<dd class="text-sm text-cool-steel-900 dark:text-white">
<dd class="text-sm text-gray-900 dark:text-white">
{if @window.created_by, do: @window.created_by.email, else: "—"}
</dd>
</div>

View file

@ -7,7 +7,7 @@
<.header>
<span class="flex items-center gap-2">
{t("Sites Map")}
<span class="inline-flex items-center rounded-full bg-cerulean-50 px-2 py-0.5 text-xs font-medium text-cerulean-700 ring-1 ring-inset ring-cerulean-700/10 dark:bg-cerulean-400/10 dark:text-cerulean-400 dark:ring-cerulean-400/30">
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
{t("Geographic")}
</span>
</span>
@ -23,19 +23,19 @@
</.header>
<div class="mb-6">
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-white/10 overflow-hidden">
<div class="p-4 border-b border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="p-4 border-b border-gray-200 dark:border-white/10">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Geographic Site Locations")}
</h3>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("Click markers to view site details")}
</p>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
Showing {length(@sites)} sites with coordinates
</span>
</div>
@ -54,13 +54,8 @@
<!-- Loading State -->
<div class="flex items-center justify-center h-full">
<div class="text-center">
<.icon
name="hero-arrow-path"
class="h-8 w-8 text-cool-steel-400 animate-spin mx-auto"
/>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
Loading map...
</p>
<.icon name="hero-arrow-path" class="h-8 w-8 text-gray-400 animate-spin mx-auto" />
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Loading map...</p>
</div>
</div>
</div>
@ -70,32 +65,24 @@
<!-- Stats and Site List -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Site Statistics -->
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<h3 class="text-lg font-medium text-cool-steel-900 dark:text-white mb-4">
{t("Statistics")}
</h3>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">{t("Statistics")}</h3>
<div class="space-y-4">
<div>
<div class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Total Sites
</div>
<div class="text-2xl font-semibold text-cool-steel-900 dark:text-white">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Sites</div>
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
{Sites.count_organization_sites(@organization.id)}
</div>
</div>
<div>
<div class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Geocoded Sites
</div>
<div class="text-2xl font-semibold text-cool-steel-900 dark:text-white">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Geocoded Sites</div>
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
{length(@sites)}
</div>
</div>
<div>
<div class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
Coverage
</div>
<div class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Coverage</div>
<div class="text-lg font-semibold text-gray-900 dark:text-white">
<%= if Sites.count_organization_sites(@organization.id) > 0 do %>
{round(length(@sites) / Sites.count_organization_sites(@organization.id) * 100)}%
<% else %>
@ -107,47 +94,47 @@
</div>
<!-- Sites with Coordinates -->
<div class="lg:col-span-2 bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="px-6 py-4 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-lg font-medium text-cool-steel-900 dark:text-white">
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-6 py-4 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
{t("Sites with Geographic Data")}
</h3>
</div>
<div class="max-h-96 overflow-y-auto">
<%= if length(@sites) > 0 do %>
<div class="divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<%= for site <- @sites do %>
<div class="px-6 py-4 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700/50">
<div class="px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center">
<h4 class="text-sm font-medium text-cool-steel-900 dark:text-white truncate">
<h4 class="text-sm font-medium text-gray-900 dark:text-white truncate">
{site.name}
</h4>
<%= if site.device_count > 0 do %>
<span class="ml-2 inline-flex items-center rounded-full bg-cerulean-100 px-2 py-0.5 text-xs font-medium text-cerulean-700 dark:bg-cerulean-900 dark:text-cerulean-300">
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
{site.device_count} devices
</span>
<% end %>
</div>
<%= if site.address do %>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{site.address}
</p>
<% end %>
<%= if site.description do %>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{site.description}
</p>
<% end %>
<p class="mt-1 text-xs text-cool-steel-400">
<p class="mt-1 text-xs text-gray-400">
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
</p>
</div>
<div class="ml-4">
<.link
navigate={~p"/sites/#{site.id}"}
class="text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400 dark:hover:text-cerulean-300 text-sm font-medium"
class="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium"
>
View →
</.link>
@ -158,11 +145,11 @@
</div>
<% else %>
<div class="px-6 py-8 text-center">
<.icon name="hero-map-pin" class="mx-auto h-12 w-12 text-cool-steel-400" />
<h3 class="mt-2 text-sm font-medium text-cool-steel-900 dark:text-white">
<.icon name="hero-map-pin" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{t("No geocoded sites")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Add addresses and coordinates to your sites to see them on the map.")}
</p>
<div class="mt-4">

View file

@ -2,38 +2,35 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-6">
<nav class="flex items-center gap-2 text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-4">
<.link
navigate={~p"/devices"}
class="hover:text-cool-steel-700 dark:hover:text-cool-steel-300"
>
<nav class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-4">
<.link navigate={~p"/devices"} class="hover:text-gray-700 dark:hover:text-gray-300">
{t("Devices")}
</.link>
<span>/</span>
<.link
navigate={~p"/devices/#{@device.id}"}
class="hover:text-cool-steel-700 dark:hover:text-cool-steel-300"
class="hover:text-gray-700 dark:hover:text-gray-300"
>
{@device.name}
</.link>
<span>/</span>
<.link
navigate={~p"/devices/#{@device.id}?tab=backups"}
class="hover:text-cool-steel-700 dark:hover:text-cool-steel-300"
class="hover:text-gray-700 dark:hover:text-gray-300"
>
{t("Backups")}
</.link>
<span>/</span>
<span class="text-cool-steel-900 dark:text-white">Compare</span>
<span class="text-gray-900 dark:text-white">Compare</span>
</nav>
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{t("Compare Configuration Backups")}
</h1>
<.link
navigate={~p"/devices/#{@device.id}?tab=backups"}
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-cool-steel-700 bg-white border border-cool-steel-300 rounded-lg hover:bg-cool-steel-50 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:border-cool-steel-600 dark:hover:bg-cool-steel-700"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"
>
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Backups
</.link>
@ -42,23 +39,21 @@
<!-- Backup Info Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<!-- Backup A -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
Backup A (Older)
</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Backup A (Older)</h3>
<button
phx-click="download_config"
phx-value-backup="a"
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Download
</button>
</div>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Date:</dt>
<dd class="text-cool-steel-900 dark:text-white font-mono">
<dt class="text-gray-500 dark:text-gray-400">Date:</dt>
<dd class="text-gray-900 dark:text-white font-mono">
{ToweropsWeb.TimeHelpers.format_iso8601(
@backup_a.backed_up_at,
@current_scope.timezone,
@ -67,18 +62,18 @@
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Size:</dt>
<dd class="text-cool-steel-900 dark:text-white font-mono">
<dt class="text-gray-500 dark:text-gray-400">Size:</dt>
<dd class="text-gray-900 dark:text-white font-mono">
{format_bytes(@backup_a.config_size_bytes)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Source:</dt>
<dt class="text-gray-500 dark:text-gray-400">Source:</dt>
<dd>
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
@backup_a.trigger_source == "daily_cron" &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900 dark:text-cerulean-200",
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
@backup_a.trigger_source == "manual" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
]}>
@ -89,23 +84,21 @@
</dl>
</div>
<!-- Backup B -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
Backup B (Newer)
</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Backup B (Newer)</h3>
<button
phx-click="download_config"
phx-value-backup="b"
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Download
</button>
</div>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Date:</dt>
<dd class="text-cool-steel-900 dark:text-white font-mono">
<dt class="text-gray-500 dark:text-gray-400">Date:</dt>
<dd class="text-gray-900 dark:text-white font-mono">
{ToweropsWeb.TimeHelpers.format_iso8601(
@backup_b.backed_up_at,
@current_scope.timezone,
@ -114,18 +107,18 @@
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Size:</dt>
<dd class="text-cool-steel-900 dark:text-white font-mono">
<dt class="text-gray-500 dark:text-gray-400">Size:</dt>
<dd class="text-gray-900 dark:text-white font-mono">
{format_bytes(@backup_b.config_size_bytes)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">Source:</dt>
<dt class="text-gray-500 dark:text-gray-400">Source:</dt>
<dd>
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
@backup_b.trigger_source == "daily_cron" &&
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900 dark:text-cerulean-200",
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
@backup_b.trigger_source == "manual" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
]}>
@ -147,37 +140,37 @@
</div>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 mb-6">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 mb-6">
<div class="flex items-center gap-6 text-sm">
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 bg-green-500 rounded-sm"></span>
<span class="text-cool-steel-600 dark:text-cool-steel-400">
<span class="text-gray-600 dark:text-gray-400">
{@stats.additions} addition{if @stats.additions != 1, do: "s"}
</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 bg-sweet-salmon-500 rounded-sm"></span>
<span class="text-cool-steel-600 dark:text-cool-steel-400">
<span class="inline-block w-3 h-3 bg-red-500 rounded-sm"></span>
<span class="text-gray-600 dark:text-gray-400">
{@stats.deletions} deletion{if @stats.deletions != 1, do: "s"}
</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block w-3 h-3 bg-wheat-500 rounded-sm"></span>
<span class="text-cool-steel-600 dark:text-cool-steel-400">
<span class="inline-block w-3 h-3 bg-yellow-500 rounded-sm"></span>
<span class="text-gray-600 dark:text-gray-400">
{@stats.changes} change{if @stats.changes != 1, do: "s"}
</span>
</div>
</div>
</div>
<!-- Unified Diff Display -->
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-cool-steel-200 dark:border-white/10 bg-cool-steel-50 dark:bg-cool-steel-800/75">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-800/75">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Configuration Differences")}
</h3>
</div>
<div class="overflow-x-auto">
<pre class="p-4 text-xs font-mono leading-relaxed"><code><%= for line <- String.split(@diff_output, "\n") do %><%= cond do %><% String.starts_with?(line, "+") && !String.starts_with?(line, "+++") -> %><span class="block bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200">{line}</span><% String.starts_with?(line, "-") && !String.starts_with?(line, "---") -> %><span class="block bg-sweet-salmon-100 dark:bg-sweet-salmon-900/30 text-sweet-salmon-800 dark:text-sweet-salmon-200">{line}</span><% String.starts_with?(line, "@@") -> %><span class="block bg-cerulean-50 dark:bg-cerulean-900/20 text-cerulean-700 dark:text-cerulean-300 font-semibold">{line}</span><% String.starts_with?(line, "---") || String.starts_with?(line, "+++") -> %><span class="block text-cool-steel-500 dark:text-cool-steel-500 italic">{line}</span><% true -> %><span class="block text-cool-steel-700 dark:text-cool-steel-300">{line}</span><% end %><% end %></code></pre>
<pre class="p-4 text-xs font-mono leading-relaxed"><code><%= for line <- String.split(@diff_output, "\n") do %><%= cond do %><% String.starts_with?(line, "+") && !String.starts_with?(line, "+++") -> %><span class="block bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200">{line}</span><% String.starts_with?(line, "-") && !String.starts_with?(line, "---") -> %><span class="block bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200">{line}</span><% String.starts_with?(line, "@@") -> %><span class="block bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 font-semibold">{line}</span><% String.starts_with?(line, "---") || String.starts_with?(line, "+++") -> %><span class="block text-gray-500 dark:text-gray-500 italic">{line}</span><% true -> %><span class="block text-gray-700 dark:text-gray-300">{line}</span><% end %><% end %></code></pre>
</div>
</div>
<% end %>

View file

@ -132,7 +132,7 @@ defmodule ToweropsWeb.MobileQRLive do
</.header>
<div :if={!@completed} class="mt-8">
<div class="rounded-lg bg-white p-8 shadow-sm ring-1 ring-cool-steel-200 dark:bg-cool-steel-800/50 dark:ring-white/10">
<div class="rounded-lg bg-white p-8 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800/50 dark:ring-white/10">
<div class="flex flex-col items-center space-y-6">
<div class="rounded-lg bg-white p-4">
<img
@ -143,66 +143,64 @@ defmodule ToweropsWeb.MobileQRLive do
</div>
<div class="text-center">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs text-gray-500 dark:text-gray-400">
This QR code expires in 5 minutes
</p>
</div>
<div class="flex items-center space-x-2">
<div class="h-2 w-2 animate-pulse rounded-full bg-cerulean-500"></div>
<span class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="h-2 w-2 animate-pulse rounded-full bg-blue-500"></div>
<span class="text-sm text-gray-600 dark:text-gray-400">
Waiting for mobile app to scan...
</span>
</div>
</div>
</div>
<div class="mt-6 rounded-lg bg-white p-6 shadow-sm ring-1 ring-cool-steel-200 dark:bg-cool-steel-800/50 dark:ring-white/10">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="mt-6 rounded-lg bg-white p-6 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800/50 dark:ring-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
How to link your phone
</h3>
<ol class="mt-4 space-y-3">
<li class="flex items-start gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-cerulean-600 text-xs font-bold text-white">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-indigo-600 text-xs font-bold text-white">
1
</span>
<span class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
Open the
<span class="font-medium text-cool-steel-900 dark:text-white">Towerops</span>
<span class="text-sm text-gray-600 dark:text-gray-400">
Open the <span class="font-medium text-gray-900 dark:text-white">Towerops</span>
app on your iPhone
</span>
</li>
<li class="flex items-start gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-cerulean-600 text-xs font-bold text-white">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-indigo-600 text-xs font-bold text-white">
2
</span>
<span class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
Tap
<span class="font-medium text-cool-steel-900 dark:text-white">"Scan QR Code"</span>
<span class="text-sm text-gray-600 dark:text-gray-400">
Tap <span class="font-medium text-gray-900 dark:text-white">"Scan QR Code"</span>
on the login screen
</span>
</li>
<li class="flex items-start gap-3">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-cerulean-600 text-xs font-bold text-white">
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-indigo-600 text-xs font-bold text-white">
3
</span>
<span class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<span class="text-sm text-gray-600 dark:text-gray-400">
Point your camera at the QR code above
</span>
</li>
</ol>
</div>
<div class="mt-4 rounded-lg bg-cerulean-50 p-4 dark:bg-cerulean-900/20">
<div class="mt-4 rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-information-circle" class="h-5 w-5 text-cerulean-400" />
<.icon name="hero-information-circle" class="h-5 w-5 text-blue-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-cerulean-800 dark:text-cerulean-300">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-300">
Don't have the app yet?
</h3>
<p class="mt-1 text-sm text-cerulean-700 dark:text-cerulean-400">
<p class="mt-1 text-sm text-blue-700 dark:text-blue-400">
Download Towerops from the App Store on your iPhone.
</p>
</div>
@ -226,29 +224,23 @@ defmodule ToweropsWeb.MobileQRLive do
</p>
</div>
<div class="mt-4 w-full max-w-md rounded-lg bg-white p-4 dark:bg-cool-steel-800/50">
<div class="mt-4 w-full max-w-md rounded-lg bg-white p-4 dark:bg-gray-800/50">
<dl class="space-y-2 text-sm">
<div>
<dt class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
Device Name
</dt>
<dd class="mt-1 text-cool-steel-900 dark:text-white">
<dt class="font-medium text-gray-700 dark:text-gray-300">Device Name</dt>
<dd class="mt-1 text-gray-900 dark:text-white">
{@mobile_session.device_name || "Unknown"}
</dd>
</div>
<div>
<dt class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
Device OS
</dt>
<dd class="mt-1 text-cool-steel-900 dark:text-white">
<dt class="font-medium text-gray-700 dark:text-gray-300">Device OS</dt>
<dd class="mt-1 text-gray-900 dark:text-white">
{@mobile_session.device_os || "Unknown"}
</dd>
</div>
<div>
<dt class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
App Version
</dt>
<dd class="mt-1 text-cool-steel-900 dark:text-white">
<dt class="font-medium text-gray-700 dark:text-gray-300">App Version</dt>
<dd class="mt-1 text-gray-900 dark:text-white">
{@mobile_session.app_version || "Unknown"}
</dd>
</div>
@ -257,7 +249,7 @@ defmodule ToweropsWeb.MobileQRLive do
<.link
navigate={~p"/users/settings"}
class="text-sm font-medium text-cerulean-600 hover:text-cerulean-500 dark:text-cerulean-400"
class="text-sm font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Manage mobile sessions
</.link>
@ -268,7 +260,7 @@ defmodule ToweropsWeb.MobileQRLive do
<div class="mt-6">
<.link
navigate={~p"/orgs"}
class="text-sm font-medium text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Back to Organizations
</.link>

View file

@ -7,13 +7,13 @@
<.header>
<span class="flex items-center gap-2">
{t("Network Map")}
<span class="inline-flex items-center rounded-full bg-cerulean-50 px-2 py-0.5 text-xs font-medium text-cerulean-700 ring-1 ring-inset ring-cerulean-700/10 dark:bg-cerulean-400/10 dark:text-cerulean-400 dark:ring-cerulean-400/30">
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
{t("Experimental")}
</span>
</span>
<:subtitle>{t("Visual topology of your network infrastructure")}</:subtitle>
<:actions>
<div class="flex items-center gap-1.5 text-xs text-cool-steel-400 dark:text-cool-steel-500 font-mono">
<div class="flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75">
</span>
@ -25,16 +25,16 @@
</.header>
<!-- Tab Navigation -->
<div class="border-b border-cool-steel-200 dark:border-white/10 mb-6">
<div class="border-b border-gray-200 dark:border-white/10 mb-6">
<nav class="-mb-px flex space-x-8">
<.link
patch={~p"/network-map?tab=added"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "added" do
"border-cerulean-500 text-cerulean-600 dark:text-cerulean-400"
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-cool-steel-500 hover:text-cool-steel-700 hover:border-cool-steel-300 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
@ -46,14 +46,14 @@
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "all" do
"border-cerulean-500 text-cerulean-600 dark:text-cerulean-400"
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-cool-steel-500 hover:text-cool-steel-700 hover:border-cool-steel-300 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
{t("All Devices")}
<span class="ml-2 inline-flex items-center rounded-full bg-cool-steel-100 px-2 py-0.5 text-xs font-medium text-cool-steel-700 dark:bg-cool-steel-700 dark:text-cool-steel-300">
<span class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{@topology.stats.discovered_devices}
</span>
</.link>
@ -63,27 +63,25 @@
<%= if @loading do %>
<div class="flex items-center justify-center h-96">
<div class="text-center">
<.icon name="hero-arrow-path" class="h-12 w-12 text-cool-steel-400 animate-spin mx-auto" />
<p class="mt-4 text-sm text-cool-steel-600 dark:text-cool-steel-400">
Loading network topology...
</p>
<.icon name="hero-arrow-path" class="h-12 w-12 text-gray-400 animate-spin mx-auto" />
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">Loading network topology...</p>
</div>
</div>
<% else %>
<!-- Stats Bar -->
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4 mb-6">
<div class="bg-white dark:bg-cool-steel-800 overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<.icon name="hero-server" class="h-6 w-6 text-cool-steel-400" />
<.icon name="hero-server" class="h-6 w-6 text-gray-400" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
{t("Total Devices")}
</dt>
<dd class="text-lg font-medium text-cool-steel-900 dark:text-white">
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.total_devices}
</dd>
</dl>
@ -92,7 +90,7 @@
</div>
</div>
<div class="bg-white dark:bg-cool-steel-800 overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
@ -100,10 +98,10 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
{t("Added Devices")}
</dt>
<dd class="text-lg font-medium text-cool-steel-900 dark:text-white">
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.added_devices}
</dd>
</dl>
@ -112,18 +110,18 @@
</div>
</div>
<div class="bg-white dark:bg-cool-steel-800 overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<.icon name="hero-magnifying-glass" class="h-6 w-6 text-cerulean-400" />
<.icon name="hero-magnifying-glass" class="h-6 w-6 text-blue-400" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
{t("Discovered")}
</dt>
<dd class="text-lg font-medium text-cool-steel-900 dark:text-white">
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.discovered_devices}
</dd>
</dl>
@ -132,7 +130,7 @@
</div>
</div>
<div class="bg-white dark:bg-cool-steel-800 overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
@ -140,10 +138,10 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
{t("Connections")}
</dt>
<dd class="text-lg font-medium text-cool-steel-900 dark:text-white">
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.total_links}
</dd>
</dl>
@ -154,7 +152,7 @@
</div>
<!-- Filter Bar -->
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-white/10 mb-4 p-3">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 mb-4 p-3">
<div class="flex items-center justify-between gap-4 flex-wrap">
<div class="flex items-center gap-2">
<button
@ -163,10 +161,8 @@
class={[
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
if(@filter == "all",
do:
"bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-400/20 dark:text-cerulean-400",
else:
"text-cool-steel-600 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-cool-steel-700"
do: "bg-blue-100 text-blue-700 dark:bg-blue-400/20 dark:text-blue-400",
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
)
]}
>
@ -178,15 +174,14 @@
class={[
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
if(@filter == "degraded",
do: "bg-wheat-100 text-wheat-700 dark:bg-wheat-400/20 dark:text-wheat-400",
else:
"text-cool-steel-600 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-cool-steel-700"
do: "bg-yellow-100 text-yellow-700 dark:bg-yellow-400/20 dark:text-yellow-400",
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
)
]}
>
Degraded Only
<%= if @topology.stats[:degraded_links] && @topology.stats.degraded_links > 0 do %>
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-wheat-800 bg-wheat-200 rounded-full dark:bg-wheat-900 dark:text-wheat-300">
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-yellow-800 bg-yellow-200 rounded-full dark:bg-yellow-900 dark:text-yellow-300">
{@topology.stats.degraded_links}
</span>
<% end %>
@ -197,16 +192,14 @@
class={[
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
if(@filter == "alerts",
do:
"bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-400/20 dark:text-sweet-salmon-400",
else:
"text-cool-steel-600 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-cool-steel-700"
do: "bg-red-100 text-red-700 dark:bg-red-400/20 dark:text-red-400",
else: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
)
]}
>
Sites with Alerts
<%= if @topology.stats[:sites_with_alerts] && @topology.stats.sites_with_alerts > 0 do %>
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-sweet-salmon-800 bg-sweet-salmon-200 rounded-full dark:bg-sweet-salmon-900 dark:text-sweet-salmon-300">
<span class="ml-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-red-800 bg-red-200 rounded-full dark:bg-red-900 dark:text-red-300">
{@topology.stats.sites_with_alerts}
</span>
<% end %>
@ -216,7 +209,7 @@
<div class="relative">
<.icon
name="hero-magnifying-glass"
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-cool-steel-400"
class="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"
/>
<input
type="text"
@ -225,21 +218,19 @@
phx-key="Enter"
phx-debounce="300"
value={@search_query}
class="pl-8 pr-3 py-1.5 text-xs border border-cool-steel-300 dark:border-cool-steel-600 rounded-md bg-white dark:bg-cool-steel-700 text-cool-steel-900 dark:text-white placeholder-cool-steel-400 focus:ring-1 focus:ring-cerulean-500 focus:border-cerulean-500 w-48"
class="pl-8 pr-3 py-1.5 text-xs border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 w-48"
/>
</div>
<%= if @topology[:has_geo] do %>
<div class="flex items-center gap-1 border-l border-cool-steel-200 dark:border-cool-steel-600 pl-3">
<div class="flex items-center gap-1 border-l border-gray-200 dark:border-gray-600 pl-3">
<button
phx-click="toggle_layout"
phx-value-mode="force"
class={[
"px-2 py-1 text-xs rounded transition-colors",
if(@layout_mode == "force",
do:
"bg-cool-steel-200 dark:bg-cool-steel-600 text-cool-steel-900 dark:text-white font-medium",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:hover:bg-cool-steel-700"
do: "bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white font-medium",
else: "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
)
]}
title="Force-directed layout"
@ -252,10 +243,8 @@
class={[
"px-2 py-1 text-xs rounded transition-colors",
if(@layout_mode == "geo",
do:
"bg-cool-steel-200 dark:bg-cool-steel-600 text-cool-steel-900 dark:text-white font-medium",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:hover:bg-cool-steel-700"
do: "bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white font-medium",
else: "text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
)
]}
title="Geographic layout"
@ -269,19 +258,19 @@
</div>
<!-- Network Map Container -->
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-white/10 overflow-hidden">
<div class="p-4 border-b border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="p-4 border-b border-gray-200 dark:border-white/10">
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Topology Visualization")}
</h3>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("Click and drag to pan, scroll to zoom, click nodes for details")}
</p>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
Last updated: <.timestamp datetime={@topology.last_updated} timezone={@timezone} />
</span>
</div>
@ -306,7 +295,7 @@
id="cy-zoom-in"
type="button"
title="Zoom in"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-cool-steel-700 border border-cool-steel-200 dark:border-white/10 rounded shadow-sm text-cool-steel-600 dark:text-cool-steel-300 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-600 hover:text-cool-steel-900 dark:hover:text-white transition-colors"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
</button>
@ -314,7 +303,7 @@
id="cy-zoom-out"
type="button"
title="Zoom out"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-cool-steel-700 border border-cool-steel-200 dark:border-white/10 rounded shadow-sm text-cool-steel-600 dark:text-cool-steel-300 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-600 hover:text-cool-steel-900 dark:hover:text-white transition-colors"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<.icon name="hero-minus" class="h-4 w-4" />
</button>
@ -322,7 +311,7 @@
id="cy-fit"
type="button"
title="Fit to screen"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-cool-steel-700 border border-cool-steel-200 dark:border-white/10 rounded shadow-sm text-cool-steel-600 dark:text-cool-steel-300 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-600 hover:text-cool-steel-900 dark:hover:text-white transition-colors"
class="flex items-center justify-center w-8 h-8 bg-white dark:bg-gray-700 border border-gray-200 dark:border-white/10 rounded shadow-sm text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<.icon name="hero-arrows-pointing-out" class="h-4 w-4" />
</button>
@ -332,18 +321,18 @@
<%= if @selected_node_detail do %>
<div
id="node-detail-panel"
class="absolute top-0 right-0 h-full w-80 bg-white dark:bg-cool-steel-800 border-l border-cool-steel-200 dark:border-white/10 shadow-lg overflow-y-auto transition-transform duration-200"
class="absolute top-0 right-0 h-full w-80 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-white/10 shadow-lg overflow-y-auto transition-transform duration-200"
>
<div class="p-4">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Node Details")}
</h3>
<button
id="close-detail-panel"
phx-click="close_detail_panel"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="h-5 w-5" />
</button>
@ -352,11 +341,11 @@
<!-- Device Info -->
<div class="space-y-3">
<div>
<h4 class="text-base font-medium text-cool-steel-900 dark:text-white">
<h4 class="text-base font-medium text-gray-900 dark:text-white">
{@selected_node_detail.name}
</h4>
<%= if @selected_node_detail.type == :discovered do %>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2 py-0.5 text-xs font-medium text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300 mt-1">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300 mt-1">
{t("Discovered")}
</span>
<% end %>
@ -364,11 +353,8 @@
<%= if @selected_node_detail.ip_address do %>
<div class="flex items-center text-sm">
<.icon
name="hero-globe-alt"
class="h-4 w-4 text-cool-steel-400 mr-2 flex-shrink-0"
/>
<span class="text-cool-steel-700 dark:text-cool-steel-300">
<.icon name="hero-globe-alt" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
<span class="text-gray-700 dark:text-gray-300">
{@selected_node_detail.ip_address}
</span>
</div>
@ -376,11 +362,8 @@
<%= if @selected_node_detail.site_name do %>
<div class="flex items-center text-sm">
<.icon
name="hero-map-pin"
class="h-4 w-4 text-cool-steel-400 mr-2 flex-shrink-0"
/>
<span class="text-cool-steel-700 dark:text-cool-steel-300">
<.icon name="hero-map-pin" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
<span class="text-gray-700 dark:text-gray-300">
{@selected_node_detail.site_name}
</span>
</div>
@ -390,17 +373,17 @@
<div class="flex items-center text-sm">
<.icon
name="hero-building-office"
class="h-4 w-4 text-cool-steel-400 mr-2 flex-shrink-0"
class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0"
/>
<span class="text-cool-steel-700 dark:text-cool-steel-300">
<span class="text-gray-700 dark:text-gray-300">
{@selected_node_detail.manufacturer}
</span>
</div>
<% end %>
<div class="flex items-center text-sm">
<.icon name="hero-tag" class="h-4 w-4 text-cool-steel-400 mr-2 flex-shrink-0" />
<span class="text-cool-steel-700 dark:text-cool-steel-300 capitalize">
<.icon name="hero-tag" class="h-4 w-4 text-gray-400 mr-2 flex-shrink-0" />
<span class="text-gray-700 dark:text-gray-300 capitalize">
{@selected_node_detail.role}
</span>
</div>
@ -411,11 +394,11 @@
"inline-block h-2 w-2 rounded-full mr-2",
if(@selected_node_detail.status == :up,
do: "bg-green-400",
else: "bg-sweet-salmon-400"
else: "bg-red-400"
)
]}>
</span>
<span class="text-cool-steel-700 dark:text-cool-steel-300 capitalize">
<span class="text-gray-700 dark:text-gray-300 capitalize">
{@selected_node_detail.status}
</span>
</div>
@ -424,33 +407,29 @@
<!-- Wireless Stats (for APs, CPEs, backhaul radios) -->
<%= if @selected_node_detail[:is_wireless] do %>
<div class="mt-4 pt-3 border-t border-cool-steel-200 dark:border-white/10">
<h4 class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-2">
<div class="mt-4 pt-3 border-t border-gray-200 dark:border-white/10">
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
RF Stats
</h4>
<div class="grid grid-cols-2 gap-2">
<!-- Client count -->
<div class="bg-cool-steel-50 dark:bg-cool-steel-700/50 rounded-md p-2">
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
Clients
</div>
<div class="text-lg font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1">
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-md p-2">
<div class="text-xs text-gray-500 dark:text-gray-400">Clients</div>
<div class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-1">
<.icon name="hero-wifi" class="h-4 w-4 text-purple-500" />
{@selected_node_detail[:client_count] || 0}
</div>
</div>
<!-- Signal health -->
<div class="bg-cool-steel-50 dark:bg-cool-steel-700/50 rounded-md p-2">
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
Signal
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-md p-2">
<div class="text-xs text-gray-500 dark:text-gray-400">Signal</div>
<div class={[
"text-sm font-semibold capitalize",
case @selected_node_detail[:signal_health] do
"good" -> "text-green-600 dark:text-green-400"
"degraded" -> "text-wheat-600 dark:text-wheat-400"
"critical" -> "text-sweet-salmon-600 dark:text-sweet-salmon-400"
_ -> "text-cool-steel-500 dark:text-cool-steel-400"
"degraded" -> "text-yellow-600 dark:text-yellow-400"
"critical" -> "text-red-600 dark:text-red-400"
_ -> "text-gray-500 dark:text-gray-400"
end
]}>
{@selected_node_detail[:signal_health] || "N/A"}
@ -458,10 +437,8 @@
</div>
<!-- SNR -->
<%= if @selected_node_detail[:snr] do %>
<div class="bg-cool-steel-50 dark:bg-cool-steel-700/50 rounded-md p-2 col-span-2">
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
SNR
</div>
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-md p-2 col-span-2">
<div class="text-xs text-gray-500 dark:text-gray-400">SNR</div>
<div class={[
"text-sm font-semibold",
cond do
@ -469,10 +446,10 @@
"text-green-600 dark:text-green-400"
@selected_node_detail.snr >= 15 ->
"text-wheat-600 dark:text-wheat-400"
"text-yellow-600 dark:text-yellow-400"
true ->
"text-sweet-salmon-600 dark:text-sweet-salmon-400"
"text-red-600 dark:text-red-400"
end
]}>
{@selected_node_detail.snr} dB
@ -485,33 +462,33 @@
<!-- Connections -->
<%= if length(@selected_node_detail.connections) > 0 do %>
<div class="mt-5 pt-4 border-t border-cool-steel-200 dark:border-white/10">
<h4 class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-3">
<div class="mt-5 pt-4 border-t border-gray-200 dark:border-white/10">
<h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
{t("Connections")} ({length(@selected_node_detail.connections)})
</h4>
<div class="space-y-2">
<%= for conn <- @selected_node_detail.connections do %>
<div class="flex items-center justify-between py-1.5 px-2 rounded bg-cool-steel-50 dark:bg-cool-steel-700/50 text-sm">
<div class="flex items-center justify-between py-1.5 px-2 rounded bg-gray-50 dark:bg-gray-700/50 text-sm">
<div class="flex-1 min-w-0">
<p class="text-cool-steel-900 dark:text-white truncate font-medium text-xs">
<p class="text-gray-900 dark:text-white truncate font-medium text-xs">
{conn.device_name}
</p>
<%= if conn.interface do %>
<p class="text-cool-steel-500 dark:text-cool-steel-400 text-xs">
<p class="text-gray-500 dark:text-gray-400 text-xs">
{conn.interface}
</p>
<% end %>
</div>
<div class="flex items-center space-x-2 ml-2 flex-shrink-0">
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400 uppercase">
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">
{conn.link_type}
</span>
<span class={[
"text-xs font-medium",
cond do
conn.confidence > 0.9 -> "text-green-600 dark:text-green-400"
conn.confidence >= 0.5 -> "text-wheat-600 dark:text-wheat-400"
true -> "text-cool-steel-500 dark:text-cool-steel-400"
conn.confidence >= 0.5 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-gray-500 dark:text-gray-400"
end
]}>
{trunc(conn.confidence * 100)}%
@ -528,7 +505,7 @@
<div class="mt-4 space-y-2">
<.link
navigate={~p"/devices/#{@selected_node_detail.id}"}
class="flex items-center justify-center w-full px-3 py-2 text-sm font-medium text-cerulean-600 dark:text-cerulean-400 bg-cerulean-50 dark:bg-cerulean-400/10 rounded-md hover:bg-cerulean-100 dark:hover:bg-cerulean-400/20"
class="flex items-center justify-center w-full px-3 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-400/10 rounded-md hover:bg-blue-100 dark:hover:bg-blue-400/20"
>
<.icon name="hero-arrow-top-right-on-square" class="h-4 w-4 mr-1.5" />
{t("View Device")}
@ -552,18 +529,18 @@
</div>
<!-- Legend -->
<div class="p-4 border-t border-cool-steel-200 dark:border-white/10 bg-cool-steel-50 dark:bg-cool-steel-900">
<div class="p-4 border-t border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-900">
<div class="flex flex-col gap-3">
<%!-- Device roles --%>
<div class="flex flex-wrap items-center gap-x-5 gap-y-1 text-xs">
<span class="font-medium text-cool-steel-600 dark:text-cool-steel-400">Roles:</span>
<span class="font-medium text-gray-600 dark:text-gray-400">Roles:</span>
<div class="flex items-center space-x-1.5">
<div class="w-3.5 h-3 rounded-sm" style="background-color: #3B82F6;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Router</span>
<span class="text-gray-700 dark:text-gray-300">Router</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-3 h-3 rotate-45" style="background-color: #10B981;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Switch</span>
<span class="text-gray-700 dark:text-gray-300">Switch</span>
</div>
<div class="flex items-center space-x-1.5">
<div
@ -571,59 +548,57 @@
style="border-bottom-color: #8B5CF6;"
>
</div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">AP / Wireless</span>
<span class="text-gray-700 dark:text-gray-300">AP / Wireless</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-3 h-3 rounded-full" style="background-color: #EF4444;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Firewall</span>
<span class="text-gray-700 dark:text-gray-300">Firewall</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-3 h-3" style="background-color: #F59E0B;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Server</span>
<span class="text-gray-700 dark:text-gray-300">Server</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-3 h-3 rounded-full bg-cool-steel-400"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Unknown</span>
<div class="w-3 h-3 rounded-full bg-gray-400"></div>
<span class="text-gray-700 dark:text-gray-300">Unknown</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-3 h-3 rounded-full border-2 border-dashed border-cool-steel-400 opacity-60">
<div class="w-3 h-3 rounded-full border-2 border-dashed border-gray-400 opacity-60">
</div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Discovered</span>
<span class="text-gray-700 dark:text-gray-300">Discovered</span>
</div>
</div>
<%!-- RF Link health legend hidden (TOW-44) — keep code intact
<div class="flex flex-wrap items-center gap-x-5 gap-y-1 text-xs">
<span class="font-medium text-cool-steel-600 dark:text-cool-steel-400">RF Links:</span>
<span class="font-medium text-gray-600 dark:text-gray-400">RF Links:</span>
<div class="flex items-center space-x-1.5">
<div class="w-6 h-1 rounded" style="background-color: #10B981;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Good signal</span>
<span class="text-gray-700 dark:text-gray-300">Good signal</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-6 h-1 rounded" style="background-color: #F59E0B;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Degraded</span>
<span class="text-gray-700 dark:text-gray-300">Degraded</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-6 h-1 rounded" style="background-color: #EF4444;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">Critical</span>
<span class="text-gray-700 dark:text-gray-300">Critical</span>
</div>
</div>
--%>
<%!-- Link types --%>
<div class="flex flex-wrap items-center gap-x-5 gap-y-1 text-xs">
<span class="font-medium text-cool-steel-600 dark:text-cool-steel-400">
Link types:
</span>
<span class="font-medium text-gray-600 dark:text-gray-400">Link types:</span>
<div class="flex items-center space-x-1.5">
<div class="w-6 h-0.5" style="background-color: #6B7280;"></div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">LLDP/CDP</span>
<span class="text-gray-700 dark:text-gray-300">LLDP/CDP</span>
</div>
<div class="flex items-center space-x-1.5">
<div class="w-6 h-0.5 border-t-2 border-dashed" style="border-color: #6B7280;">
</div>
<span class="text-cool-steel-700 dark:text-cool-steel-300">MAC/ARP inferred</span>
<span class="text-gray-700 dark:text-gray-300">MAC/ARP inferred</span>
</div>
<div class="flex items-center space-x-1.5">
<span class="text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-500 dark:text-gray-400">
Line thickness = bandwidth capacity
</span>
</div>
@ -635,17 +610,14 @@
<!-- Empty State -->
<%= if @topology.stats.total_devices == 0 do %>
<div class="text-center py-16">
<.icon
name="hero-map"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-map" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No network topology available")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Add devices with SNMP enabled to start building your network map.")}
</p>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("The topology is automatically discovered via LLDP and CDP protocols.")}
</p>
<div class="mt-6">

View file

@ -15,13 +15,12 @@
class={[
"flex items-center gap-1.5 text-sm font-medium px-2 py-1 rounded transition",
if(step == @step,
do:
"text-cerulean-600 dark:text-cerulean-400 bg-cerulean-50 dark:bg-cerulean-900/30",
do: "text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/30",
else:
if(step_index(step) < step_index(@step),
do:
"text-cool-steel-600 dark:text-cool-steel-300 hover:text-cool-steel-900 dark:hover:text-white",
else: "text-cool-steel-400 dark:text-cool-steel-500"
"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white",
else: "text-gray-400 dark:text-gray-500"
)
)
]}
@ -29,12 +28,11 @@
<span class={[
"flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold",
if(step == @step,
do: "bg-cerulean-600 text-white dark:bg-cerulean-500",
do: "bg-indigo-600 text-white dark:bg-indigo-500",
else:
if(step_index(step) < step_index(@step),
do: "bg-cool-steel-400 dark:bg-cool-steel-500 text-white",
else:
"bg-cool-steel-200 dark:bg-cool-steel-700 text-cool-steel-500 dark:text-cool-steel-400"
do: "bg-gray-400 dark:bg-gray-500 text-white",
else: "bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
)
)
]}>
@ -49,7 +47,7 @@
<%= if step != List.last(@steps) do %>
<.icon
name="hero-chevron-right-mini"
class="h-4 w-4 text-cool-steel-300 dark:text-cool-steel-600"
class="h-4 w-4 text-gray-300 dark:text-gray-600"
/>
<% end %>
</li>
@ -58,13 +56,13 @@
</nav>
<%!-- Step content --%>
<div class="bg-white dark:bg-white/5 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6">
<div class="bg-white dark:bg-white/5 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<%= case @step do %>
<% :snmp -> %>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-1">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t("SNMP Configuration")}
</h2>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t("Set organization-wide SNMP defaults. Can be overridden per-site or per-device.")}
</p>
@ -118,17 +116,17 @@
/>
<% end %>
<div class="flex items-center justify-between pt-4 border-t border-cool-steel-200 dark:border-white/10">
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
<button
type="button"
phx-click="skip"
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
{t("Skip")}
</button>
<button
type="submit"
class="rounded-md bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Save & Continue")}
</button>
@ -136,10 +134,10 @@
</div>
</.form>
<% :site -> %>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-1">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t("Add Your First Site")}
</h2>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t("Sites represent physical locations — tower sites, POPs, data centers, etc.")}
</p>
@ -175,17 +173,17 @@
/>
</div>
<div class="flex items-center justify-between pt-4 border-t border-cool-steel-200 dark:border-white/10">
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
<button
type="button"
phx-click="skip"
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
{t("Skip")}
</button>
<button
type="submit"
class="rounded-md bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Create Site & Continue")}
</button>
@ -193,10 +191,10 @@
</div>
</.form>
<% :billing -> %>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-1">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t("Connect a Billing Platform")}
</h2>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t(
"Optional. Sync subscriber data for outage impact analysis and inventory reconciliation."
)}
@ -209,13 +207,10 @@
type="button"
phx-click="select_provider"
phx-value-provider={provider.id}
class="flex items-center gap-3 p-4 rounded-lg border border-cool-steel-200 dark:border-white/10 hover:border-cerulean-400 dark:hover:border-cerulean-500 hover:bg-cool-steel-50 dark:hover:bg-white/5 transition text-left"
class="flex items-center gap-3 p-4 rounded-lg border border-gray-200 dark:border-white/10 hover:border-indigo-400 dark:hover:border-indigo-500 hover:bg-gray-50 dark:hover:bg-white/5 transition text-left"
>
<.icon
name={provider.icon}
class="h-6 w-6 text-cool-steel-400 dark:text-cool-steel-500"
/>
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<.icon name={provider.icon} class="h-6 w-6 text-gray-400 dark:text-gray-500" />
<span class="text-sm font-medium text-gray-900 dark:text-white">
{provider.name}
</span>
</button>
@ -227,7 +222,7 @@
type="button"
phx-click="select_provider"
phx-value-provider=""
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
← {t("Back to providers")}
</button>
@ -278,17 +273,17 @@
/>
<% end %>
<div class="flex items-center justify-between pt-4 border-t border-cool-steel-200 dark:border-white/10">
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
<button
type="button"
phx-click="skip"
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200"
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
{t("Skip")}
</button>
<button
type="submit"
class="rounded-md bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Save & Continue")}
</button>
@ -298,21 +293,21 @@
<% end %>
<%= if @selected_provider == nil do %>
<div class="flex items-center justify-between pt-4 border-t border-cool-steel-200 dark:border-white/10">
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
<button
type="button"
phx-click="skip"
class="text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400 hover:text-cool-steel-700 dark:hover:text-cool-steel-200"
class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
>
{t("Skip — I'll set this up later")}
</button>
</div>
<% end %>
<% :agent -> %>
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-1">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{t("Deploy Agent")}
</h2>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t(
"The TowerOps agent polls your network devices via SNMP. Deploy it on a host with network access to your infrastructure."
)}
@ -321,20 +316,20 @@
<%= if @agent_token do %>
<div class="space-y-4">
<%= if @agent_token_value do %>
<div class="rounded-md bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 p-3">
<p class="text-sm text-wheat-800 dark:text-wheat-200">
<div class="rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-3">
<p class="text-sm text-amber-800 dark:text-amber-200">
<.icon name="hero-exclamation-triangle" class="h-4 w-4 inline -mt-0.5" />
{t("Save this token — it won't be shown again.")}
</p>
<code class="mt-2 block text-xs font-mono bg-wheat-100 dark:bg-wheat-900/40 rounded p-2 break-all select-all text-wheat-900 dark:text-wheat-100">
<code class="mt-2 block text-xs font-mono bg-amber-100 dark:bg-amber-900/40 rounded p-2 break-all select-all text-amber-900 dark:text-amber-100">
{@agent_token_value}
</code>
</div>
<% else %>
<div class="rounded-md bg-cool-steel-50 dark:bg-white/5 border border-cool-steel-200 dark:border-white/10 p-3">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-md bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 p-3">
<p class="text-xs text-gray-500 dark:text-gray-400">
{t("Agent:")}
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{@agent_token.name}
</span>
</p>
@ -342,10 +337,10 @@
<% end %>
<div>
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Docker")}
</h3>
<pre class="text-xs font-mono bg-cool-steel-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>docker run -d \
<pre class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>docker run -d \
--name towerops-agent \
--restart unless-stopped \
-e TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
@ -355,26 +350,26 @@
</div>
<div>
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Systemd")}
</h3>
<pre class="text-xs font-mono bg-cool-steel-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>curl -sSL https://install.towerops.net | \
<pre class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>curl -sSL https://install.towerops.net | \
TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
TOWEROPS_SERVER=<%= ToweropsWeb.Endpoint.url() %> \
bash</code></pre>
</div>
</div>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Failed to create agent token. You can create one later in Settings → Agents.")}
</p>
<% end %>
<div class="flex items-center justify-end pt-6 border-t border-cool-steel-200 dark:border-white/10 mt-6">
<div class="flex items-center justify-end pt-6 border-t border-gray-200 dark:border-white/10 mt-6">
<button
type="button"
phx-click="finish"
class="rounded-md bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Done — Go to Dashboard")}
</button>

View file

@ -3,17 +3,17 @@
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings", navigate: ~p"/orgs/#{@organization.slug}/settings"},
%{label: "Integrations", navigate: ~p"/orgs/#{@organization.slug}/settings/integrations"},
%{label: "Gaiia"}
]} />
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Link Devices & Sites to Gaiia")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t(
"For each of your monitored devices and sites, find the matching Gaiia inventory item. This enables subscriber impact data when devices go down."
)}
@ -21,17 +21,17 @@
</div>
<%!-- Sub-navigation tabs --%>
<div class="mt-4 border-b border-cool-steel-200 dark:border-white/10">
<div class="mt-4 border-b border-gray-200 dark:border-white/10">
<nav class="flex space-x-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="border-b-2 border-cerulean-500 px-1 pb-3 text-sm font-medium text-cerulean-600 dark:text-cerulean-400"
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
{t("Device & Site Linking")}
</.link>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-white"
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
{t("Comparison Report")}
</.link>
@ -39,7 +39,7 @@
</div>
<%!-- Tab navigation --%>
<div class="mt-6 border-b border-cool-steel-200 dark:border-white/10">
<div class="mt-6 border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8">
<.link
id="tab-devices"
@ -49,10 +49,9 @@
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
if(@tab == "devices",
do:
"border-cerulean-500 text-cerulean-600 dark:border-cerulean-400 dark:text-cerulean-400",
do: "border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400",
else:
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
)
]}
>
@ -66,10 +65,9 @@
class={[
"whitespace-nowrap border-b-2 px-1 py-4 text-sm font-medium",
if(@tab == "sites",
do:
"border-cerulean-500 text-cerulean-600 dark:border-cerulean-400 dark:text-cerulean-400",
do: "border-indigo-500 text-indigo-600 dark:border-indigo-400 dark:text-indigo-400",
else:
"border-transparent text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-cool-steel-300"
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
)
]}
>
@ -88,9 +86,9 @@
class={[
"rounded-full px-3 py-1 text-xs font-medium",
if(@filter == f,
do: "bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/40 dark:text-cerulean-300",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300",
else:
"bg-cool-steel-100 text-cool-steel-600 hover:bg-cool-steel-200 dark:bg-white/5 dark:text-cool-steel-400 dark:hover:bg-white/10"
"bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-white/5 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -103,13 +101,13 @@
<%= if @items == [] do %>
<div
id="empty-state"
class="rounded-lg border-2 border-dashed border-cool-steel-300 p-12 text-center dark:border-cool-steel-700"
class="rounded-lg border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-700"
>
<.icon
name={if(@tab == "sites", do: "hero-map-pin", else: "hero-cpu-chip")}
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
<%= case {@tab, @filter} do %>
<% {"devices", "linked"} -> %>
{t("No linked devices yet")}
@ -125,7 +123,7 @@
{t("No sites in your organization")}
<% end %>
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= case {@tab, @filter} do %>
<% {_, "unlinked"} -> %>
{t("Nothing left to do here.")}
@ -137,27 +135,27 @@
</p>
</div>
<% else %>
<div class="overflow-x-auto rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-white/5">
<div class="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">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{if @tab == "devices", do: t("Your Device"), else: t("Your Site")}
</th>
<%= if @tab == "devices" do %>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 sm:table-cell"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:table-cell"
>
{t("IP Address")}
</th>
<% end %>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 md:table-cell"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 md:table-cell"
>
{if @tab == "devices",
do: t("Gaiia Inventory Item"),
@ -165,13 +163,13 @@
</th>
<th
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for row <- @items do %>
<% row_id = if @tab == "devices", do: row.device.id, else: row.site.id
@ -182,7 +180,7 @@
<tr
id={"row-#{row_id}"}
class={[
"hover:bg-cool-steel-50 dark:hover:bg-white/5",
"hover:bg-gray-50 dark:hover:bg-white/5",
if(not is_linked, do: "cursor-pointer")
]}
phx-click={if(not is_linked, do: "start_link")}
@ -196,19 +194,19 @@
<div class="flex items-center gap-2 flex-wrap">
<.link
navigate={~p"/devices/#{row.device.id}"}
class="font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="font-medium text-gray-900 hover:text-indigo-600 dark:text-white dark:hover:text-indigo-400"
>
{row.device.name || row.device.ip_address || "Unnamed"}
</.link>
<%= if Map.has_key?(@suggestions, row.device.id) do %>
<span class="inline-flex items-center rounded-full bg-wheat-100 px-2 py-0.5 text-xs font-medium text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
<.icon name="hero-light-bulb-mini" class="mr-0.5 h-3 w-3" />
Match found
</span>
<% end %>
</div>
<%= if row.device.site do %>
<div class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{row.device.site.name}
</div>
<% end %>
@ -216,11 +214,11 @@
<% else %>
<div>
<div class="flex items-center gap-2 flex-wrap">
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{row.site.name || "Unnamed"}
</span>
<%= if Map.has_key?(@suggestions, row.site.id) do %>
<span class="inline-flex items-center rounded-full bg-wheat-100 px-2 py-0.5 text-xs font-medium text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
<.icon name="hero-light-bulb-mini" class="mr-0.5 h-3 w-3" />
Match found
</span>
@ -233,13 +231,13 @@
<%!-- IP Address (devices only) --%>
<%= if @tab == "devices" do %>
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400 sm:table-cell">
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400 sm:table-cell">
<%= if row.device.ip_address do %>
<a
href={"http://#{row.device.ip_address}"}
target="_blank"
rel="noopener noreferrer"
class="font-mono hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="font-mono hover:text-indigo-600 dark:hover:text-indigo-400"
onclick="event.stopPropagation()"
>
{row.device.ip_address}
@ -260,7 +258,7 @@
href={"#{@gaiia_app_url}/inventory/items/#{row.inventory_item.gaiia_id}"}
target="_blank"
rel="noopener noreferrer"
class="font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="font-medium text-gray-900 hover:text-indigo-600 dark:text-white dark:hover:text-indigo-400"
>
{row.inventory_item.name || "Unnamed"}
<.icon
@ -269,18 +267,18 @@
/>
</a>
<% else %>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{row.inventory_item.name || "Unnamed"}
</span>
<% end %>
<%= if row.inventory_item.model_name do %>
<div class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{row.inventory_item.model_name}
</div>
<% end %>
</div>
<% else %>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2.5 py-0.5 text-xs font-medium text-cool-steel-500 dark:bg-white/10 dark:text-cool-steel-400">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500 dark:bg-white/10 dark:text-gray-400">
Not linked
</span>
<% end %>
@ -291,7 +289,7 @@
href={"#{@gaiia_app_url}/network-sites/#{row.network_site.gaiia_id}"}
target="_blank"
rel="noopener noreferrer"
class="font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="font-medium text-gray-900 hover:text-indigo-600 dark:text-white dark:hover:text-indigo-400"
onclick="event.stopPropagation()"
>
{row.network_site.name || "Unnamed"}
@ -301,12 +299,12 @@
/>
</a>
<% else %>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{row.network_site.name || "Unnamed"}
</span>
<% end %>
<% else %>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2.5 py-0.5 text-xs font-medium text-cool-steel-500 dark:bg-white/10 dark:text-cool-steel-400">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500 dark:bg-white/10 dark:text-gray-400">
Not linked
</span>
<% end %>
@ -325,7 +323,7 @@
data-confirm={
t("Remove the link between this device and its Gaiia item?")
}
class="text-sweet-salmon-600 hover:text-sweet-salmon-800 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
{t("Unlink")}
</button>
@ -335,7 +333,7 @@
id={"link-#{row.device.id}"}
phx-click="start_link"
phx-value-id={row.device.id}
class="text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Link")}
</button>
@ -350,7 +348,7 @@
data-confirm={
t("Remove the link between this site and its Gaiia network site?")
}
class="text-sweet-salmon-600 hover:text-sweet-salmon-800 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
{t("Unlink")}
</button>
@ -360,7 +358,7 @@
id={"link-#{row.site.id}"}
phx-click="start_link"
phx-value-id={row.site.id}
class="text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Link")}
</button>
@ -371,19 +369,19 @@
<%!-- Inline linking row --%>
<%= if row_id == @linking_id do %>
<tr id={"linking-row-#{row_id}"} class="bg-cerulean-50 dark:bg-cerulean-900/20">
<tr id={"linking-row-#{row_id}"} class="bg-indigo-50 dark:bg-indigo-900/20">
<td colspan={if(@tab == "devices", do: "4", else: "3")} class="px-4 py-4">
<div class="flex items-start gap-4">
<div class="flex-1">
<p class="mb-2 text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
<%= if @tab == "devices" do %>
Find a Gaiia item to link to
<span class="text-cerulean-600 dark:text-cerulean-400">
<span class="text-indigo-600 dark:text-indigo-400">
{row.device.name || row.device.ip_address}
</span>
<% else %>
Find a Gaiia network site to link to
<span class="text-cerulean-600 dark:text-cerulean-400">
<span class="text-indigo-600 dark:text-indigo-400">
{row.site.name}
</span>
<% end %>
@ -392,7 +390,7 @@
<%!-- Suggestions --%>
<%= if suggestions = @suggestions[row_id] do %>
<div class="mb-3">
<p class="mb-1 text-xs font-medium text-wheat-700 dark:text-wheat-400">
<p class="mb-1 text-xs font-medium text-amber-700 dark:text-amber-400">
<.icon name="hero-light-bulb-mini" class="inline h-3 w-3 mr-0.5" />
{t("Suggested match (same IP):")}
</p>
@ -406,15 +404,15 @@
phx-value-device-id={if(@tab == "devices", do: row.device.id)}
phx-value-site-id={if(@tab == "sites", do: row.site.id)}
phx-value-gaiia-id={suggestion.entity.gaiia_id}
class="inline-flex items-center gap-1.5 rounded-md border border-wheat-300 bg-wheat-50 px-2.5 py-1.5 text-xs font-medium text-wheat-800 hover:bg-wheat-100 dark:border-wheat-700 dark:bg-wheat-900/20 dark:text-wheat-300 dark:hover:bg-wheat-900/40"
class="inline-flex items-center gap-1.5 rounded-md border border-amber-300 bg-amber-50 px-2.5 py-1.5 text-xs font-medium text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300 dark:hover:bg-amber-900/40"
>
{suggestion.entity.name}
<%= if @tab == "devices" && suggestion.entity.ip_address do %>
<span class="text-wheat-600 dark:text-wheat-400">
<span class="text-amber-600 dark:text-amber-400">
{suggestion.entity.ip_address}
</span>
<% end %>
<span class="rounded bg-wheat-200 px-1 text-wheat-700 dark:bg-wheat-800 dark:text-wheat-300">
<span class="rounded bg-amber-200 px-1 text-amber-700 dark:bg-amber-800 dark:text-amber-300">
{suggestion.match_type}
</span>
</button>
@ -438,13 +436,13 @@
}
autocomplete="off"
phx-debounce="300"
class="block w-full rounded-md border-cool-steel-300 text-sm shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-white/10 dark:bg-white/5 dark:text-white"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
</form>
<%= if @search_results != [] do %>
<div class="mt-2 max-h-48 overflow-y-auto rounded-md border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800">
<ul class="divide-y divide-cool-steel-200 dark:divide-white/5">
<div class="mt-2 max-h-48 overflow-y-auto rounded-md border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800">
<ul class="divide-y divide-gray-200 dark:divide-white/5">
<li :for={result <- @search_results}>
<button
type="button"
@ -455,13 +453,13 @@
phx-value-device-id={if(@tab == "devices", do: @linking_id)}
phx-value-site-id={if(@tab == "sites", do: @linking_id)}
phx-value-gaiia-id={result.gaiia_id}
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-cool-steel-50 dark:hover:bg-white/5"
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-white/5"
>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{result.name || "Unnamed"}
</span>
<%= if @tab == "devices" do %>
<span class="flex items-center gap-2 text-cool-steel-500 dark:text-cool-steel-400">
<span class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
<%= if result.model_name do %>
<span class="text-xs">{result.model_name}</span>
<% end %>
@ -475,7 +473,7 @@
<% end %>
<%= if @search_query != "" and String.length(@search_query) >= 2 and @search_results == [] do %>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
No Gaiia {if(@tab == "sites",
do: "network sites",
else: "inventory items"
@ -487,7 +485,7 @@
type="button"
id="cancel-link"
phx-click="cancel_link"
class="flex-shrink-0 self-start text-sm text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
class="flex-shrink-0 self-start text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
{t("Cancel")}
</button>

View file

@ -21,17 +21,17 @@
</.header>
<%!-- Sub-navigation tabs --%>
<div class="mt-4 border-b border-cool-steel-200 dark:border-white/10">
<div class="mt-4 border-b border-gray-200 dark:border-white/10">
<nav class="flex space-x-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-white"
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
{t("Entity Mapping")}
</.link>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="border-b-2 border-cerulean-500 px-1 pb-3 text-sm font-medium text-cerulean-600 dark:text-cerulean-400"
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
{t("Reconciliation")}
</.link>
@ -39,29 +39,29 @@
</div>
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-5">
<div class="rounded-lg border border-cool-steel-200 bg-white p-4 dark:border-white/10 dark:bg-cool-steel-800/50">
<div class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-gray-900 dark:text-white">
{@report.summary.total_mapped}
</div>
<div class="text-sm text-cool-steel-600 dark:text-cool-steel-400">Mapped</div>
<div class="text-sm text-gray-600 dark:text-gray-400">Mapped</div>
</div>
<div class="rounded-lg border border-wheat-200 bg-wheat-50 p-4 dark:border-wheat-600/30 dark:bg-wheat-900/20">
<div class="text-2xl font-bold text-wheat-800 dark:text-wheat-400">
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-600/30 dark:bg-yellow-900/20">
<div class="text-2xl font-bold text-yellow-800 dark:text-yellow-400">
{@report.summary.missing_mapping_count}
</div>
<div class="text-sm text-wheat-700 dark:text-wheat-500">Unmapped Items</div>
<div class="text-sm text-yellow-700 dark:text-yellow-500">Unmapped Items</div>
</div>
<div class="rounded-lg border border-cerulean-200 bg-cerulean-50 p-4 dark:border-cerulean-600/30 dark:bg-cerulean-900/20">
<div class="text-2xl font-bold text-cerulean-800 dark:text-cerulean-400">
<div class="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-600/30 dark:bg-blue-900/20">
<div class="text-2xl font-bold text-blue-800 dark:text-blue-400">
{@report.summary.untracked_count}
</div>
<div class="text-sm text-cerulean-700 dark:text-cerulean-500">Untracked Devices</div>
<div class="text-sm text-blue-700 dark:text-blue-500">Untracked Devices</div>
</div>
<div class="rounded-lg border border-sweet-salmon-200 bg-sweet-salmon-50 p-4 dark:border-sweet-salmon-600/30 dark:bg-sweet-salmon-900/20">
<div class="text-2xl font-bold text-sweet-salmon-800 dark:text-sweet-salmon-400">
<div class="rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-600/30 dark:bg-red-900/20">
<div class="text-2xl font-bold text-red-800 dark:text-red-400">
{@report.summary.mismatch_count}
</div>
<div class="text-sm text-sweet-salmon-700 dark:text-sweet-salmon-500">Data Mismatches</div>
<div class="text-sm text-red-700 dark:text-red-500">Data Mismatches</div>
</div>
<div class="rounded-lg border border-purple-200 bg-purple-50 p-4 dark:border-purple-600/30 dark:bg-purple-900/20">
<div class="text-2xl font-bold text-purple-800 dark:text-purple-400">
@ -72,7 +72,7 @@
</div>
<div class="mt-6 mb-6 flex items-center justify-between">
<div class="inline-flex rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="inline-flex rounded-lg border border-gray-200 dark:border-white/10">
<.link
patch={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=summary"
@ -80,9 +80,9 @@
class={[
"px-4 py-2 text-sm font-medium rounded-l-lg",
@active_tab == "summary" &&
"bg-cerulean-600 text-white dark:bg-cerulean-500",
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "summary" &&
"bg-white text-cool-steel-700 hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
{t("Summary")}
@ -92,11 +92,11 @@
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=untracked"
}
class={[
"px-4 py-2 text-sm font-medium border-l border-cool-steel-200 dark:border-white/10",
"px-4 py-2 text-sm font-medium border-l border-gray-200 dark:border-white/10",
@active_tab == "untracked" &&
"bg-cerulean-600 text-white dark:bg-cerulean-500",
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "untracked" &&
"bg-white text-cool-steel-700 hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Untracked ({@report.summary.untracked_count})
@ -106,11 +106,11 @@
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=mismatches"
}
class={[
"px-4 py-2 text-sm font-medium border-l border-cool-steel-200 dark:border-white/10",
"px-4 py-2 text-sm font-medium border-l border-gray-200 dark:border-white/10",
@active_tab == "mismatches" &&
"bg-cerulean-600 text-white dark:bg-cerulean-500",
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "mismatches" &&
"bg-white text-cool-steel-700 hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Mismatches ({@report.summary.mismatch_count})
@ -120,11 +120,11 @@
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=unmapped"
}
class={[
"px-4 py-2 text-sm font-medium border-l border-cool-steel-200 dark:border-white/10",
"px-4 py-2 text-sm font-medium border-l border-gray-200 dark:border-white/10",
@active_tab == "unmapped" &&
"bg-cerulean-600 text-white dark:bg-cerulean-500",
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "unmapped" &&
"bg-white text-cool-steel-700 hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Unmapped ({@report.summary.missing_mapping_count})
@ -134,11 +134,11 @@
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation?tab=ghosts"
}
class={[
"px-4 py-2 text-sm font-medium rounded-r-lg border-l border-cool-steel-200 dark:border-white/10",
"px-4 py-2 text-sm font-medium rounded-r-lg border-l border-gray-200 dark:border-white/10",
@active_tab == "ghosts" &&
"bg-cerulean-600 text-white dark:bg-cerulean-500",
"bg-blue-600 text-white dark:bg-blue-500",
@active_tab != "ghosts" &&
"bg-white text-cool-steel-700 hover:bg-cool-steel-50 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Ghosts ({@report.summary.ghost_count})
@ -147,7 +147,7 @@
<button
phx-click="refresh"
class="inline-flex items-center gap-1.5 rounded-lg border border-cool-steel-200 bg-white px-3 py-2 text-sm font-medium text-cool-steel-700 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800/50 dark:text-cool-steel-300 dark:hover:bg-cool-steel-800"
class="inline-flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
>
<.icon name="hero-arrow-path" class="h-4 w-4" />
{t("Refresh")}
@ -158,61 +158,61 @@
<% "untracked" -> %>
<div class="space-y-2">
<%= if @report.untracked_devices == [] do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("All Towerops devices have matching Gaiia inventory items.")}
</p>
<% else %>
<div class="mb-4 flex items-center gap-3">
<button
phx-click="auto_match"
class="inline-flex items-center gap-1.5 rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-medium text-white hover:bg-cerulean-700"
class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
>
<.icon name="hero-bolt" class="h-4 w-4" />
{t("Auto-Match All")}
</button>
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-sm text-gray-500 dark:text-gray-400">
{t("Links by MAC, IP, or site assignment.")}
</span>
</div>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Site")}
</th>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for entry <- @report.untracked_devices do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{entry.device.id}"}
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{entry.device.name}
</.link>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<a
href={"http://#{entry.device.ip_address}"}
target="_blank"
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{entry.device.ip_address}
</a>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
<%= if entry.device.site do %>
{entry.device.site.name}
<% end %>
@ -224,7 +224,7 @@
<form phx-change="select_manufacturer">
<select
name="manufacturer_id"
class="rounded border border-cool-steel-300 bg-white px-2 py-1 text-xs text-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300"
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
>
<option value="">{t("Select manufacturer")}</option>
<%= for mfr <- @manufacturers do %>
@ -245,21 +245,21 @@
<input type="hidden" name="device_id" value={entry.device.id} />
<select
name="model_id"
class="rounded border border-cool-steel-300 bg-white px-2 py-1 text-xs text-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300"
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
>
<option value="">{t("Select model")}</option>
<%= for model <- Map.get(@models, @selected_manufacturer, []) do %>
<option value={model["id"]}>{model["name"]}</option>
<% end %>
</select>
<button class="rounded bg-cerulean-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-cerulean-700">
<button class="rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700">
{t("Create")}
</button>
</form>
<% end %>
<button
phx-click="cancel_create"
class="rounded px-2 py-1 text-xs text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400"
class="rounded px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
>
{t("Cancel")}
</button>
@ -273,13 +273,13 @@
name="query"
value={@link_search_query}
placeholder={t("Search by name or IP...")}
class="rounded border border-cool-steel-300 bg-white px-2 py-1 text-xs text-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300"
class="rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300"
autocomplete="off"
/>
</form>
<button
phx-click="cancel_link_search"
class="rounded px-2 py-1 text-xs text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400"
class="rounded px-2 py-1 text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400"
>
{t("Cancel")}
</button>
@ -289,14 +289,14 @@
<button
phx-click="start_link_search"
phx-value-device_id={entry.device.id}
class="rounded bg-white px-2.5 py-1 text-xs font-medium text-cool-steel-700 border border-cool-steel-300 hover:bg-cool-steel-50 dark:border-white/10 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
class="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
{t("Link")}
</button>
<button
phx-click="open_create_form"
phx-value-device_id={entry.device.id}
class="inline-flex items-center gap-1 rounded bg-cerulean-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-cerulean-700"
class="inline-flex items-center gap-1 rounded bg-indigo-600 px-2.5 py-1 text-xs font-medium text-white hover:bg-indigo-700"
>
{t("Create in Gaiia")}
</button>
@ -307,15 +307,15 @@
<%!-- Search results row for link search --%>
<%= if @open_link_for == entry.device.id and @link_search_results != [] do %>
<tr>
<td colspan="4" class="px-4 py-2 bg-cool-steel-50 dark:bg-cool-steel-800/50">
<td colspan="4" class="px-4 py-2 bg-gray-50 dark:bg-gray-800/50">
<div class="space-y-1">
<%= for item <- @link_search_results do %>
<div class="flex items-center justify-between rounded border border-cool-steel-200 bg-white px-3 py-2 dark:border-white/10 dark:bg-cool-steel-800">
<div class="flex items-center justify-between rounded border border-gray-200 bg-white px-3 py-2 dark:border-white/10 dark:bg-gray-800">
<div>
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{item.name}
</span>
<span class="ml-2 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
{item.ip_address || "—"}
</span>
</div>
@ -323,7 +323,7 @@
phx-click="link_to_gaiia"
phx-value-device_id={entry.device.id}
phx-value-gaiia_id={item.gaiia_id}
class="rounded bg-cerulean-600 px-2 py-1 text-xs font-medium text-white hover:bg-cerulean-700"
class="rounded bg-indigo-600 px-2 py-1 text-xs font-medium text-white hover:bg-indigo-700"
>
{t("Link")}
</button>
@ -342,46 +342,46 @@
<% "mismatches" -> %>
<div class="space-y-2">
<%= if @report.data_mismatches == [] do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("No data mismatches found between mapped devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Field")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Towerops")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Gaiia")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for mismatch <- @report.data_mismatches do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{mismatch.device.id}"}
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{mismatch.device.name}
</.link>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{mismatch.field}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-900 dark:text-white">
{mismatch.towerops_value}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-sweet-salmon-600 dark:text-sweet-salmon-400">
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-red-600 dark:text-red-400">
{mismatch.gaiia_value}
</td>
</tr>
@ -394,41 +394,41 @@
<% "unmapped" -> %>
<div class="space-y-2">
<%= if @report.missing_mappings == [] do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("All Gaiia inventory items are mapped to Towerops devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Gaiia Item")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Category")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Status")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for entry <- @report.missing_mappings do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{entry.inventory_item.name}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-600 dark:text-gray-400">
{entry.inventory_item.ip_address || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.inventory_item.category || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.inventory_item.status || "—"}
</td>
</tr>
@ -441,43 +441,43 @@
<% "ghosts" -> %>
<div class="space-y-2">
<%= if @report.ghost_devices == [] do %>
<p class="py-8 text-center text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
{t("No stale mappings — all linked Gaiia items reference existing Towerops devices.")}
</p>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t(
"These Gaiia inventory items are linked to Towerops devices that no longer exist. Unlink them to restore them to the unmapped pool."
)}
</p>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Gaiia Item")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Status")}
</th>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for entry <- @report.ghost_devices do %>
<tr>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{entry.inventory_item.name}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm font-mono text-gray-600 dark:text-gray-400">
{entry.inventory_item.ip_address || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entry.inventory_item.status || "—"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm">
@ -498,7 +498,7 @@
</div>
<% _ -> %>
<div class="space-y-4">
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="text-sm text-gray-600 dark:text-gray-400">
Last reconciled: {ToweropsWeb.TimeHelpers.format_datetime(
@report.reconciled_at,
@current_scope.timezone,
@ -510,61 +510,59 @@
<div class="py-8 text-center">
<.icon
name="hero-check-circle"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No inventory data")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Sync Gaiia data and map inventory items to see reconciliation results.")}
</p>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="mt-4 inline-flex items-center gap-1 text-sm font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="mt-4 inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Go to Entity Mapping <.icon name="hero-arrow-right" class="h-4 w-4" />
</.link>
</div>
<% else %>
<div class="rounded-lg border border-cool-steel-200 bg-white p-6 dark:border-white/10 dark:bg-cool-steel-800/50">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
{t("Overview")}
</h3>
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-gray-800/50">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Overview")}</h3>
<dl class="mt-4 space-y-3">
<div class="flex justify-between text-sm">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-gray-600 dark:text-gray-400">
Mapped devices (Gaiia ↔ Towerops)
</dt>
<dd class="font-medium text-cool-steel-900 dark:text-white">
<dd class="font-medium text-gray-900 dark:text-white">
{@report.summary.total_mapped}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-gray-600 dark:text-gray-400">
{t("Unmapped Gaiia items (no Towerops link)")}
</dt>
<dd class="font-medium text-wheat-600 dark:text-wheat-400">
<dd class="font-medium text-yellow-600 dark:text-yellow-400">
{@report.summary.missing_mapping_count}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-gray-600 dark:text-gray-400">
{t("Untracked Towerops devices (not in Gaiia)")}
</dt>
<dd class="font-medium text-cerulean-600 dark:text-cerulean-400">
<dd class="font-medium text-blue-600 dark:text-blue-400">
{@report.summary.untracked_count}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-gray-600 dark:text-gray-400">
{t("Data mismatches (IP, serial, etc.)")}
</dt>
<dd class="font-medium text-sweet-salmon-600 dark:text-sweet-salmon-400">
<dd class="font-medium text-red-600 dark:text-red-400">
{@report.summary.mismatch_count}
</dd>
</div>
<div class="flex justify-between text-sm">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-gray-600 dark:text-gray-400">
{t("Ghost mappings (stale device links)")}
</dt>
<dd class="font-medium text-purple-600 dark:text-purple-400">

View file

@ -3,19 +3,19 @@
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<div class="mb-4">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings"}
class="inline-flex items-center gap-1 text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Settings
</.link>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Integrations")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Connect third-party services to enhance your monitoring capabilities.")}
</p>
</div>
@ -23,10 +23,10 @@
<div class="mt-8 space-y-10">
<div :for={category <- @provider_categories}>
<div class="mb-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{category.name}
</h2>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{category.description}
</p>
</div>
@ -38,41 +38,41 @@
@integrations[p.id] && @integrations[p.id].enabled
end) %>
<div class="mb-4 flex items-center gap-2">
<span class="inline-flex items-center gap-1 rounded-full bg-cerulean-100 px-3 py-1 text-xs font-semibold text-cerulean-700 dark:bg-cerulean-900/40 dark:text-cerulean-300">
<span class="inline-flex items-center gap-1 rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300">
<.icon name="hero-arrow-path" class="h-3.5 w-3.5" /> Choose one
</span>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
— only one billing platform can be active at a time
</span>
</div>
<div class="rounded-lg border-2 border-cerulean-200 bg-white p-6 dark:border-cerulean-800/50 dark:bg-white/5">
<div class="rounded-lg border-2 border-indigo-200 bg-white p-6 dark:border-indigo-800/50 dark:bg-white/5">
<%= if active_billing do %>
<%!-- Active billing provider --%>
<% integration = @integrations[active_billing.id] %>
<div class="flex items-start justify-between">
<div class="flex items-start gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-cerulean-50 dark:bg-cerulean-900/30">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
<.icon
name={active_billing.icon}
class="h-6 w-6 text-cerulean-600 dark:text-cerulean-400"
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
/>
</div>
<div>
<div class="flex items-center gap-3">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{active_billing.name}
</h3>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/30 dark:text-green-400">
Connected
</span>
</div>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{active_billing.description}
</p>
<div class="mt-3 flex flex-wrap items-center gap-4">
<%= if integration.last_synced_at do %>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
{t("Last synced:")}
<.timestamp
datetime={integration.last_synced_at}
@ -90,20 +90,20 @@
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
"partial" ->
"bg-wheat-100 text-wheat-700 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
"failed" ->
"bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
_ ->
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
end
]}>
{String.capitalize(integration.last_sync_status)}
</span>
<% end %>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500">
<span class="text-xs text-gray-400 dark:text-gray-500">
· Syncs every {humanize_interval(integration.sync_interval_minutes)}
<%= if integration.last_synced_at do %>
· Next in ~{next_sync_minutes(
@ -118,7 +118,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
}
class="text-sm text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Device & Site Linking →
</.link>
@ -126,7 +126,7 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
}
class="text-sm text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Comparison Report →
</.link>
@ -140,7 +140,7 @@
type="button"
phx-click="toggle_enabled"
phx-value-provider={active_billing.id}
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-cerulean-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cerulean-600 focus:ring-offset-2"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-indigo-600 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
>
<span class="pointer-events-none inline-block h-5 w-5 translate-x-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out" />
</button>
@ -149,7 +149,7 @@
id={"configure-#{active_billing.id}"}
phx-click="configure"
phx-value-provider={active_billing.id}
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-cool-steel-900 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{t("Configure")}
</button>
@ -157,14 +157,14 @@
</div>
<%!-- Switch provider link --%>
<div class="mt-4 border-t border-cool-steel-100 pt-3 dark:border-white/5">
<p class="text-xs text-cool-steel-400 dark:text-cool-steel-500">
<div class="mt-4 border-t border-gray-100 pt-3 dark:border-white/5">
<p class="text-xs text-gray-400 dark:text-gray-500">
Using {active_billing.name}. To switch providers, disable this integration first.
</p>
</div>
<% else %>
<%!-- No active billing — show provider picker --%>
<p class="mb-4 text-sm text-cool-steel-600 dark:text-cool-steel-300 font-medium">
<p class="mb-4 text-sm text-gray-600 dark:text-gray-300 font-medium">
{t("Which billing platform does your organization use?")}
</p>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
@ -175,24 +175,24 @@
phx-click="configure"
phx-value-provider={provider.id}
class={[
"group relative flex flex-col items-center rounded-lg border-2 p-4 text-center transition-all hover:border-cerulean-300 hover:bg-cerulean-50/50 dark:hover:border-cerulean-700 dark:hover:bg-cerulean-900/10",
"group relative flex flex-col items-center rounded-lg border-2 p-4 text-center transition-all hover:border-indigo-300 hover:bg-indigo-50/50 dark:hover:border-indigo-700 dark:hover:bg-indigo-900/10",
if(@configuring == provider.id,
do:
"border-cerulean-500 bg-cerulean-50/50 dark:border-cerulean-600 dark:bg-cerulean-900/20",
else: "border-cool-steel-200 dark:border-white/10"
"border-indigo-500 bg-indigo-50/50 dark:border-indigo-600 dark:bg-indigo-900/20",
else: "border-gray-200 dark:border-white/10"
)
]}
>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-cerulean-50 group-hover:bg-cerulean-100 dark:bg-cerulean-900/30">
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50 group-hover:bg-indigo-100 dark:bg-indigo-900/30">
<.icon
name={provider.icon}
class="h-5 w-5 text-cerulean-600 dark:text-cerulean-400"
class="h-5 w-5 text-indigo-600 dark:text-indigo-400"
/>
</div>
<span class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<span class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{provider.name}
</span>
<span class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400 line-clamp-2">
<span class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
{provider.description}
</span>
</button>
@ -202,8 +202,8 @@
<%!-- Config form for whichever billing provider is being configured --%>
<%= if @configuring && find_category_id(@configuring) == "billing" do %>
<% provider = Enum.find(category.providers, &(&1.id == @configuring)) %>
<div class="mt-6 border-t border-cool-steel-200 pt-6 dark:border-white/10">
<h4 class="text-sm font-semibold text-cool-steel-900 dark:text-white mb-4">
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">
Configure {provider.name}
</h4>
<.form
@ -274,14 +274,14 @@
"rounded-md p-4",
case @test_result do
{:ok, _} -> "bg-green-50 dark:bg-green-900/20"
{:error, _} -> "bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20"
{:error, _} -> "bg-red-50 dark:bg-red-900/20"
end
]}>
<p class={[
"text-sm",
case @test_result do
{:ok, _} -> "text-green-800 dark:text-green-200"
{:error, _} -> "text-sweet-salmon-800 dark:text-sweet-salmon-200"
{:error, _} -> "text-red-800 dark:text-red-200"
end
]}>
{elem(@test_result, 1)}
@ -294,7 +294,7 @@
type="button"
id="test-connection"
phx-click="test_connection"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-cool-steel-900 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{t("Test Connection")}
</button>
@ -302,14 +302,14 @@
<button
type="button"
phx-click="close_config"
class="text-sm font-semibold text-cool-steel-900 dark:text-white"
class="text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Cancel")}
</button>
<button
type="submit"
phx-disable-with={t("Saving...")}
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Save")}
</button>
@ -321,11 +321,11 @@
<%!-- Gaiia webhook config --%>
<%= if @configuring == "gaiia" && @integrations["gaiia"] do %>
<div class="mt-6 border-t border-cool-steel-200 pt-6 dark:border-white/10">
<h4 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Webhook Configuration")}
</h4>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-300">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">
{t(
"Webhooks keep your data in sync in real-time. Without webhooks, data is only updated during scheduled syncs (every %{interval} minutes). With webhooks enabled, changes in Gaiia are reflected here within seconds.",
interval:
@ -336,18 +336,18 @@
)}
</p>
<div class="mt-4 rounded-md bg-cool-steel-50 p-4 dark:bg-white/5">
<h5 class="text-xs font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<div class="mt-4 rounded-md bg-gray-50 p-4 dark:bg-white/5">
<h5 class="text-xs font-semibold text-gray-700 dark:text-gray-300">
{t("What webhooks enable")}
</h5>
<ul class="mt-2 space-y-1.5 text-xs text-cool-steel-600 dark:text-cool-steel-400">
<ul class="mt-2 space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<li class="flex items-start gap-2">
<.icon
name="hero-user-group"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-cool-steel-400"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400"
/>
<span>
<span class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
<span class="font-medium text-gray-700 dark:text-gray-300">
Account changes
</span>
— new subscribers, status changes, and address updates are reflected instantly for accurate outage impact analysis.
@ -356,10 +356,10 @@
<li class="flex items-start gap-2">
<.icon
name="hero-credit-card"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-cool-steel-400"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400"
/>
<span>
<span class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
<span class="font-medium text-gray-700 dark:text-gray-300">
Billing subscription changes
</span>
— plan upgrades, cancellations, and MRR changes stay current so revenue impact estimates are always accurate.
@ -368,10 +368,10 @@
<li class="flex items-start gap-2">
<.icon
name="hero-wrench-screwdriver"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-cool-steel-400"
class="mt-0.5 h-3.5 w-3.5 shrink-0 text-gray-400"
/>
<span>
<span class="font-medium text-cool-steel-700 dark:text-cool-steel-300">
<span class="font-medium text-gray-700 dark:text-gray-300">
Inventory item changes
</span>
— equipment assignments, IP changes, and new installs are matched to your monitored devices automatically.
@ -382,7 +382,7 @@
<div class="mt-4 space-y-4">
<div>
<label class="block text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300">
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
{t("Webhook URL")}
</label>
<div class="mt-1 flex items-center gap-2">
@ -391,7 +391,7 @@
type="text"
readonly
value={webhook_url(@organization.id)}
class="block w-full rounded-md border-cool-steel-300 bg-cool-steel-50 py-1.5 text-sm text-cool-steel-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
class="block w-full rounded-md border-gray-300 bg-gray-50 py-1.5 text-sm text-gray-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
<button
type="button"
@ -399,7 +399,7 @@
phx-update="ignore"
data-target="#gaiia-webhook-url"
id="copy-webhook-url"
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-cool-steel-700 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-cool-steel-300 dark:ring-white/10 dark:hover:bg-white/20"
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
</button>
@ -407,7 +407,7 @@
</div>
<div>
<label class="block text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300">
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
{t("Webhook Secret")}
</label>
<div class="mt-1 flex items-center gap-2">
@ -416,7 +416,7 @@
type="text"
readonly
value={get_credential(@integrations["gaiia"], "webhook_secret")}
class="block w-full rounded-md border-cool-steel-300 bg-cool-steel-50 py-1.5 font-mono text-sm text-cool-steel-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
class="block w-full rounded-md border-gray-300 bg-gray-50 py-1.5 font-mono text-sm text-gray-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
<button
type="button"
@ -424,12 +424,12 @@
phx-update="ignore"
data-target="#gaiia-webhook-secret"
id="copy-webhook-secret"
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-cool-steel-700 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-cool-steel-300 dark:ring-white/10 dark:hover:bg-white/20"
class="inline-flex items-center gap-1 rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/20"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
</button>
</div>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t(
"Used to verify that incoming webhooks are genuinely from Gaiia. Keep this secret."
)}
@ -444,18 +444,18 @@
"Are you sure? Any existing Gaiia webhook using the current secret will stop working."
)
}
class="text-xs text-sweet-salmon-600 hover:text-sweet-salmon-700 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-xs text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Regenerate Secret")}
</button>
</div>
</div>
<div class="rounded-md bg-cerulean-50 p-3 dark:bg-cerulean-900/20">
<h5 class="text-xs font-medium text-cerulean-800 dark:text-cerulean-300">
<div class="rounded-md bg-blue-50 p-3 dark:bg-blue-900/20">
<h5 class="text-xs font-medium text-blue-800 dark:text-blue-300">
{t("Setup Instructions")}
</h5>
<ol class="mt-2 list-inside list-decimal space-y-1.5 text-xs text-cerulean-700 dark:text-cerulean-400">
<ol class="mt-2 list-inside list-decimal space-y-1.5 text-xs text-blue-700 dark:text-blue-400">
<li>
In your Gaiia admin panel, navigate to
<span class="font-semibold">Settings → Webhooks</span>
@ -479,7 +479,7 @@
</li>
<li>Save the webhook configuration in Gaiia</li>
</ol>
<p class="mt-3 text-xs text-cerulean-600 dark:text-cerulean-400/80">
<p class="mt-3 text-xs text-blue-600 dark:text-blue-400/80">
{t(
"Once configured, Gaiia will send updates to Towerops automatically. You can verify it's working by making a change in Gaiia and checking that it appears here within a few seconds."
)}
@ -495,21 +495,21 @@
<div class="space-y-4">
<div
:for={provider <- category.providers}
class="rounded-lg border border-cool-steel-200 bg-white p-6 dark:border-white/10 dark:bg-white/5"
class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-white/5"
>
<div class="flex items-start justify-between">
<div class="flex items-start gap-4">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-cerulean-50 dark:bg-cerulean-900/30">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
<.icon
name={provider.icon}
class="h-6 w-6 text-cerulean-600 dark:text-cerulean-400"
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
/>
</div>
<div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{provider.name}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{provider.description}
</p>
@ -520,15 +520,14 @@
if(integration.enabled,
do:
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
else:
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
else: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
)
]}>
{if integration.enabled, do: "Enabled", else: "Disabled"}
</span>
<%= if integration.last_synced_at do %>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
{t("Last synced:")}
<.timestamp
datetime={integration.last_synced_at}
@ -546,20 +545,20 @@
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
"partial" ->
"bg-wheat-100 text-wheat-700 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
"failed" ->
"bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
_ ->
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
end
]}>
{String.capitalize(integration.last_sync_status)}
</span>
<% end %>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500">
<span class="text-xs text-gray-400 dark:text-gray-500">
· Syncs every {humanize_interval(integration.sync_interval_minutes)}
<%= if integration.last_synced_at do %>
· Next in ~{next_sync_minutes(
@ -574,13 +573,13 @@
navigate={
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"
}
class="text-sm text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Manage Devices →
</.link>
<.link
navigate={~p"/insights?source=preseem"}
class="text-sm text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Network Insights →
</.link>
@ -597,10 +596,10 @@
phx-click="toggle_enabled"
phx-value-provider={provider.id}
class={[
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-cerulean-600 focus:ring-offset-2",
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2",
if(integration.enabled,
do: "bg-cerulean-600",
else: "bg-cool-steel-200 dark:bg-cool-steel-700"
do: "bg-indigo-600",
else: "bg-gray-200 dark:bg-gray-700"
)
]}
>
@ -616,7 +615,7 @@
id={"configure-#{provider.id}"}
phx-click="configure"
phx-value-provider={provider.id}
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-cool-steel-900 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{t("Configure")}
</button>
@ -624,7 +623,7 @@
</div>
<%= if @configuring == provider.id do %>
<div class="mt-6 border-t border-cool-steel-200 pt-6 dark:border-white/10">
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
<.form
for={@form}
id={"#{provider.id}-form"}
@ -698,14 +697,14 @@
"rounded-md p-4",
case @test_result do
{:ok, _} -> "bg-green-50 dark:bg-green-900/20"
{:error, _} -> "bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20"
{:error, _} -> "bg-red-50 dark:bg-red-900/20"
end
]}>
<p class={[
"text-sm",
case @test_result do
{:ok, _} -> "text-green-800 dark:text-green-200"
{:error, _} -> "text-sweet-salmon-800 dark:text-sweet-salmon-200"
{:error, _} -> "text-red-800 dark:text-red-200"
end
]}>
{elem(@test_result, 1)}
@ -718,7 +717,7 @@
type="button"
id="test-connection"
phx-click="test_connection"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-cool-steel-900 shadow-xs ring-1 ring-inset ring-cool-steel-300 hover:bg-cool-steel-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
{t("Test Connection")}
</button>
@ -726,14 +725,14 @@
<button
type="button"
phx-click="close_config"
class="text-sm font-semibold text-cool-steel-900 dark:text-white"
class="text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Cancel")}
</button>
<button
type="submit"
phx-disable-with={t("Saving...")}
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Save")}
</button>

View file

@ -108,13 +108,13 @@ defmodule ToweropsWeb.Org.PreseemDevicesLive do
defp match_confidence_classes(confidence) do
case confidence do
"unmatched" ->
"bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
"ambiguous" ->
"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400"
"manual" ->
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
_ ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"

View file

@ -3,17 +3,17 @@
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings", navigate: ~p"/orgs/#{@organization.slug}/settings"},
%{label: "Integrations", navigate: ~p"/orgs/#{@organization.slug}/settings/integrations"},
%{label: "Preseem"}
]} />
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Preseem Devices")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t(
"For each Preseem access point, find and link the matching TowerOps device. This enables subscriber impact data when access points go down."
)}
@ -21,17 +21,17 @@
</div>
<%!-- Sub-navigation tabs --%>
<div class="mt-4 border-b border-cool-steel-200 dark:border-white/10">
<div class="mt-4 border-b border-gray-200 dark:border-white/10">
<nav class="flex space-x-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"}
class="border-b-2 border-cerulean-500 px-1 pb-3 text-sm font-medium text-cerulean-600 dark:text-cerulean-400"
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
{t("Devices")}
</.link>
<.link
navigate={~p"/insights?source=preseem"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-cool-steel-500 hover:border-cool-steel-300 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:border-cool-steel-600 dark:hover:text-white"
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
{t("Insights")}
</.link>
@ -47,9 +47,9 @@
class={[
"rounded-full px-3 py-1 text-xs font-medium",
if(@filter == f,
do: "bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/40 dark:text-cerulean-300",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300",
else:
"bg-cool-steel-100 text-cool-steel-600 hover:bg-cool-steel-200 dark:bg-white/5 dark:text-cool-steel-400 dark:hover:bg-white/10"
"bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-white/5 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -62,16 +62,13 @@
<%= if @access_points == [] do %>
<div
id="empty-state"
class="rounded-lg border-2 border-dashed border-cool-steel-300 p-12 text-center dark:border-cool-steel-700"
class="rounded-lg border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-700"
>
<.icon
name="hero-signal"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
/>
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No access points found")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= case @filter do %>
<% "unmatched" -> %>
{t("All access points are matched. Nice!")}
@ -85,48 +82,48 @@
</p>
</div>
<% else %>
<div class="overflow-x-auto rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-white/5">
<div class="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">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Access Point")}
</th>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 md:table-cell"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 md:table-cell"
>
{t("IP Address")}
</th>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 sm:table-cell"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:table-cell"
>
{t("Status")}
</th>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 md:table-cell"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 md:table-cell"
>
{t("Linked Device")}
</th>
<th
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Actions")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/5 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for ap <- @access_points do %>
<tr
id={"ap-row-#{ap.id}"}
class={[
"hover:bg-cool-steel-50 dark:hover:bg-white/5",
"hover:bg-gray-50 dark:hover:bg-white/5",
if(is_nil(ap.device), do: "cursor-pointer")
]}
phx-click={if(is_nil(ap.device), do: "start_link")}
@ -135,16 +132,16 @@
<td class="px-4 py-4 text-sm">
<div>
<div class="flex flex-wrap items-center gap-2">
<span class="break-all font-medium text-cool-steel-900 dark:text-white">
<span class="break-all font-medium text-gray-900 dark:text-white">
{ap.name || "Unnamed"}
</span>
<%= if Map.has_key?(@suggestions, ap.id) do %>
<span class="inline-flex items-center rounded-full bg-wheat-100 px-2 py-0.5 text-xs font-medium text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400">
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
<.icon name="hero-light-bulb-mini" class="mr-0.5 h-3 w-3" /> Match found
</span>
<% end %>
</div>
<span class="mt-0.5 block text-xs text-cool-steel-400 dark:text-cool-steel-500 sm:hidden">
<span class="mt-0.5 block text-xs text-gray-400 dark:text-gray-500 sm:hidden">
{ap.preseem_id}
</span>
<span class={[
@ -155,13 +152,13 @@
</span>
</div>
</td>
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400 md:table-cell">
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400 md:table-cell">
<%= if ap.ip_address do %>
<a
href={"http://#{ap.ip_address}"}
target="_blank"
rel="noopener noreferrer"
class="font-mono hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="font-mono hover:text-indigo-600 dark:hover:text-indigo-400"
onclick="event.stopPropagation()"
>
{ap.ip_address}
@ -178,17 +175,17 @@
{humanize_confidence(ap.match_confidence)}
</span>
</td>
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-cool-steel-500 dark:text-cool-steel-400 md:table-cell">
<td class="hidden whitespace-nowrap px-4 py-4 text-sm text-gray-500 dark:text-gray-400 md:table-cell">
<%= if ap.device do %>
<.link
navigate={~p"/devices/#{ap.device.id}"}
class="text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
onclick="event.stopPropagation()"
>
{ap.device.name || ap.device.ip_address}
</.link>
<% else %>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2.5 py-0.5 text-xs font-medium text-cool-steel-500 dark:bg-white/10 dark:text-cool-steel-400">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500 dark:bg-white/10 dark:text-gray-400">
Not linked
</span>
<% end %>
@ -201,7 +198,7 @@
phx-click="unlink_device"
phx-value-ap-id={ap.id}
data-confirm={t("Unlink this device from the Preseem access point?")}
class="text-sweet-salmon-600 hover:text-sweet-salmon-800 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
onclick="event.stopPropagation()"
>
{t("Unlink")}
@ -212,7 +209,7 @@
id={"link-ap-#{ap.id}"}
phx-click="start_link"
phx-value-ap-id={ap.id}
class="text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
onclick="event.stopPropagation()"
>
{t("Link")}
@ -222,19 +219,19 @@
</tr>
<%!-- Inline linking row --%>
<%= if ap.id == @linking_ap_id do %>
<tr id={"linking-row-#{ap.id}"} class="bg-cerulean-50 dark:bg-cerulean-900/20">
<tr id={"linking-row-#{ap.id}"} class="bg-indigo-50 dark:bg-indigo-900/20">
<td colspan="5" class="px-4 py-4">
<div class="flex items-start gap-4">
<div class="flex-1">
<p class="mb-2 text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
Find a TowerOps device to link to
<span class="text-cerulean-600 dark:text-cerulean-400">{ap.name}</span>
<span class="text-indigo-600 dark:text-indigo-400">{ap.name}</span>
</p>
<%!-- Suggestions --%>
<%= if suggestions = @suggestions[ap.id] do %>
<div class="mb-3">
<p class="mb-1 text-xs font-medium text-wheat-700 dark:text-wheat-400">
<p class="mb-1 text-xs font-medium text-amber-700 dark:text-amber-400">
<.icon name="hero-light-bulb-mini" class="mr-0.5 inline h-3 w-3" />
{t("Suggested match (same IP):")}
</p>
@ -245,11 +242,11 @@
phx-click="link_device"
phx-value-ap-id={ap.id}
phx-value-device-id={suggestion.device.id}
class="inline-flex items-center gap-1.5 rounded-md border border-wheat-300 bg-wheat-50 px-2.5 py-1.5 text-xs font-medium text-wheat-800 hover:bg-wheat-100 dark:border-wheat-700 dark:bg-wheat-900/20 dark:text-wheat-300 dark:hover:bg-wheat-900/40"
class="inline-flex items-center gap-1.5 rounded-md border border-amber-300 bg-amber-50 px-2.5 py-1.5 text-xs font-medium text-amber-800 hover:bg-amber-100 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-300 dark:hover:bg-amber-900/40"
>
{suggestion.device.name || suggestion.device.ip_address ||
"Unnamed"}
<span class="rounded bg-wheat-200 px-1 text-wheat-700 dark:bg-wheat-800 dark:text-wheat-300">
<span class="rounded bg-amber-200 px-1 text-amber-700 dark:bg-amber-800 dark:text-amber-300">
{suggestion.match_type}
</span>
</button>
@ -272,13 +269,13 @@
placeholder={t("Search by device name or IP...")}
autocomplete="off"
phx-debounce="300"
class="block w-full rounded-md border-cool-steel-300 text-sm shadow-sm focus:border-cerulean-500 focus:ring-cerulean-500 dark:border-white/10 dark:bg-white/5 dark:text-white"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
</form>
<%= if @search_results != [] do %>
<div class="mt-2 max-h-48 overflow-y-auto rounded-md border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800">
<ul class="divide-y divide-cool-steel-200 dark:divide-white/5">
<div class="mt-2 max-h-48 overflow-y-auto rounded-md border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800">
<ul class="divide-y divide-gray-200 dark:divide-white/5">
<li :for={device <- @search_results}>
<button
type="button"
@ -286,15 +283,15 @@
phx-click="link_device"
phx-value-ap-id={ap.id}
phx-value-device-id={device.id}
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-cool-steel-50 dark:hover:bg-white/5"
class="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-gray-50 dark:hover:bg-white/5"
>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{device.name || "Unnamed"}
</span>
<span class="flex items-center gap-2 text-cool-steel-500 dark:text-cool-steel-400">
<span class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
{device.ip_address}
<%= if device.site do %>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500">
<span class="text-xs text-gray-400 dark:text-gray-500">
{device.site.name}
</span>
<% end %>
@ -306,7 +303,7 @@
<% end %>
<%= if @device_search != "" and String.length(@device_search) >= 2 and @search_results == [] do %>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
No devices found matching "{@device_search}"
</p>
<% end %>
@ -315,7 +312,7 @@
type="button"
id="cancel-link"
phx-click="cancel_link"
class="flex-shrink-0 self-start text-sm text-cool-steel-500 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:text-cool-steel-300"
class="flex-shrink-0 self-start text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
{t("Cancel")}
</button>

View file

@ -133,16 +133,16 @@ defmodule ToweropsWeb.Org.PreseemInsightsLive do
defp urgency_classes(urgency) do
case urgency do
"critical" ->
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
"warning" ->
"bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
"info" ->
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
_ ->
"bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
end
end

View file

@ -3,7 +3,7 @@
current_scope={@current_scope}
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="border-b border-cool-steel-200 pb-5 dark:border-white/5">
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings", navigate: ~p"/orgs/#{@organization.slug}/settings"},
@ -12,10 +12,10 @@
]} />
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Network Insights")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Proactive network health observations generated from Preseem data analysis.")}
</p>
</div>
@ -23,7 +23,7 @@
</div>
<%!-- Filter bar --%>
<div class="mt-6 flex flex-wrap items-center gap-4 border-b border-cool-steel-200 pb-4 dark:border-white/10">
<div class="mt-6 flex flex-wrap items-center gap-4 border-b border-gray-200 pb-4 dark:border-white/10">
<%!-- Status filter tabs --%>
<nav class="flex space-x-4">
<.link
@ -34,10 +34,9 @@
class={[
"rounded-md px-3 py-1.5 text-sm font-medium",
if(@filter_status == "active",
do:
"bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
)
]}
>
@ -51,10 +50,9 @@
class={[
"rounded-md px-3 py-1.5 text-sm font-medium",
if(@filter_status == "dismissed",
do:
"bg-cerulean-100 text-cerulean-700 dark:bg-cerulean-900/30 dark:text-cerulean-400",
do: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 hover:text-cool-steel-700 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-cool-steel-300"
"text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-300"
)
]}
>
@ -63,10 +61,8 @@
</nav>
<%!-- Urgency filter --%>
<div class="flex items-center gap-2 border-l border-cool-steel-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400">
Urgency:
</span>
<div class="flex items-center gap-2 border-l border-gray-200 pl-4 dark:border-white/10">
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">Urgency:</span>
<.link
id="filter-urgency-all"
patch={
@ -75,9 +71,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(is_nil(@filter_urgency),
do: "bg-cool-steel-200 text-cool-steel-800 dark:bg-white/20 dark:text-white",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -91,10 +86,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(@filter_urgency == "critical",
do:
"bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -108,9 +101,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(@filter_urgency == "warning",
do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -124,10 +116,8 @@
class={[
"rounded-md px-2 py-1 text-xs font-medium",
if(@filter_urgency == "info",
do:
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400",
else:
"text-cool-steel-500 hover:bg-cool-steel-100 dark:text-cool-steel-400 dark:hover:bg-white/10"
do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400",
else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10"
)
]}
>
@ -140,16 +130,16 @@
<%= if any_selected?(@selected_ids) do %>
<div
id="bulk-actions"
class="mt-4 flex items-center gap-3 rounded-lg bg-cerulean-50 px-4 py-3 dark:bg-cerulean-900/20"
class="mt-4 flex items-center gap-3 rounded-lg bg-indigo-50 px-4 py-3 dark:bg-indigo-900/20"
>
<span class="text-sm font-medium text-cerulean-700 dark:text-cerulean-300">
<span class="text-sm font-medium text-indigo-700 dark:text-indigo-300">
{MapSet.size(@selected_ids)} selected
</span>
<button
type="button"
id="deselect-all-btn"
phx-click="deselect_all"
class="text-sm text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Deselect All")}
</button>
@ -157,7 +147,7 @@
type="button"
id="bulk-dismiss-btn"
phx-click="bulk_dismiss"
class="rounded-md bg-cerulean-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Dismiss Selected")}
</button>
@ -169,16 +159,16 @@
<%= if @insights == [] do %>
<div
id="empty-state"
class="rounded-lg border-2 border-dashed border-cool-steel-300 p-12 text-center dark:border-cool-steel-700"
class="rounded-lg border-2 border-dashed border-gray-300 p-12 text-center dark:border-gray-700"
>
<.icon
name="hero-light-bulb"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No insights found")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= cond do %>
<% @filter_status == "dismissed" -> %>
{t("No dismissed insights.")}
@ -196,7 +186,7 @@
type="button"
id="select-all-btn"
phx-click="select_all"
class="text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
{t("Select All")}
</button>
@ -207,11 +197,11 @@
:for={insight <- @insights}
id={"insight-#{insight.id}"}
class={[
"rounded-lg border bg-white p-4 transition-shadow hover:shadow-md dark:bg-cool-steel-800/50",
"rounded-lg border bg-white p-4 transition-shadow hover:shadow-md dark:bg-gray-800/50",
if(selected?(@selected_ids, insight.id),
do:
"border-cerulean-300 ring-1 ring-cerulean-200 dark:border-cerulean-600 dark:ring-cerulean-800",
else: "border-cool-steel-200 dark:border-white/10"
"border-indigo-300 ring-1 ring-indigo-200 dark:border-indigo-600 dark:ring-indigo-800",
else: "border-gray-200 dark:border-white/10"
)
]}
>
@ -228,9 +218,8 @@
"flex h-5 w-5 items-center justify-center rounded border",
if(selected?(@selected_ids, insight.id),
do:
"border-cerulean-600 bg-cerulean-600 dark:border-cerulean-500 dark:bg-cerulean-500",
else:
"border-cool-steel-300 bg-white dark:border-cool-steel-600 dark:bg-cool-steel-700"
"border-indigo-600 bg-indigo-600 dark:border-indigo-500 dark:bg-indigo-500",
else: "border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-700"
)
]}>
<%= if selected?(@selected_ids, insight.id) do %>
@ -250,23 +239,23 @@
</span>
<%!-- Type badge --%>
<span class="inline-flex items-center rounded-full bg-cool-steel-100 px-2 py-0.5 text-xs font-medium text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300">
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{String.replace(insight.type, "_", " ")}
</span>
</div>
<h3 class="mt-1 text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
{insight.title}
</h3>
<%= if insight.description do %>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{insight.description}
</p>
<% end %>
<%= if insight.llm_summary do %>
<div class="mt-2 rounded-md border border-cerulean-200 bg-cerulean-50 px-3 py-2 text-sm text-cerulean-900 dark:border-cerulean-900/40 dark:bg-cerulean-950/40 dark:text-cerulean-100">
<div class="mt-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm text-blue-900 dark:border-blue-900/40 dark:bg-blue-950/40 dark:text-blue-100">
<p class="font-medium">{t("Summary")}</p>
<p>{insight.llm_summary}</p>
<%= if insight.recommended_action do %>
@ -275,13 +264,13 @@
{insight.recommended_action}
</p>
<% end %>
<p class="mt-1 text-xs text-cerulean-700 dark:text-cerulean-300">
<p class="mt-1 text-xs text-blue-700 dark:text-blue-300">
AI-generated{if insight.llm_model, do: " · " <> insight.llm_model, else: ""}
</p>
</div>
<% end %>
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-2 flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<%!-- Access point --%>
<%= if insight.preseem_access_point do %>
<span class="flex items-center gap-1">
@ -294,7 +283,7 @@
<%= if insight.device do %>
<.link
navigate={~p"/devices/#{insight.device.id}"}
class="flex items-center gap-1 text-cerulean-600 hover:text-cerulean-800 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="flex items-center gap-1 text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
<.icon name="hero-server" class="h-3.5 w-3.5" />
{insight.device.name || "Device"}
@ -320,7 +309,7 @@
type="button"
phx-click="dismiss"
phx-value-id={insight.id}
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-cool-steel-600 hover:bg-cool-steel-100 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:bg-white/10 dark:hover:text-white"
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
>
{t("Dismiss")}
</button>

View file

@ -8,27 +8,25 @@
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Settings"}
]} />
<h1 class="text-3xl font-semibold tracking-tight text-cool-steel-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
{t("Organization Settings")}
</h1>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Manage organization defaults, integrations, and configuration")}
</p>
</div>
<header class="border-b border-cool-steel-200 dark:border-white/5">
<header class="border-b border-gray-200 dark:border-white/5">
<nav class="flex overflow-x-auto py-4">
<ul
role="list"
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-cool-steel-500 sm:px-6 lg:px-8 dark:text-cool-steel-400"
class="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
>
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=general"}
class={
if @active_tab == "general",
do: "text-cerulean-600 dark:text-cerulean-400",
else: ""
if @active_tab == "general", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("General")}
@ -37,9 +35,7 @@
<li>
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=snmp"}
class={
if @active_tab == "snmp", do: "text-cerulean-600 dark:text-cerulean-400", else: ""
}
class={if @active_tab == "snmp", do: "text-indigo-600 dark:text-indigo-400", else: ""}
>
{t("SNMP")}
</.link>
@ -51,7 +47,7 @@
patch={~p"/orgs/#{@organization.slug}/settings?tab=mikrotik"}
class={
if @active_tab == "mikrotik",
do: "text-cerulean-600 dark:text-cerulean-400",
do: "text-indigo-600 dark:text-indigo-400",
else: ""
}
>
@ -64,7 +60,7 @@
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=agents"}
class={
if @active_tab == "agents", do: "text-cerulean-600 dark:text-cerulean-400", else: ""
if @active_tab == "agents", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Agents")}
@ -74,9 +70,7 @@
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=members"}
class={
if @active_tab == "members",
do: "text-cerulean-600 dark:text-cerulean-400",
else: ""
if @active_tab == "members", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Members")}
@ -94,9 +88,7 @@
<.link
patch={~p"/orgs/#{@organization.slug}/settings?tab=billing"}
class={
if @active_tab == "billing",
do: "text-cerulean-600 dark:text-cerulean-400",
else: ""
if @active_tab == "billing", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
{t("Billing")}
@ -115,15 +107,15 @@
data-1p-ignore
data-form-type="other"
>
<div class="divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<%= if @active_tab == "general" do %>
<!-- Organization Name Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Organization Name")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Update the name of your organization.")}
</p>
</div>
@ -142,10 +134,10 @@
<!-- Default Organization Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Organization")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set this organization as your default. When you log in, you'll be directed to your default organization."
)}
@ -170,11 +162,11 @@
id="toggle-default-org"
type="button"
phx-click="toggle_default_org"
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:shadow-none dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400"
>
<.icon name="hero-star" class="h-4 w-4 inline" /> Set as Default Organization
</button>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Click to make this your default organization")}
</p>
<% end %>
@ -185,10 +177,10 @@
<!-- Use Sites Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Site Organization")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Enable site organization to group devices into physical locations (offices, datacenters, etc.)."
)}
@ -201,23 +193,23 @@
type="checkbox"
label={t("Use sites to organize devices")}
/>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t(
"When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization."
)}
</p>
<%= if @organization.use_sites && @form[:use_sites].value == false do %>
<div class="mt-4 rounded-md bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 p-4">
<div class="mt-4 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<div class="flex">
<div class="shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-wheat-400" />
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-amber-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-wheat-800 dark:text-wheat-200">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
{t("Warning: Disabling Sites")}
</h3>
<div class="mt-2 text-sm text-wheat-700 dark:text-wheat-300">
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t(
"All devices will be removed from their current sites and assigned directly to the organization."
@ -235,12 +227,12 @@
</div>
<!-- Alert Routing Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-cool-steel-200 dark:border-white/5">
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-gray-200 dark:border-white/5">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Alert Routing")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose how alerts are routed for on-call notifications.")}
</p>
</div>
@ -255,9 +247,9 @@
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "builtin",
do:
"border-cerulean-500 bg-cerulean-50/50 ring-1 ring-cerulean-500 dark:bg-cerulean-900/10 dark:border-cerulean-400",
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-cool-steel-200 hover:border-cool-steel-300 dark:border-white/10 dark:hover:border-white/20"
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
@ -265,14 +257,14 @@
name="organization[alert_routing]"
value="builtin"
checked={routing_value == "builtin"}
class="mt-0.5 h-4 w-4 border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-600 dark:border-cool-steel-600 dark:bg-cool-steel-800"
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-cool-steel-900 dark:text-white">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon name="hero-bell-alert" class="h-4 w-4 inline -mt-0.5" />
{t("TowerOps Built-in")}
</span>
<span class="block text-sm text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Use TowerOps schedules and escalation policies to notify on-call staff."
)}
@ -285,9 +277,9 @@
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "pagerduty",
do:
"border-cerulean-500 bg-cerulean-50/50 ring-1 ring-cerulean-500 dark:bg-cerulean-900/10 dark:border-cerulean-400",
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-cool-steel-200 hover:border-cool-steel-300 dark:border-white/10 dark:hover:border-white/20"
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
@ -295,29 +287,29 @@
name="organization[alert_routing]"
value="pagerduty"
checked={routing_value == "pagerduty"}
class="mt-0.5 h-4 w-4 border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-600 dark:border-cool-steel-600 dark:bg-cool-steel-800"
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-cool-steel-900 dark:text-white">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon
name="hero-arrow-top-right-on-square"
class="h-4 w-4 inline -mt-0.5"
/>
{t("PagerDuty")}
</span>
<span class="block text-sm text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Route alerts to PagerDuty for incident management and on-call notifications."
)}
</span>
<%= if routing_value == "pagerduty" && !@integrations["pagerduty"] do %>
<div class="mt-2 rounded-md bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 p-2.5">
<div class="mt-2 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-2.5">
<div class="flex items-center gap-2">
<.icon
name="hero-exclamation-triangle"
class="h-4 w-4 text-wheat-500 shrink-0"
class="h-4 w-4 text-amber-500 shrink-0"
/>
<span class="text-xs text-wheat-800 dark:text-wheat-200">
<span class="text-xs text-amber-800 dark:text-amber-200">
{t("PagerDuty integration not configured yet.")}
<.link
navigate={
@ -339,9 +331,9 @@
"relative flex cursor-pointer rounded-lg border p-4 transition-colors",
if(routing_value == "both",
do:
"border-cerulean-500 bg-cerulean-50/50 ring-1 ring-cerulean-500 dark:bg-cerulean-900/10 dark:border-cerulean-400",
"border-blue-500 bg-blue-50/50 ring-1 ring-blue-500 dark:bg-blue-900/10 dark:border-blue-400",
else:
"border-cool-steel-200 hover:border-cool-steel-300 dark:border-white/10 dark:hover:border-white/20"
"border-gray-200 hover:border-gray-300 dark:border-white/10 dark:hover:border-white/20"
)
]}>
<input
@ -349,14 +341,14 @@
name="organization[alert_routing]"
value="both"
checked={routing_value == "both"}
class="mt-0.5 h-4 w-4 border-cool-steel-300 text-cerulean-600 focus:ring-cerulean-600 dark:border-cool-steel-600 dark:bg-cool-steel-800"
class="mt-0.5 h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-600 dark:border-gray-600 dark:bg-gray-800"
/>
<div class="ml-3">
<span class="block text-sm font-semibold text-cool-steel-900 dark:text-white">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">
<.icon name="hero-arrows-right-left" class="h-4 w-4 inline -mt-0.5" />
{t("Both")}
</span>
<span class="block text-sm text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<span class="block text-sm text-gray-500 dark:text-gray-400 mt-0.5">
{t(
"Send alerts to both PagerDuty and built-in escalation. Useful during migration."
)}
@ -373,15 +365,15 @@
<!-- SNMP Configuration Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level."
)}
</p>
<p class="mt-3 text-xs text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
@ -487,11 +479,11 @@
/>
<%= if @organization.snmp_community do %>
<div class="mt-6 rounded-md bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 p-4">
<h3 class="text-sm font-medium text-wheat-900 dark:text-wheat-200 mb-2">
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-wheat-700 dark:text-wheat-300 mb-3">
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t(
"This will override SNMP settings for ALL devices across all sites in this organization."
)}
@ -505,7 +497,7 @@
"This will replace SNMP settings for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-sweet-salmon-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sweet-salmon-500 dark:bg-sweet-salmon-500 dark:shadow-none dark:hover:bg-sweet-salmon-400"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
{t("Apply SNMP Config to All Devices")}
@ -520,22 +512,22 @@
<!-- MikroTik API Configuration Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik."
)}
</p>
<div class="mt-3 rounded-md bg-cerulean-50 dark:bg-cerulean-900/20 p-3">
<p class="text-xs text-cerulean-700 dark:text-cerulean-300">
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
<p class="mt-3 text-xs text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
@ -587,8 +579,8 @@
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value != false, do: "hidden"}"}>
<div class="rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 p-4">
<p class="text-sm text-sweet-salmon-700 dark:text-sweet-salmon-300">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>{t("Critical Security Warning:")}</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network. This setting is"
@ -600,8 +592,8 @@
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value == false, do: "hidden"}"}>
<div class="rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-4">
<p class="text-sm text-wheat-700 dark:text-wheat-300">
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
<strong>{t("Security Warning:")}</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible."
@ -618,15 +610,15 @@
<%= if @available_agents != [] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t(
"Select a default agent for SNMP polling. This will be used for all devices in this organization unless overridden at the site or device level."
)}
</p>
<p class="mt-3 text-xs text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
Hierarchy: Device > Site > Organization
</p>
@ -641,11 +633,11 @@
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<div class="rounded-md bg-cool-steel-50 dark:bg-cool-steel-800/50 p-4">
<p class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2">
<div class="rounded-md bg-gray-50 dark:bg-gray-800/50 p-4">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Current Device Assignment:")}
</p>
<div class="space-y-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<.icon name="hero-server" class="h-4 w-4" />
<span>
@ -675,11 +667,11 @@
</div>
<%= if @organization.default_agent_token_id do %>
<div class="mt-6 rounded-md bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 p-4">
<h3 class="text-sm font-medium text-wheat-900 dark:text-wheat-200 mb-2">
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-wheat-700 dark:text-wheat-300 mb-3">
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t(
"This will assign the default agent to ALL devices across all sites in this organization."
)}
@ -693,7 +685,7 @@
"This will replace agent assignments for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-sweet-salmon-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-sweet-salmon-500 dark:bg-sweet-salmon-500 dark:shadow-none dark:hover:bg-sweet-salmon-400"
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
{t("Apply Default Agent to All Devices")}
@ -705,14 +697,14 @@
<% else %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("No agents configured yet.")}
<.link
navigate={~p"/agents"}
class="text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300"
>
{t("Create an agent")}
</.link>
@ -729,17 +721,17 @@
</div>
<!-- Save Button at Bottom (not shown on integrations tab) -->
<div class="flex items-center justify-end gap-x-6 border-t border-cool-steel-200 px-4 py-4 sm:px-6 lg:px-8 dark:border-white/10">
<div class="flex items-center justify-end gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-6 lg:px-8 dark:border-white/10">
<.link
navigate={~p"/dashboard"}
class="text-sm font-semibold text-cool-steel-900 dark:text-white"
class="text-sm font-semibold text-gray-900 dark:text-white"
>
{t("Cancel")}
</.link>
<button
type="submit"
phx-disable-with={t("Saving...")}
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cerulean-600 dark:bg-cerulean-500 dark:shadow-none dark:hover:bg-cerulean-400 dark:focus-visible:outline-cerulean-500"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
{t("Save Settings")}
</button>
@ -748,15 +740,15 @@
<% end %>
<%= if @active_tab == "members" do %>
<div class="divide-y divide-cool-steel-200 dark:divide-white/10">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Invite form (owner/admin only) -->
<%= if @membership.role in [:owner, :admin] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Invite a Team Member")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Send an invitation email to add someone to this organization.")}
</p>
</div>
@ -783,13 +775,13 @@
]}
/>
<div>
<label class="block text-sm/6 font-medium text-cool-steel-900 dark:text-white invisible">
<label class="block text-sm/6 font-medium text-gray-900 dark:text-white invisible">
&nbsp;
</label>
<button
type="submit"
phx-disable-with={t("Sending...")}
class="rounded-md bg-cerulean-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:shadow-none dark:hover:bg-cerulean-400 h-[42px]"
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 h-[42px]"
>
{t("Send Invite")}
</button>
@ -804,56 +796,56 @@
<%= if @pending_invitations != [] do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Pending Invitations")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Invitations that haven't been accepted yet.")}
</p>
</div>
<div class="md:col-span-2">
<div class="overflow-x-auto rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-white/5">
<div class="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">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Email")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Sent")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Expires")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tr :for={invitation <- @pending_invitations}>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{invitation.email}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400">
<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
{invitation.role}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={invitation.inserted_at}
timezone={@timezone}
format="relative"
/>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={invitation.expires_at}
timezone={@timezone}
@ -867,7 +859,7 @@
phx-click="cancel_invitation"
phx-value-id={invitation.id}
data-confirm={t("Cancel this invitation?")}
class="text-sweet-salmon-600 hover:text-sweet-salmon-700 dark:text-sweet-salmon-400 dark:hover:text-sweet-salmon-300"
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Cancel")}
</button>
@ -884,40 +876,40 @@
<!-- Current members -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-cool-steel-900 dark:text-white">
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Members")}
</h2>
<p class="mt-1 text-sm/6 text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("People who have access to this organization.")}
</p>
</div>
<div class="md:col-span-2">
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-white/5">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("User")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Joined")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Actions")}
</th>
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-transparent">
<tr :for={member <- @members}>
<td class="whitespace-nowrap px-4 py-3">
<div>
<div class="text-sm font-medium text-cool-steel-900 dark:text-white">
<div class="text-sm font-medium text-gray-900 dark:text-white">
{member.user.email}
</div>
</div>
@ -930,22 +922,22 @@
"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
:admin ->
"bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
:executive ->
"bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
role when role in [:technician, :member] ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
:viewer ->
"bg-cool-steel-100 text-cool-steel-800 dark:bg-cool-steel-800 dark:text-cool-steel-400"
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400"
end
]}>
{member.role}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
<.timestamp
datetime={member.inserted_at}
timezone={@timezone}
@ -959,7 +951,7 @@
<form phx-change="change_role" phx-value-user-id={member.user_id}>
<select
name="role"
class="rounded-md border-cool-steel-300 py-1 pl-2 pr-8 text-xs dark:border-white/10 dark:bg-white/5 dark:text-white"
class="rounded-md border-gray-300 py-1 pl-2 pr-8 text-xs dark:border-white/10 dark:bg-white/5 dark:text-white"
>
<option value="admin" selected={member.role == :admin}>
{t("Admin")}
@ -1002,18 +994,18 @@
<%= if @active_tab == "integrations" do %>
<div class="mt-8 flex items-center justify-center">
<div class="text-center rounded-xl border border-cool-steel-200 bg-white p-10 shadow-sm dark:border-white/10 dark:bg-white/[0.03]">
<.icon name="hero-arrow-top-right-on-square" class="mx-auto h-10 w-10 text-cerulean-400" />
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<div class="text-center rounded-xl border border-gray-200 bg-white p-10 shadow-sm dark:border-white/10 dark:bg-white/[0.03]">
<.icon name="hero-arrow-top-right-on-square" class="mx-auto h-10 w-10 text-indigo-400" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("Integrations have moved to their own page")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Manage all your integrations from the dedicated integrations page.")}
</p>
<div class="mt-6">
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations"}
class="rounded-md bg-cerulean-600 px-4 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-cerulean-500 dark:bg-cerulean-500 dark:hover:bg-cerulean-400"
class="rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
{t("Go to Integrations")}
</.link>
@ -1029,22 +1021,18 @@
<dl class="space-y-3">
<div>
<dt class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
{t("Current Plan")}
</dt>
<dt class="text-sm text-gray-600 dark:text-gray-400">{t("Current Plan")}</dt>
<dd class="text-lg font-semibold capitalize dark:text-white">
{@organization.subscription_plan || "free"}
</dd>
</div>
<div>
<dt class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
{t("Device Usage")}
</dt>
<dt class="text-sm text-gray-600 dark:text-gray-400">{t("Device Usage")}</dt>
<dd class="text-lg dark:text-white">
{@current_devices} {t("devices")}
<%= if @device_limit != :unlimited do %>
<span class="text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-500 dark:text-gray-400">
/ {@device_limit} {t("limit")}
</span>
<% end %>
@ -1053,13 +1041,13 @@
<%= if @organization.subscription_plan == "paid" do %>
<div>
<dt class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<dt class="text-sm text-gray-600 dark:text-gray-400">
{t("Estimated Monthly Cost")}
</dt>
<dd class="text-2xl font-bold text-green-600 dark:text-green-400">
${@estimated_cost.cost_usd}
</dd>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-1">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{@estimated_cost.billable} {t("billable devices")} × ${@estimated_cost.price_per_device}
</p>
</div>
@ -1081,23 +1069,23 @@
<% end %>
<%= if @organization.subscription_status == "past_due" do %>
<div class="bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800/50 rounded p-3">
<p class="text-sm text-wheat-800 dark:text-wheat-300">
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800/50 rounded p-3">
<p class="text-sm text-yellow-800 dark:text-yellow-300">
⚠ {t("Payment Past Due")}
</p>
<p class="text-xs text-wheat-600 dark:text-wheat-400 mt-1">
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-1">
{t("Please update your payment method to avoid service interruption.")}
</p>
</div>
<% end %>
<% else %>
<div class="bg-cerulean-50 dark:bg-cerulean-900/20 border border-cerulean-200 dark:border-cerulean-800/50 rounded p-3">
<p class="text-sm text-cerulean-800 dark:text-cerulean-300">
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 rounded p-3">
<p class="text-sm text-blue-800 dark:text-blue-300">
{t("Free Plan - First %{count} devices included",
count: @estimated_cost.free_included
)}
</p>
<p class="text-xs text-cerulean-600 dark:text-cerulean-400 mt-1">
<p class="text-xs text-blue-600 dark:text-blue-400 mt-1">
{t("Upgrade to monitor unlimited devices at $%{price}/device/month",
price: @estimated_cost.price_per_device
)}
@ -1130,18 +1118,14 @@
<h3 class="text-lg font-semibold mb-3 dark:text-white">{t("Billing Information")}</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
{t("Subscription Status")}
</dt>
<dt class="text-gray-600 dark:text-gray-400">{t("Subscription Status")}</dt>
<dd class="font-medium capitalize dark:text-white">
{@organization.subscription_status}
</dd>
</div>
<%= if @organization.subscription_current_period_start do %>
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
{t("Current Period Start")}
</dt>
<dt class="text-gray-600 dark:text-gray-400">{t("Current Period Start")}</dt>
<dd class="font-medium dark:text-white">
{Calendar.strftime(@organization.subscription_current_period_start, "%B %d, %Y")}
</dd>
@ -1149,9 +1133,7 @@
<% end %>
<%= if @organization.subscription_current_period_end do %>
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
{t("Current Period End")}
</dt>
<dt class="text-gray-600 dark:text-gray-400">{t("Current Period End")}</dt>
<dd class="font-medium dark:text-white">
{Calendar.strftime(@organization.subscription_current_period_end, "%B %d, %Y")}
</dd>
@ -1159,14 +1141,12 @@
<% end %>
<%= if @organization.payment_method_status do %>
<div class="flex justify-between">
<dt class="text-cool-steel-600 dark:text-cool-steel-400">
{t("Payment Method")}
</dt>
<dt class="text-gray-600 dark:text-gray-400">{t("Payment Method")}</dt>
<dd class={[
"font-medium capitalize",
if(@organization.payment_method_status == "valid",
do: "text-green-600 dark:text-green-400",
else: "text-wheat-600 dark:text-wheat-400"
else: "text-yellow-600 dark:text-yellow-400"
)
]}>
{String.replace(@organization.payment_method_status, "_", " ")}

View file

@ -22,10 +22,10 @@
<input type="hidden" name="org_id" value={org.id} />
<button
type="submit"
class="relative w-full overflow-hidden rounded-lg border border-cool-steel-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-white/10 dark:bg-cool-steel-800/50 cursor-pointer text-left"
class="relative w-full overflow-hidden rounded-lg border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-white/10 dark:bg-gray-800/50 cursor-pointer text-left"
>
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white">{org.name}</h2>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{org.name}</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{Enum.find(org.memberships, &(&1.user_id == @current_scope.user.id)).role
|> to_string()
|> String.capitalize()}
@ -35,14 +35,11 @@
</div>
<div :if={@organizations == []} class="text-center py-16">
<.icon
name="hero-building-office"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-600"
/>
<h3 class="mt-4 text-lg font-semibold text-cool-steel-900 dark:text-white">
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No organizations")}
</h3>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by creating a new organization.")}
</p>
<div class="mt-6">

View file

@ -138,13 +138,10 @@ defmodule ToweropsWeb.ReportsLive do
@doc false
def status_badge("success"), do: {"Success", "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300"}
def status_badge("failed"),
do: {"Failed", "bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/50 dark:text-sweet-salmon-300"}
def status_badge("failed"), do: {"Failed", "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300"}
def status_badge("partial_failure"),
do: {"Partial", "bg-wheat-100 text-wheat-700 dark:bg-wheat-900/50 dark:text-wheat-300"}
do: {"Partial", "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300"}
def status_badge(_),
do: {"Never Run", "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"}
def status_badge(_), do: {"Never Run", "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"}
end

View file

@ -11,17 +11,15 @@
<div class="mb-6 flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
{t("Scheduled Reports")}
</h1>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("Scheduled Reports")}</h1>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Automated report generation and email delivery")}
</p>
</div>
<button
:if={!@show_form}
phx-click="show_form"
class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-cerulean-600 hover:bg-cerulean-700 rounded-lg transition-colors"
class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
<.icon name="hero-plus" class="w-4 h-4" />
{t("New Report")}
@ -30,14 +28,12 @@
<%!-- Create Report Form --%>
<%= if @show_form do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-6 mb-6">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
{t("Create Report")}
</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Create Report")}</h2>
<button
phx-click="hide_form"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
@ -46,7 +42,7 @@
<.form for={@form} phx-submit="save" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Report Name")}
</label>
<input
@ -54,18 +50,18 @@
name="report[name]"
placeholder="Weekly Uptime Report"
required
class="w-full rounded-lg border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm text-cool-steel-900 dark:text-white focus:ring-2 focus:ring-cerulean-500 focus:border-cerulean-500"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Report Type")}
</label>
<select
name="report[report_type]"
required
class="w-full rounded-lg border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm text-cool-steel-900 dark:text-white focus:ring-2 focus:ring-cerulean-500 focus:border-cerulean-500"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="uptime_summary">{t("Uptime Summary")}</option>
<option value="alert_history">{t("Alert History")}</option>
@ -75,12 +71,12 @@
</div>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Schedule")}
</label>
<select
name="report[schedule_type]"
class="w-full rounded-lg border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm text-cool-steel-900 dark:text-white focus:ring-2 focus:ring-cerulean-500 focus:border-cerulean-500"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="daily">{t("Daily")}</option>
<option value="weekly" selected>{t("Weekly")}</option>
@ -90,12 +86,12 @@
</div>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Time Range (days)")}
</label>
<select
name="report[scope_days]"
class="w-full rounded-lg border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm text-cool-steel-900 dark:text-white focus:ring-2 focus:ring-cerulean-500 focus:border-cerulean-500"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="1">{t("Last 24 hours")}</option>
<option value="7" selected>{t("Last 7 days")}</option>
@ -106,9 +102,9 @@
</div>
<div>
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-1">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t("Recipients")}
<span class="font-normal text-cool-steel-400">
<span class="font-normal text-gray-400">
{t("(comma-separated email addresses)")}
</span>
</label>
@ -117,7 +113,7 @@
rows="2"
required
placeholder="admin@example.com, noc@example.com"
class="w-full rounded-lg border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm text-cool-steel-900 dark:text-white focus:ring-2 focus:ring-cerulean-500 focus:border-cerulean-500"
class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
></textarea>
</div>
@ -125,13 +121,13 @@
<button
type="button"
phx-click="hide_form"
class="px-4 py-2 text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 bg-cool-steel-100 dark:bg-cool-steel-700 hover:bg-cool-steel-200 dark:hover:bg-cool-steel-600 rounded-lg transition-colors"
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors"
>
{t("Cancel")}
</button>
<button
type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-cerulean-600 hover:bg-cerulean-700 rounded-lg transition-colors"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
{t("Create Report")}
</button>
@ -142,11 +138,11 @@
<%!-- Reports List --%>
<%= if @reports != [] do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/75">
<tr class="text-xs text-cool-steel-600 dark:text-cool-steel-400">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-2 text-left font-medium">{t("Name")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Type")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Schedule")}</th>
@ -156,24 +152,22 @@
<th class="px-4 py-2 text-right font-medium">{t("Actions")}</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-white/10">
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for report <- @reports do %>
<tr class={"text-sm #{if !report.enabled, do: "opacity-50"}"}>
<td class="px-4 py-3">
<span class="font-medium text-cool-steel-900 dark:text-white">
{report.name}
</span>
<span class="font-medium text-gray-900 dark:text-white">{report.name}</span>
</td>
<td class="px-4 py-3 text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{humanize_type(report.report_type)}
</td>
<td class="px-4 py-3 text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{humanize_schedule(report.schedule)}
</td>
<td class="px-4 py-3 text-xs text-cool-steel-500 dark:text-cool-steel-400 max-w-48 truncate">
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400 max-w-48 truncate">
{format_recipients(report.recipients)}
</td>
<td class="px-4 py-3 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
<%= if report.last_run_at do %>
{Calendar.strftime(report.last_run_at, "%b %d, %H:%M")}
<% else %>
@ -192,7 +186,7 @@
phx-click="run_now"
phx-value-id={report.id}
title="Run now"
class="text-cool-steel-400 hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
>
<.icon name="hero-play" class="w-4 h-4" />
</button>
@ -200,7 +194,7 @@
phx-click="toggle"
phx-value-id={report.id}
title={if report.enabled, do: "Disable", else: "Enable"}
class="text-cool-steel-400 hover:text-wheat-600 dark:hover:text-wheat-400"
class="text-gray-400 hover:text-yellow-600 dark:hover:text-yellow-400"
>
<.icon
name={if report.enabled, do: "hero-pause", else: "hero-play"}
@ -212,7 +206,7 @@
phx-value-id={report.id}
data-confirm="Delete this report?"
title="Delete"
class="text-cool-steel-400 hover:text-sweet-salmon-600 dark:hover:text-sweet-salmon-400"
class="text-gray-400 hover:text-red-600 dark:hover:text-red-400"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
@ -225,20 +219,20 @@
</div>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-12 text-center">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12 text-center">
<.icon
name="hero-document-chart-bar"
class="w-12 h-12 mx-auto text-cool-steel-300 dark:text-cool-steel-600"
class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600"
/>
<h3 class="mt-4 text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">
{t("No scheduled reports")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Create a report to start receiving automated updates via email.")}
</p>
<button
phx-click="show_form"
class="mt-4 inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-cerulean-600 hover:bg-cerulean-700 rounded-lg transition-colors"
class="mt-4 inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
<.icon name="hero-plus" class="w-4 h-4" />
{t("Create First Report")}

View file

@ -82,18 +82,15 @@ defmodule ToweropsWeb.RfLinkHealthLive do
@doc false
def health_color(:healthy), do: "text-green-600 dark:text-green-400"
def health_color(:degraded), do: "text-wheat-600 dark:text-wheat-400"
def health_color(:critical), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def health_color(_), do: "text-cool-steel-400 dark:text-cool-steel-500"
def health_color(:degraded), do: "text-yellow-600 dark:text-yellow-400"
def health_color(:critical), do: "text-red-600 dark:text-red-400"
def health_color(_), do: "text-gray-400 dark:text-gray-500"
@doc false
def health_bg(:healthy), do: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300"
def health_bg(:degraded), do: "bg-wheat-100 text-wheat-700 dark:bg-wheat-900/50 dark:text-wheat-300"
def health_bg(:critical),
do: "bg-sweet-salmon-100 text-sweet-salmon-700 dark:bg-sweet-salmon-900/50 dark:text-sweet-salmon-300"
def health_bg(_), do: "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"
def health_bg(:degraded), do: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300"
def health_bg(:critical), do: "bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300"
def health_bg(_), do: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
@doc false
def health_label(:healthy), do: "Healthy"
@ -111,10 +108,10 @@ defmodule ToweropsWeb.RfLinkHealthLive do
end
@doc false
def signal_bar_color(nil), do: "bg-cool-steel-300 dark:bg-cool-steel-600"
def signal_bar_color(nil), do: "bg-gray-300 dark:bg-gray-600"
def signal_bar_color(signal) when signal > -65, do: "bg-green-500"
def signal_bar_color(signal) when signal > -75, do: "bg-wheat-500"
def signal_bar_color(_), do: "bg-sweet-salmon-500"
def signal_bar_color(signal) when signal > -75, do: "bg-yellow-500"
def signal_bar_color(_), do: "bg-red-500"
@doc false
def snr_bar_width(nil), do: 0
@ -126,10 +123,10 @@ defmodule ToweropsWeb.RfLinkHealthLive do
end
@doc false
def snr_bar_color(nil), do: "bg-cool-steel-300 dark:bg-cool-steel-600"
def snr_bar_color(nil), do: "bg-gray-300 dark:bg-gray-600"
def snr_bar_color(snr) when snr > 25, do: "bg-green-500"
def snr_bar_color(snr) when snr > 15, do: "bg-wheat-500"
def snr_bar_color(_), do: "bg-sweet-salmon-500"
def snr_bar_color(snr) when snr > 15, do: "bg-yellow-500"
def snr_bar_color(_), do: "bg-red-500"
@doc false
def format_signal(nil), do: "-"
@ -174,8 +171,8 @@ defmodule ToweropsWeb.RfLinkHealthLive do
end
@doc false
def stat_card_class(:all, _), do: "ring-2 ring-cerulean-500"
def stat_card_class(filter, filter), do: "ring-2 ring-cerulean-500"
def stat_card_class(:all, _), do: "ring-2 ring-blue-500"
def stat_card_class(filter, filter), do: "ring-2 ring-blue-500"
def stat_card_class(_, _), do: ""
@doc false

View file

@ -10,8 +10,8 @@
]} />
<div class="mb-6">
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{t("RF Link Health")}</h1>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("RF Link Health")}</h1>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Wireless link quality triage — weakest links first")}
</p>
</div>
@ -21,12 +21,12 @@
<button
phx-click="filter"
phx-value-filter="all"
class={"bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-cerulean-300 dark:hover:border-cerulean-700 transition-colors #{stat_card_class(@filter, :all)}"}
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-blue-300 dark:hover:border-blue-700 transition-colors #{stat_card_class(@filter, :all)}"}
>
<div class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Total Links")}
</div>
<div class="mt-1 text-2xl font-bold text-cool-steel-900 dark:text-white">
<div class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{@summary.total}
</div>
</button>
@ -34,9 +34,9 @@
<button
phx-click="filter"
phx-value-filter="healthy"
class={"bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-green-300 dark:hover:border-green-700 transition-colors #{stat_card_class(@filter, :healthy)}"}
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-green-300 dark:hover:border-green-700 transition-colors #{stat_card_class(@filter, :healthy)}"}
>
<div class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Healthy")}
</div>
<div class="mt-1 text-2xl font-bold text-green-600 dark:text-green-400">
@ -47,12 +47,12 @@
<button
phx-click="filter"
phx-value-filter="degraded"
class={"bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-wheat-300 dark:hover:border-wheat-700 transition-colors #{stat_card_class(@filter, :degraded)}"}
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-yellow-300 dark:hover:border-yellow-700 transition-colors #{stat_card_class(@filter, :degraded)}"}
>
<div class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Degraded")}
</div>
<div class="mt-1 text-2xl font-bold text-wheat-600 dark:text-wheat-400">
<div class="mt-1 text-2xl font-bold text-yellow-600 dark:text-yellow-400">
{@summary.degraded}
</div>
</button>
@ -60,12 +60,12 @@
<button
phx-click="filter"
phx-value-filter="critical"
class={"bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-sweet-salmon-300 dark:hover:border-sweet-salmon-700 transition-colors #{stat_card_class(@filter, :critical)}"}
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-red-300 dark:hover:border-red-700 transition-colors #{stat_card_class(@filter, :critical)}"}
>
<div class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider">
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Critical")}
</div>
<div class="mt-1 text-2xl font-bold text-sweet-salmon-600 dark:text-sweet-salmon-400">
<div class="mt-1 text-2xl font-bold text-red-600 dark:text-red-400">
{@summary.critical}
</div>
</button>
@ -73,19 +73,19 @@
<%!-- Weakest Links Table --%>
<%= if @links != [] do %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-cool-steel-200 dark:border-white/10">
<h2 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("RF Links")}
<span class="text-cool-steel-400 font-normal ml-1">
<span class="text-gray-400 font-normal ml-1">
({length(@links)})
</span>
</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/75">
<tr class="text-xs text-cool-steel-600 dark:text-cool-steel-400">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-2 text-left font-medium">{t("Client")}</th>
<th class="px-4 py-2 text-left font-medium">{t("AP / Device")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Signal")}</th>
@ -95,19 +95,19 @@
<th class="px-4 py-2 text-left font-medium">{t("Last Seen")}</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-200 dark:divide-white/10">
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for link <- @links do %>
<%!-- Main row --%>
<tr
phx-click="toggle_expand"
phx-value-id={link.id}
class="text-sm hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800 transition-colors cursor-pointer"
class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
>
<td class="px-4 py-3">
<div class="font-medium text-cool-steel-900 dark:text-white">
<div class="font-medium text-gray-900 dark:text-white">
{link_name(link)}
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<div class="text-xs text-gray-500 dark:text-gray-400">
{link.mac_address}
</div>
</td>
@ -115,17 +115,17 @@
<%= if link.device do %>
<.link
navigate={~p"/devices/#{link.device_id}"}
class="text-cool-steel-900 dark:text-white hover:text-cerulean-600 dark:hover:text-cerulean-400 hover:underline"
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{device_name(link)}
</.link>
<% else %>
<span class="text-cool-steel-400">-</span>
<span class="text-gray-400">-</span>
<% end %>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-16 h-2 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 overflow-hidden">
<div class="w-16 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full #{signal_bar_color(link.signal_strength)}"}
style={"width: #{signal_bar_width(link.signal_strength)}%"}
@ -139,21 +139,21 @@
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-12 h-2 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 overflow-hidden">
<div class="w-12 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full #{snr_bar_color(link.snr)}"}
style={"width: #{snr_bar_width(link.snr)}%"}
>
</div>
</div>
<span class="text-xs text-cool-steel-600 dark:text-cool-steel-400">
<span class="text-xs text-gray-600 dark:text-gray-400">
{format_snr(link.snr)}
</span>
</div>
</td>
<td class="px-4 py-3 text-xs text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">
<span>{format_rate(link.tx_rate)}</span>
<span class="text-cool-steel-300 dark:text-cool-steel-600 mx-1">/</span>
<span class="text-gray-300 dark:text-gray-600 mx-1">/</span>
<span>{format_rate(link.rx_rate)}</span>
</td>
<td class="px-4 py-3">
@ -161,7 +161,7 @@
{health_label(link.health)}
</span>
</td>
<td class="px-4 py-3 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
<%= if link.last_seen_at do %>
<time datetime={DateTime.to_iso8601(link.last_seen_at)}>
{Calendar.strftime(link.last_seen_at, "%H:%M")}
@ -174,16 +174,16 @@
<%!-- Expandable detail row --%>
<%= if @expanded_id == link.id do %>
<tr class="bg-cool-steel-50/50 dark:bg-cool-steel-800/30">
<tr class="bg-gray-50/50 dark:bg-gray-800/30">
<td colspan="7" class="px-4 py-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%!-- Signal Strength Detail --%>
<div>
<h4 class="text-xs font-semibold text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-2">
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal Strength")}
</h4>
<div class="flex items-center gap-3">
<div class="flex-1 h-3 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 overflow-hidden">
<div class="flex-1 h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full transition-all #{signal_bar_color(link.signal_strength)}"}
style={"width: #{signal_bar_width(link.signal_strength)}%"}
@ -194,9 +194,9 @@
{format_signal(link.signal_strength)}
</span>
</div>
<div class="mt-2 flex justify-between text-[10px] text-cool-steel-400">
<div class="mt-2 flex justify-between text-[10px] text-gray-400">
<span>-100 dBm</span>
<span class="text-wheat-500">-75</span>
<span class="text-yellow-500">-75</span>
<span class="text-green-500">-65</span>
<span>-30 dBm</span>
</div>
@ -204,24 +204,24 @@
<%!-- SNR Detail --%>
<div>
<h4 class="text-xs font-semibold text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-2">
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal-to-Noise Ratio")}
</h4>
<div class="flex items-center gap-3">
<div class="flex-1 h-3 rounded-full bg-cool-steel-200 dark:bg-cool-steel-700 overflow-hidden">
<div class="flex-1 h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full transition-all #{snr_bar_color(link.snr)}"}
style={"width: #{snr_bar_width(link.snr)}%"}
>
</div>
</div>
<span class="text-sm font-bold text-cool-steel-900 dark:text-white">
<span class="text-sm font-bold text-gray-900 dark:text-white">
{format_snr(link.snr)}
</span>
</div>
<div class="mt-2 flex justify-between text-[10px] text-cool-steel-400">
<div class="mt-2 flex justify-between text-[10px] text-gray-400">
<span>0 dB</span>
<span class="text-wheat-500">15</span>
<span class="text-yellow-500">15</span>
<span class="text-green-500">25</span>
<span>40 dB</span>
</div>
@ -229,48 +229,38 @@
<%!-- Additional Metrics --%>
<div>
<h4 class="text-xs font-semibold text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-2">
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Link Details")}
</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">
{t("Distance")}
</dt>
<dd class="text-cool-steel-900 dark:text-white font-medium">
<dt class="text-gray-500 dark:text-gray-400">{t("Distance")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_distance(link.distance)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">
{t("TX Rate")}
</dt>
<dd class="text-cool-steel-900 dark:text-white font-medium">
<dt class="text-gray-500 dark:text-gray-400">{t("TX Rate")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_rate(link.tx_rate)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">
{t("RX Rate")}
</dt>
<dd class="text-cool-steel-900 dark:text-white font-medium">
<dt class="text-gray-500 dark:text-gray-400">{t("RX Rate")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_rate(link.rx_rate)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">
{t("Uptime")}
</dt>
<dd class="text-cool-steel-900 dark:text-white font-medium">
<dt class="text-gray-500 dark:text-gray-400">{t("Uptime")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_uptime(link.uptime_seconds)}
</dd>
</div>
<%= if link.capacity_utilization do %>
<div class="flex justify-between">
<dt class="text-cool-steel-500 dark:text-cool-steel-400">
{t("Capacity")}
</dt>
<dd class="text-cool-steel-900 dark:text-white font-medium">
<dt class="text-gray-500 dark:text-gray-400">{t("Capacity")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{link.capacity_utilization}%
</dd>
</div>
@ -281,8 +271,8 @@
<%!-- 24h Sparkline --%>
<%= if sparkline = @sparkline_data[link.id] do %>
<div class="mt-4 pt-4 border-t border-cool-steel-200 dark:border-white/10">
<h4 class="text-xs font-semibold text-cool-steel-500 dark:text-cool-steel-400 uppercase tracking-wider mb-2">
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-white/10">
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal Strength — Last 24 Hours")}
</h4>
<svg
@ -295,7 +285,7 @@
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-cerulean-500"
class="text-blue-500"
/>
</svg>
</div>
@ -309,15 +299,12 @@
</div>
</div>
<% else %>
<div class="bg-white dark:bg-cool-steel-800/50 rounded-lg border border-cool-steel-200 dark:border-white/10 p-12 text-center">
<.icon
name="hero-signal"
class="w-12 h-12 mx-auto text-cool-steel-300 dark:text-cool-steel-600"
/>
<h3 class="mt-4 text-sm font-semibold text-cool-steel-900 dark:text-white">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12 text-center">
<.icon name="hero-signal" class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" />
<h3 class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">
{t("No RF links found")}
</h3>
<p class="mt-1 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("RF links will appear here when wireless clients are discovered via SNMP polling.")}
</p>
</div>

View file

@ -7,11 +7,11 @@
<div class="flex items-center gap-3 mb-6">
<.link
navigate={~p"/schedules"}
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<.icon name="hero-arrow-left" class="h-5 w-5" />
</.link>
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">{@page_title}</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{@page_title}</h1>
</div>
<div class="max-w-2xl">
@ -37,7 +37,7 @@
</.button>
<.link
navigate={~p"/schedules"}
class="text-sm text-cool-steel-600 dark:text-cool-steel-400 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</.link>

View file

@ -5,14 +5,12 @@
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="flex items-center justify-between mb-1">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">
{t("On-Call & Schedules")}
</h1>
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("On-Call & Schedules")}</h1>
<div class="flex items-center gap-2">
<%= if @tab == "escalation-policies" do %>
<.link
navigate={~p"/schedules/escalation-policies/new"}
class="inline-flex items-center gap-2 rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Policy")}
@ -20,7 +18,7 @@
<% else %>
<.link
navigate={~p"/schedules/new"}
class="inline-flex items-center gap-2 rounded-lg bg-cerulean-600 px-4 py-2 text-sm font-semibold text-white hover:bg-cerulean-500 transition-colors"
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("New Schedule")}
@ -28,7 +26,7 @@
<% end %>
</div>
</div>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 mb-6">
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t("Define on-call rotations and escalation policies for alert notifications.")}
</p>
@ -37,10 +35,9 @@
patch={~p"/schedules?tab=schedules"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@tab == "schedules" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@tab == "schedules" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@tab != "schedules" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Schedules")}
@ -49,10 +46,9 @@
patch={~p"/schedules?tab=escalation-policies"}
class={[
"inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium transition-colors",
@tab == "escalation-policies" &&
"bg-cool-steel-900 text-white dark:bg-white dark:text-cool-steel-900",
@tab == "escalation-policies" && "bg-gray-900 text-white dark:bg-white dark:text-gray-900",
@tab != "escalation-policies" &&
"bg-cool-steel-100 text-cool-steel-700 hover:bg-cool-steel-200 dark:bg-cool-steel-800 dark:text-cool-steel-300 dark:hover:bg-cool-steel-700"
"bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
]}
>
{t("Escalation Policies")}
@ -65,39 +61,39 @@
<div class="text-center">
<.icon
name="hero-arrow-trending-up"
class="h-12 w-12 text-cool-steel-300 dark:text-cool-steel-600 mx-auto mb-4"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No escalation policies")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Create an escalation policy to define how alerts are escalated to on-call users.")}
</p>
</div>
</div>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Name")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Levels")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-900">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
<%= for policy <- @policies do %>
<tr
class="hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800/30 transition-colors cursor-pointer"
class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors cursor-pointer"
phx-click={JS.navigate(~p"/schedules/escalation-policies/#{policy.id}")}
>
<td class="px-4 py-3 text-sm font-medium text-cool-steel-900 dark:text-white">
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
{policy.name}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{length(policy.rules)}
</td>
</tr>
@ -111,7 +107,7 @@
<form phx-change="search" phx-submit="search" class="mb-4">
<div class="relative max-w-md">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
<.icon name="hero-magnifying-glass" class="h-4 w-4 text-cool-steel-400" />
<.icon name="hero-magnifying-glass" class="h-4 w-4 text-gray-400" />
</span>
<input
type="text"
@ -119,7 +115,7 @@
value={@search}
placeholder={t("Search schedules…")}
phx-debounce="300"
class="block w-full rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-sm pl-9 py-1.5"
class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm pl-9 py-1.5"
/>
</div>
</form>
@ -129,32 +125,32 @@
<div class="flex items-center gap-2">
<button
phx-click="today"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-1.5 text-sm font-medium text-cool-steel-700 dark:text-cool-steel-200 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{t("Today")}
</button>
<button
phx-click="prev"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 p-1.5 text-cool-steel-500 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-700"
title={t("Previous range")}
>
<.icon name="hero-chevron-left" class="h-4 w-4" />
</button>
<button
phx-click="next"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 p-1.5 text-cool-steel-500 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-700"
title={t("Next range")}
>
<.icon name="hero-chevron-right" class="h-4 w-4" />
</button>
<span class="ml-2 text-sm font-semibold text-cool-steel-900 dark:text-white">
<span class="ml-2 text-sm font-semibold text-gray-900 dark:text-white">
{window_label(@start_date, @range_days)}
</span>
</div>
<form phx-change="set_range" class="inline-flex">
<select
name="range"
class="rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-sm py-1.5"
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
>
<option value="7" selected={@range_days == 7}>{t("1 Week")}</option>
<option value="14" selected={@range_days == 14}>{t("2 Weeks")}</option>
@ -168,12 +164,12 @@
<div class="text-center">
<.icon
name="hero-calendar-days"
class="h-12 w-12 text-cool-steel-300 dark:text-cool-steel-600 mx-auto mb-4"
class="h-12 w-12 text-gray-300 dark:text-gray-600 mx-auto mb-4"
/>
<h3 class="text-lg font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{t("No schedules")}
</h3>
<p class="mt-2 text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Create an on-call schedule to define rotation coverage.")}
</p>
</div>
@ -183,23 +179,21 @@
<%= for schedule <- @schedules do %>
<% days = days_in_window(@start_date, @range_days) %>
<% today_idx = today_offset(@start_date, @range_days) %>
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4">
<div class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
<div class="flex items-center justify-between mb-3">
<div>
<.link
navigate={~p"/schedules/#{schedule.id}"}
class="text-base font-semibold text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="text-base font-semibold text-blue-600 dark:text-blue-400 hover:underline"
>
{schedule.name}
</.link>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{schedule.timezone}
</p>
</div>
<div class="text-right">
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
{t("On-Call Now")}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{t("On-Call Now")}</p>
<%= if schedule.current_on_call do %>
<span class="inline-flex items-center gap-1.5">
<span class={[
@ -207,27 +201,25 @@
schedule.current_on_call_color
]}>
</span>
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{schedule.current_on_call_name}
</span>
</span>
<% else %>
<span class="text-sm text-cool-steel-400 dark:text-cool-steel-500">
{t("No one")}
</span>
<span class="text-sm text-gray-400 dark:text-gray-500">{t("No one")}</span>
<% end %>
</div>
</div>
<%!-- Day header row --%>
<div
class="grid gap-px text-xs text-cool-steel-500 dark:text-cool-steel-400 mb-1"
class="grid gap-px text-xs text-gray-500 dark:text-gray-400 mb-1"
style={"grid-template-columns: repeat(#{@range_days}, minmax(0, 1fr));"}
>
<%= for {day, idx} <- Enum.with_index(days) do %>
<div class={[
"px-1 py-0.5 text-center",
today_idx == idx && "font-bold text-sweet-salmon-600 dark:text-sweet-salmon-400"
today_idx == idx && "font-bold text-red-600 dark:text-red-400"
]}>
<div class="leading-tight">{Calendar.strftime(day, "%a")}</div>
<div class="leading-tight">{Calendar.strftime(day, "%-d")}</div>
@ -237,7 +229,7 @@
<%!-- Gantt strip --%>
<div
class="relative grid gap-px h-8 rounded-md overflow-hidden bg-cool-steel-100 dark:bg-cool-steel-800"
class="relative grid gap-px h-8 rounded-md overflow-hidden bg-gray-100 dark:bg-gray-800"
style={"grid-template-columns: repeat(#{@range_days}, minmax(0, 1fr));"}
>
<%= for segment <- schedule.segments do %>
@ -254,7 +246,7 @@
<% end %>
<%= if today_idx do %>
<div
class="pointer-events-none absolute top-0 bottom-0 w-px bg-sweet-salmon-500"
class="pointer-events-none absolute top-0 bottom-0 w-px bg-red-500"
style={"left: calc((100% / #{@range_days}) * #{today_idx});"}
>
</div>

View file

@ -8,21 +8,19 @@
<div class="flex items-center gap-3 min-w-0">
<.link
navigate={~p"/schedules"}
class="shrink-0 text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-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" />
</.link>
<div class="min-w-0">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white truncate">
{@schedule.name}
</h1>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">{@schedule.timezone}</p>
<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>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<.link
navigate={~p"/schedules/#{@schedule.id}/edit"}
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 border border-cool-steel-300 dark:border-cool-steel-600 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-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" />
<span class="hidden sm:inline">{t("Edit")}</span>
@ -30,7 +28,7 @@
<button
phx-click="delete"
data-confirm={t("Are you sure you want to delete this schedule?")}
class="inline-flex items-center gap-2 rounded-lg bg-white dark:bg-cool-steel-800 px-3 py-2 text-sm font-medium text-sweet-salmon-600 dark:text-sweet-salmon-400 border border-cool-steel-300 dark:border-cool-steel-600 hover:bg-sweet-salmon-50 dark:hover:bg-sweet-salmon-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" />
<span class="hidden sm:inline">{t("Delete")}</span>
@ -39,38 +37,36 @@
</div>
<%= if @schedule.description do %>
<p class="mb-6 text-sm text-cool-steel-600 dark:text-cool-steel-400">
{@schedule.description}
</p>
<p class="mb-6 text-sm text-gray-600 dark:text-gray-400">{@schedule.description}</p>
<% end %>
<%!-- On-Call Status --%>
<div class="mb-8 rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4">
<div class="mb-8 rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4">
<div class="flex items-center gap-3">
<div class={[
"h-10 w-10 rounded-full flex items-center justify-center",
@on_call && "bg-green-100 dark:bg-green-900/40",
!@on_call && "bg-cool-steel-100 dark:bg-cool-steel-800"
!@on_call && "bg-gray-100 dark:bg-gray-800"
]}>
<.icon
name="hero-user"
class={[
"h-5 w-5",
@on_call && "text-green-600 dark:text-green-400",
!@on_call && "text-cool-steel-400 dark:text-cool-steel-500"
!@on_call && "text-gray-400 dark:text-gray-500"
]}
/>
</div>
<div>
<p class="text-xs font-medium uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Currently On-Call")}
</p>
<%= if @on_call do %>
<p class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<p class="text-sm font-semibold text-gray-900 dark:text-white">
{display_name(@on_call)}
</p>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No one is currently on-call")}
</p>
<% end %>
@ -85,30 +81,30 @@
<div class="flex items-center gap-2">
<button
phx-click="timeline_today"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 px-3 py-1.5 text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
>
{t("Today")}
</button>
<button
phx-click="timeline_prev"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 p-1.5 text-cool-steel-500 dark:text-cool-steel-400 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<.icon name="hero-chevron-left" class="h-4 w-4" />
</button>
<button
phx-click="timeline_next"
class="rounded-md border border-cool-steel-300 dark:border-cool-steel-600 bg-white dark:bg-cool-steel-800 p-1.5 text-cool-steel-500 dark:text-cool-steel-400 hover:bg-cool-steel-50 dark:hover:bg-cool-steel-700"
class="rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 p-1.5 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700"
>
<.icon name="hero-chevron-right" class="h-4 w-4" />
</button>
<span class="text-sm font-semibold text-cool-steel-900 dark:text-white ml-2">
<span class="text-sm font-semibold text-gray-900 dark:text-white ml-2">
{Calendar.strftime(@timeline_start, "%b %-d")} {Calendar.strftime(
@timeline_end,
"%b %-d, %Y"
)}
</span>
</div>
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-sm text-gray-500 dark:text-gray-400">
{t("2 Weeks")}
</span>
</div>
@ -116,8 +112,8 @@
<%!-- Layer timelines --%>
<%= if @layer_timelines != [] do %>
<div class="overflow-x-auto">
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4 mb-4 min-w-[320px]">
<h3 class="text-sm font-semibold text-cool-steel-500 dark:text-cool-steel-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]">
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
{t("Configuration Layers")}
</h3>
@ -128,8 +124,8 @@
<div class="w-28 shrink-0"></div>
<div class="flex-1 flex">
<%= for day <- @timeline_days do %>
<div class="flex-1 text-center border-l first:border-l-0 border-cool-steel-200 dark:border-cool-steel-700">
<span class="text-[10px] text-cool-steel-400 dark:text-cool-steel-500">
<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">
{Calendar.strftime(day, "%-d %a")}
</span>
</div>
@ -139,16 +135,16 @@
<%!-- Timeline bar --%>
<div class="flex items-center">
<div class="w-28 shrink-0 pr-2 text-right">
<span class="text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300 truncate">
<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-cool-steel-100 dark:bg-cool-steel-800 rounded overflow-hidden">
<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-cool-steel-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}%;"}
>
@ -161,7 +157,7 @@
<% 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-sweet-salmon-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}%;"}
>
</div>
@ -175,8 +171,8 @@
<%!-- Final Schedule --%>
<div class="overflow-x-auto">
<div class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4 min-w-[320px]">
<h3 class="text-sm font-semibold text-cool-steel-500 dark:text-cool-steel-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]">
<h3 class="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">
{t("Final Schedule")}
</h3>
<%!-- Day headers --%>
@ -184,8 +180,8 @@
<div class="w-28 shrink-0"></div>
<div class="flex-1 flex">
<%= for day <- @timeline_days do %>
<div class="flex-1 text-center border-l first:border-l-0 border-cool-steel-200 dark:border-cool-steel-700">
<span class="text-[10px] text-cool-steel-400 dark:text-cool-steel-500">
<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">
{Calendar.strftime(day, "%-d %a")}
</span>
</div>
@ -195,12 +191,12 @@
<%!-- Timeline bar --%>
<div class="flex items-center">
<div class="w-28 shrink-0"></div>
<div class="flex-1 relative h-8 bg-cool-steel-100 dark:bg-cool-steel-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 %>
<div
class={[
"absolute inset-y-0 flex items-center px-1 overflow-hidden",
@user_colors[span.user.id] || "bg-cool-steel-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}%;"}
>
@ -213,7 +209,7 @@
<% 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-sweet-salmon-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}%;"}
>
</div>
@ -228,10 +224,10 @@
<%!-- Layers --%>
<div class="mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">{t("Layers")}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Layers")}</h2>
<button
phx-click="toggle_add_layer"
class="inline-flex items-center gap-1.5 text-sm font-medium text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-500"
class="inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("Add Layer")}
@ -239,7 +235,7 @@
</div>
<%= if @show_add_layer do %>
<div class="mb-4 rounded-lg border border-cerulean-200 dark:border-cerulean-800 bg-cerulean-50/50 dark:bg-cerulean-900/10 p-4">
<div class="mb-4 rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-4">
<.form for={@layer_form} phx-change="validate_layer" phx-submit="save_layer">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<.input
@ -284,19 +280,19 @@
<%!-- User selection for new layer --%>
<div class="mb-4">
<label class="block text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300 mb-2">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
{t("Members")}
</label>
<%= if @new_layer_members != [] do %>
<div class="flex flex-wrap gap-2 mb-2">
<%= for user <- @new_layer_members do %>
<span class="inline-flex items-center gap-1 rounded-full bg-cerulean-100 dark:bg-cerulean-900/40 pl-2.5 pr-1 py-0.5 text-xs font-medium text-cerulean-700 dark:text-cerulean-300">
<span class="inline-flex items-center gap-1 rounded-full bg-blue-100 dark:bg-blue-900/40 pl-2.5 pr-1 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">
{display_name(user)}
<button
type="button"
phx-click="new_layer_remove_user"
phx-value-id={user.id}
class="ml-0.5 rounded-full p-0.5 hover:bg-cerulean-200 dark:hover:bg-cerulean-800 transition-colors"
class="ml-0.5 rounded-full p-0.5 hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors"
>
<.icon name="hero-x-mark" class="h-3 w-3" />
</button>
@ -310,7 +306,7 @@
<select
phx-change="new_layer_add_user"
name="user_id"
class="block w-full max-w-xs rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-sm py-1.5"
class="block w-full max-w-xs rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
>
<option value="">{t("Select a user")}</option>
<%= for user <- available do %>
@ -327,7 +323,7 @@
<button
type="button"
phx-click="toggle_add_layer"
class="text-sm text-cool-steel-600 dark:text-cool-steel-400 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</button>
@ -337,8 +333,8 @@
<% end %>
<%= if Enum.empty?(@schedule.layers) do %>
<div class="rounded-lg border border-dashed border-cool-steel-300 dark:border-cool-steel-600 p-6 text-center">
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-6 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No layers configured. Add a layer to define rotation coverage.")}
</p>
</div>
@ -355,16 +351,16 @@
<div
data-id={layer.id}
draggable="true"
class="rounded-lg border border-cool-steel-200 dark:border-white/10 bg-white dark:bg-cool-steel-900 p-4 cursor-move"
class="rounded-lg border border-gray-200 dark:border-white/10 bg-white dark:bg-gray-900 p-4 cursor-move"
>
<div class="flex items-start justify-between mb-3 gap-3">
<div class="flex items-start gap-2 min-w-0">
<.icon name="hero-bars-3" class="h-4 w-4 text-cool-steel-400 mt-0.5 shrink-0" />
<.icon name="hero-bars-3" class="h-4 w-4 text-gray-400 mt-0.5 shrink-0" />
<div class="min-w-0">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white truncate">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white truncate">
{layer.name}
</h3>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400 truncate">
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
{String.capitalize(layer.rotation_type)} rotation, every {layer.rotation_interval}
<%= case layer.rotation_type do %>
<% "daily" -> %>
@ -383,7 +379,7 @@
phx-click="move_layer"
phx-value-id={layer.id}
phx-value-direction="up"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300 disabled:opacity-30 disabled:hover:text-cool-steel-400"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
disabled={idx == 0}
title={t("Move up")}
>
@ -393,7 +389,7 @@
phx-click="move_layer"
phx-value-id={layer.id}
phx-value-direction="down"
class="text-cool-steel-400 hover:text-cool-steel-600 dark:hover:text-cool-steel-300 disabled:opacity-30 disabled:hover:text-cool-steel-400"
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30 disabled:hover:text-gray-400"
disabled={idx == layer_count - 1}
title={t("Move down")}
>
@ -403,7 +399,7 @@
phx-click="delete_layer"
phx-value-id={layer.id}
data-confirm={t("Are you sure you want to delete this layer?")}
class="text-cool-steel-400 hover:text-sweet-salmon-500 dark:hover:text-sweet-salmon-400 transition-colors ml-1"
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors ml-1"
title={t("Delete")}
>
<.icon name="hero-trash" class="h-4 w-4" />
@ -413,22 +409,22 @@
<%!-- Members --%>
<div class="mb-2">
<p class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 mb-2">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
{t("Members")}
</p>
<%= if Enum.empty?(layer.members) do %>
<p class="text-xs text-cool-steel-400 dark:text-cool-steel-500 mb-2">
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">
{t("No members yet")}
</p>
<% else %>
<div class="flex flex-wrap gap-2 mb-2">
<%= for member <- Enum.sort_by(layer.members, & &1.position) do %>
<span class="inline-flex items-center gap-1 rounded-full bg-cool-steel-100 dark:bg-cool-steel-800 pl-2.5 pr-1 py-0.5 text-xs font-medium text-cool-steel-700 dark:text-cool-steel-300">
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 dark:bg-gray-800 pl-2.5 pr-1 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">
{display_name(member.user)}
<button
phx-click="remove_member"
phx-value-id={member.id}
class="ml-0.5 rounded-full p-0.5 hover:bg-cool-steel-200 dark:hover:bg-cool-steel-700 transition-colors"
class="ml-0.5 rounded-full p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<.icon name="hero-x-mark" class="h-3 w-3" />
</button>
@ -445,7 +441,7 @@
phx-change="add_member"
name="user_id"
phx-value-layer_id={layer.id}
class="block w-full max-w-xs rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-sm py-1.5"
class="block w-full max-w-xs rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-sm py-1.5"
>
<option value="">{t("Select a user")}</option>
<%= for user <- available_users do %>
@ -456,8 +452,8 @@
</div>
<%!-- Restrictions editor --%>
<div class="mt-3 pt-3 border-t border-cool-steel-100 dark:border-white/5">
<p class="text-xs font-medium text-cool-steel-500 dark:text-cool-steel-400 mb-2">
<div class="mt-3 pt-3 border-t border-gray-100 dark:border-white/5">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
{t("Restrict on-call shifts")}
</p>
<form
@ -468,7 +464,7 @@
<input type="hidden" name="layer_id" value={layer.id} />
<select
name="restriction_type"
class="block rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-xs py-1"
class="block rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
>
<option value="" selected={is_nil(layer.restriction_type)}>
{t("Always on call (no restriction)")}
@ -481,25 +477,25 @@
</option>
</select>
<%= if layer.restriction_type in ["time_of_day", "time_of_week"] do %>
<div class="flex items-center gap-2 text-xs text-cool-steel-700 dark:text-cool-steel-300">
<div class="flex items-center gap-2 text-xs text-gray-700 dark:text-gray-300">
<span>{t("From")}</span>
<input
type="time"
name="start_time"
value={layer.restrictions["start_time"] || "09:00"}
class="rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-xs py-1"
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
/>
<span>{t("to")}</span>
<input
type="time"
name="end_time"
value={layer.restrictions["end_time"] || "17:00"}
class="rounded-md border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800 dark:text-white text-xs py-1"
class="rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white text-xs py-1"
/>
</div>
<% end %>
<%= if layer.restriction_type == "time_of_week" do %>
<div class="flex flex-wrap gap-2 text-xs text-cool-steel-700 dark:text-cool-steel-300">
<div class="flex flex-wrap gap-2 text-xs text-gray-700 dark:text-gray-300">
<% selected_days =
((layer.restrictions || %{})["days"] || [])
|> Enum.map(fn d ->
@ -515,7 +511,7 @@
name={"days[#{day}]"}
value="true"
checked={day in selected_days}
class="rounded border-cool-steel-300 dark:border-cool-steel-600 dark:bg-cool-steel-800"
class="rounded border-gray-300 dark:border-gray-600 dark:bg-gray-800"
/>
<span>{label}</span>
</label>
@ -533,10 +529,10 @@
<%!-- Overrides --%>
<div>
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white">{t("Overrides")}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Overrides")}</h2>
<button
phx-click="toggle_add_override"
class="inline-flex items-center gap-1.5 text-sm font-medium text-cerulean-600 dark:text-cerulean-400 hover:text-cerulean-500"
class="inline-flex items-center gap-1.5 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500"
>
<.icon name="hero-plus" class="h-4 w-4" />
{t("Add Override")}
@ -544,7 +540,7 @@
</div>
<%= if @show_add_override do %>
<div class="mb-4 rounded-lg border border-cerulean-200 dark:border-cerulean-800 bg-cerulean-50/50 dark:bg-cerulean-900/10 p-4">
<div class="mb-4 rounded-lg border border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-4">
<.form for={@override_form} phx-change="validate_override" phx-submit="save_override">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<.input
@ -572,7 +568,7 @@
<button
type="button"
phx-click="toggle_add_override"
class="text-sm text-cool-steel-600 dark:text-cool-steel-400 hover:text-cool-steel-900 dark:hover:text-white"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
>
{t("Cancel")}
</button>
@ -582,38 +578,38 @@
<% end %>
<%= if Enum.empty?(@schedule.overrides) do %>
<div class="rounded-lg border border-dashed border-cool-steel-300 dark:border-cool-steel-600 p-6 text-center">
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-dashed border-gray-300 dark:border-gray-600 p-6 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No overrides configured.")}
</p>
</div>
<% else %>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/50">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("User")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("Start")}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
{t("End")}
</th>
<th class="px-4 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5 bg-white dark:bg-cool-steel-900">
<tbody class="divide-y divide-gray-100 dark:divide-white/5 bg-white dark:bg-gray-900">
<%= for override <- @schedule.overrides do %>
<tr>
<td class="px-4 py-3 text-sm text-cool-steel-900 dark:text-white">
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
{display_name(override.user)}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{Calendar.strftime(override.start_time, "%Y-%m-%d %H:%M")}
</td>
<td class="px-4 py-3 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{Calendar.strftime(override.end_time, "%Y-%m-%d %H:%M")}
</td>
<td class="px-4 py-3 text-right">
@ -621,7 +617,7 @@
phx-click="delete_override"
phx-value-id={override.id}
data-confirm={t("Are you sure?")}
class="text-cool-steel-400 hover:text-sweet-salmon-500 dark:hover:text-sweet-salmon-400 transition-colors"
class="text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors"
>
<.icon name="hero-trash" class="h-4 w-4" />
</button>

View file

@ -10,7 +10,7 @@
do: ~p"/sites/#{@site.id}",
else: ~p"/sites"
}
class="inline-flex items-center gap-1 text-sm text-cool-steel-600 hover:text-cool-steel-900 dark:text-cool-steel-400 dark:hover:text-white"
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-4 w-4" />
{if @live_action == :edit, do: "Back to Site", else: "Back to Sites"}
@ -32,16 +32,14 @@
<div class="mb-4">
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Parent Site")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">
(optional)
</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</span>
<select
id={@form[:parent_site_id].id}
name={@form[:parent_site_id].name}
class="block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
>
<option value="">No parent site</option>
{Phoenix.HTML.Form.options_for_select(
@ -54,18 +52,15 @@
<div class="mb-4">
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
Location
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">
(optional)
</span>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Location <span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</span>
<input
type="text"
id={@form[:location].id}
name={@form[:location].name}
value={@form[:location].value}
class="block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
/>
</label>
</div>
@ -74,16 +69,16 @@
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
{t("Geographic Location")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">(optional)</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Add geographic coordinates to enable network mapping and spatial analysis.")}
</p>
<div class="space-y-4">
<div>
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Address")}
</span>
<div class="flex gap-2">
@ -93,17 +88,17 @@
name={@form[:address].name}
value={@form[:address].value}
placeholder="123 Main St, City, State, Country"
class="flex-1 rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="flex-1 rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
/>
<button
type="button"
phx-click="geocode"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-cerulean-600 hover:bg-cerulean-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-cerulean-500 dark:focus:ring-offset-cool-steel-800"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"
>
{t("Geocode")}
</button>
</div>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t(
"Enter a full address and click Geocode to automatically fill latitude/longitude"
)}
@ -114,7 +109,7 @@
<div class="grid grid-cols-2 gap-4">
<div>
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Latitude")}
</span>
<input
@ -126,9 +121,9 @@
placeholder="40.7128"
min="-90"
max="90"
class="block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
/>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
-90 to 90
</p>
</label>
@ -136,7 +131,7 @@
<div>
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Longitude")}
</span>
<input
@ -148,9 +143,9 @@
placeholder="-74.0060"
min="-180"
max="180"
class="block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
/>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
-180 to 180
</p>
</label>
@ -160,10 +155,10 @@
<!-- Map Preview -->
<%= if @form[:latitude].value && @form[:longitude].value do %>
<div class="mt-4">
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Map Preview")}
</span>
<div class="border border-cool-steel-300 dark:border-cool-steel-600 rounded-lg overflow-hidden">
<div class="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
<img
src={"https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/#{@form[:longitude].value},#{@form[:latitude].value},14,0/400x200?access_token=pk.1234"}
alt="Location preview"
@ -171,7 +166,7 @@
onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjIwMCIgdmlld0JveD0iMCAwIDQwMCAyMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSI0MDAiIGhlaWdodD0iMjAwIiBmaWxsPSIjRjNGNEY2Ii8+Cjx0ZXh0IHg9IjIwMCIgeT0iMTAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNiIgZmlsbD0iIzZCNzI4MCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9ImNlbnRyYWwiPk1hcCBwcmV2aWV3IG5vdCBhdmFpbGFibGU8L3RleHQ+Cjwvc3ZnPgo='; this.alt='Map preview not available';"
/>
</div>
<p class="mt-1 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Coordinates: {@form[:latitude].value}, {@form[:longitude].value}
</p>
</div>
@ -181,16 +176,14 @@
<div class="mb-4">
<label>
<span class="block text-sm font-medium text-cool-steel-900 dark:text-white mb-2">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
{t("Description")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">
(optional)
</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</span>
<textarea
id={@form[:description].id}
name={@form[:description].name}
class="block w-full rounded-lg border-0 py-2 px-3 text-cool-steel-900 shadow-sm ring-1 ring-inset ring-cool-steel-300 focus:ring-2 focus:ring-inset focus:ring-cerulean-600 sm:text-sm sm:leading-6 dark:bg-cool-steel-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-cerulean-500"
class="block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:focus:ring-blue-500"
>{Phoenix.HTML.Form.normalize_value("textarea", @form[:description].value)}</textarea>
</label>
</div>
@ -199,11 +192,9 @@
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
{t("Agent Configuration")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">
(optional)
</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t(
"Set a default agent for SNMP polling at this site. This will override the organization default for all devices at this site. Leave blank to inherit from organization."
)}
@ -218,17 +209,17 @@
/>
<%= if @live_action == :edit and @site.agent_token_id do %>
<p class="mt-1 text-sm text-wheat-600 dark:text-wheat-400 flex items-center gap-1">
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>{t("Overriding")}</strong> organization default -
all devices at this site will use this agent
</p>
<div class="mt-4 p-3 bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 rounded-lg">
<p class="text-sm font-medium text-wheat-900 dark:text-wheat-200 mb-2">
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to Site Device")}
</p>
<p class="text-sm text-wheat-700 dark:text-wheat-300 mb-3">
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t("This will assign this agent to ALL devices at this site.")}
</p>
<.button
@ -246,12 +237,12 @@
</div>
<% else %>
<%= if Map.get(assigns, :org_agent) do %>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400 flex items-center gap-1">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-building-office-2" class="h-4 w-4" />
Currently inheriting organization default: <strong>{@org_agent.name}</strong>
</p>
<% else %>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t(
"Set a default agent for all devices at this site. Device can override this setting individually."
)}
@ -264,9 +255,9 @@
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
{t("SNMP Configuration")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">(optional)</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t(
"Override organization SNMP defaults for all devices at this site. Leave blank to inherit from organization."
)}
@ -371,11 +362,11 @@
/>
<%= if @live_action == :edit and @site.snmp_community do %>
<div class="mt-4 p-3 bg-wheat-50 dark:bg-wheat-900/20 border border-wheat-200 dark:border-wheat-800 rounded-lg">
<p class="text-sm font-medium text-wheat-900 dark:text-wheat-200 mb-2">
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
{t("Force Apply to Site Device")}
</p>
<p class="text-sm text-wheat-700 dark:text-wheat-300 mb-3">
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t("This will override SNMP settings for ALL devices at this site.")}
</p>
<.button
@ -396,17 +387,15 @@
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
{t("MikroTik API Configuration")}
<span class="text-cool-steel-400 dark:text-cool-steel-500 font-normal">
(optional)
</span>
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t(
"Override organization MikroTik API defaults for all devices at this site. Leave blank to inherit from organization. Only applies to MikroTik devices."
)}
</p>
<div class="mb-4 rounded-md bg-cerulean-50 dark:bg-cerulean-900/20 p-3">
<p class="text-xs text-cerulean-700 dark:text-cerulean-300">
<div class="mb-4 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
@ -448,8 +437,8 @@
/>
<%= if @form[:mikrotik_use_ssl].value == false do %>
<div class="mt-3 rounded-md bg-sweet-salmon-50 dark:bg-sweet-salmon-900/20 p-4">
<p class="text-sm text-sweet-salmon-700 dark:text-sweet-salmon-300">
<div class="mt-3 rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>🚨 Critical Security Warning:</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network. This setting is"
@ -459,8 +448,8 @@
</p>
</div>
<% else %>
<div class="mt-3 rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-4">
<p class="text-sm text-wheat-700 dark:text-wheat-300">
<div class="mt-3 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
<strong>⚠️ Security Warning:</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible."
@ -479,11 +468,11 @@
</.form>
<%= if @live_action == :edit do %>
<div class="mt-12 pt-8 border-t border-cool-steel-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-sweet-salmon-600 dark:text-sweet-salmon-500 mb-4">
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-red-600 dark:text-red-500 mb-4">
{t("Danger Zone")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 mb-4">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t(
"Once you delete a site, there is no going back. This will also delete all devices at this site."
)}

View file

@ -55,33 +55,32 @@ defmodule ToweropsWeb.SiteLive.Index do
def format_qoe(score), do: :erlang.float_to_binary(score / 1, decimals: 1)
@doc false
def qoe_color(nil), do: "text-cool-steel-400 dark:text-cool-steel-500"
def qoe_color(score) when score < 2.0, do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def qoe_color(score) when score < 4.0, do: "text-wheat-600 dark:text-wheat-400"
def qoe_color(nil), do: "text-gray-400 dark:text-gray-500"
def qoe_color(score) when score < 2.0, do: "text-red-600 dark:text-red-400"
def qoe_color(score) when score < 4.0, do: "text-yellow-600 dark:text-yellow-400"
def qoe_color(_), do: "text-green-600 dark:text-green-400"
@doc false
def health_dot(nil), do: "bg-cool-steel-400"
def health_dot(:red), do: "bg-sweet-salmon-500"
def health_dot(:yellow), do: "bg-wheat-500"
def health_dot(nil), do: "bg-gray-400"
def health_dot(:red), do: "bg-red-500"
def health_dot(:yellow), do: "bg-yellow-500"
def health_dot(_), do: "bg-green-500"
@doc false
def urgency_classes("critical"),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def urgency_classes("critical"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
def urgency_classes("warning"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def urgency_classes("warning"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def urgency_classes("info"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def urgency_classes("info"), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
def urgency_classes(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
def urgency_classes(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
@doc false
def source_classes("preseem"), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
def source_classes("snmp"), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
def source_classes("gaiia"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def source_classes("gaiia"), do: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
def source_classes(_), do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-800 dark:text-cool-steel-400"
def source_classes(_), do: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
end

View file

@ -24,16 +24,11 @@
<div class="flex items-center justify-center py-16">
<div class="card shadow-md border dark:border-white/10 max-w-md w-full">
<div class="items-center text-center">
<div class="rounded-full bg-cerulean-100 dark:bg-cerulean-900/40 p-4 mb-2">
<.icon
name="hero-map-pin-solid"
class="h-12 w-12 text-cerulean-500 dark:text-cerulean-400"
/>
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
<.icon name="hero-map-pin-solid" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
</div>
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white">
{t("No Sites Yet")}
</h3>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{t("No Sites Yet")}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
{t("Create your first site to organize devices by tower location.")}
</p>
<div class="mt-4 flex gap-2">
@ -46,45 +41,45 @@
</div>
<% else %>
<%!-- Sites table --%>
<div class="mt-6 overflow-x-auto rounded-lg border border-cool-steel-200 dark:border-white/10">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<thead class="bg-cool-steel-50 dark:bg-cool-steel-800/80">
<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">
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Site")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Devices")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Down")}
</th>
<th
class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-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 (0100)")}
>
{t("QoE")}
</th>
<th class="hidden sm:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th class="hidden md:table-cell px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-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")}
</th>
<th class="hidden lg:table-cell px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-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")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 bg-white dark:divide-white/5 dark:bg-cool-steel-800/50">
<tr :for={site <- @sites} class="hover:bg-cool-steel-50 dark:hover:bg-white/5">
<tbody class="divide-y divide-gray-100 bg-white dark:divide-white/5 dark:bg-gray-800/50">
<tr :for={site <- @sites} class="hover:bg-gray-50 dark:hover:bg-white/5">
<td class="px-4 py-2.5">
<.link
navigate={~p"/sites/#{site.id}"}
class="flex items-center gap-2 text-sm font-medium text-cool-steel-900 hover:text-cerulean-600 dark:text-white dark:hover:text-cerulean-400"
class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
<span class={[
"h-3 w-3 rounded-full flex-shrink-0",
@ -93,23 +88,20 @@
</span>
{site.name}
</.link>
<p
:if={site.parent_site}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400 ml-4"
>
<p :if={site.parent_site} class="text-xs text-gray-500 dark:text-gray-400 ml-4">
↳ {site.parent_site.name}
</p>
</td>
<td class="px-4 py-2.5 text-right text-sm text-cool-steel-600 dark:text-cool-steel-400">
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
{if @summary_map[site.id], do: @summary_map[site.id].device_count, else: 0}
</td>
<td class="px-4 py-2.5 text-right text-sm">
<%= if @summary_map[site.id] && @summary_map[site.id].devices_down > 0 do %>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">
<span class="text-red-600 dark:text-red-400">
{@summary_map[site.id].devices_down}
</span>
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-500">0</span>
<span class="text-gray-400 dark:text-gray-500">0</span>
<% end %>
</td>
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm">
@ -118,30 +110,30 @@
{format_qoe(@summary_map[site.id].avg_qoe)}
</span>
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-500">—</span>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
<td class="hidden sm:table-cell px-4 py-2.5 text-right text-sm text-cool-steel-600 dark:text-cool-steel-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 %>
{@summary_map[site.id].subscribers}
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-500">—</span>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
<td class="hidden md:table-cell px-4 py-2.5 text-sm text-cool-steel-500 dark:text-cool-steel-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 || "—"}
</td>
<td class="hidden lg:table-cell px-4 py-2.5 text-right">
<%= if site.latitude && site.longitude do %>
<span class="text-xs font-mono text-cool-steel-400 dark:text-cool-steel-500">
<span class="text-xs font-mono text-gray-400 dark:text-gray-500">
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
</span>
<% else %>
<span class="text-cool-steel-400 dark:text-cool-steel-500 text-sm">—</span>
<span class="text-gray-400 dark:text-gray-500 text-sm">—</span>
<% end %>
</td>
<td class="hidden lg:table-cell px-4 py-2.5 text-right text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-cool-steel-400 dark:text-cool-steel-500">—</span>
<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>
</td>
</tr>
</tbody>
@ -152,21 +144,21 @@
<%= if @insights != [] do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-cool-steel-500 dark:text-cool-steel-400">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Insights")}
</h2>
<.link
navigate={~p"/insights"}
class="text-xs font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400"
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
View all →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-cool-steel-200 dark:border-white/10 divide-y divide-cool-steel-100 dark:divide-white/5">
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<div
:for={insight <- @insights}
id={"insight-#{insight.id}"}
class="flex items-start justify-between gap-3 px-4 py-3 bg-white dark:bg-cool-steel-800/50 hover:bg-cool-steel-50 dark:hover:bg-white/5"
class="flex items-start justify-between gap-3 px-4 py-3 bg-white dark:bg-gray-800/50 hover:bg-gray-50 dark:hover:bg-white/5"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
@ -183,12 +175,12 @@
{insight.source}
</span>
</div>
<p class="mt-1 text-sm font-medium text-cool-steel-900 dark:text-white">
<p class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
{insight.title}
</p>
<p
:if={insight.description}
class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400 line-clamp-1"
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-1"
>
{insight.description}
</p>
@ -197,7 +189,7 @@
type="button"
phx-click="dismiss_insight"
phx-value-id={insight.id}
class="flex-shrink-0 rounded p-1 text-cool-steel-300 hover:bg-cool-steel-100 hover:text-cool-steel-500 dark:text-cool-steel-600 dark:hover:bg-white/10 dark:hover:text-cool-steel-400"
class="flex-shrink-0 rounded p-1 text-gray-300 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-400"
title="Dismiss"
>
<.icon name="hero-x-mark" class="h-3.5 w-3.5" />

View file

@ -168,30 +168,30 @@ defmodule ToweropsWeb.SiteLive.Show do
defp format_number(number), do: to_string(number)
@doc false
def qoe_color(nil), do: "text-cool-steel-400 dark:text-cool-steel-500"
def qoe_color(nil), do: "text-gray-400 dark:text-gray-500"
def qoe_color(score) when score >= 8.0, do: "text-green-600 dark:text-green-400"
def qoe_color(score) when score >= 6.0, do: "text-wheat-600 dark:text-wheat-400"
def qoe_color(_), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def qoe_color(score) when score >= 6.0, do: "text-yellow-600 dark:text-yellow-400"
def qoe_color(_), do: "text-red-600 dark:text-red-400"
@doc false
def qoe_bg(nil), do: "bg-cool-steel-50 border-cool-steel-200 dark:bg-cool-steel-800/50 dark:border-white/10"
def qoe_bg(nil), do: "bg-gray-50 border-gray-200 dark:bg-gray-800/50 dark:border-white/10"
def qoe_bg(score) when score >= 8.0, do: "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800/30"
def qoe_bg(score) when score >= 6.0, do: "bg-wheat-50 border-wheat-200 dark:bg-wheat-900/20 dark:border-wheat-800/30"
def qoe_bg(score) when score >= 6.0,
do: "bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800/30"
def qoe_bg(_),
do: "bg-sweet-salmon-50 border-sweet-salmon-200 dark:bg-sweet-salmon-900/20 dark:border-sweet-salmon-800/30"
def qoe_bg(_), do: "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800/30"
@doc false
def capacity_bar_color(nil), do: "bg-cool-steel-300"
def capacity_bar_color(nil), do: "bg-gray-300"
def capacity_bar_color(score) when score >= 70, do: "bg-green-500"
def capacity_bar_color(score) when score >= 40, do: "bg-wheat-500"
def capacity_bar_color(_), do: "bg-sweet-salmon-500"
def capacity_bar_color(score) when score >= 40, do: "bg-yellow-500"
def capacity_bar_color(_), do: "bg-red-500"
@doc false
def status_dot_class(:up), do: "bg-green-500"
def status_dot_class(:down), do: "bg-sweet-salmon-500"
def status_dot_class(_), do: "bg-cool-steel-400"
def status_dot_class(:down), do: "bg-red-500"
def status_dot_class(_), do: "bg-gray-400"
@doc false
def time_ago(nil), do: ""
@ -218,18 +218,17 @@ defmodule ToweropsWeb.SiteLive.Show do
def change_size_label(_), do: "Large"
@doc false
def change_size_color(size) when size < 100,
do: "bg-cool-steel-100 text-cool-steel-600 dark:bg-cool-steel-700 dark:text-cool-steel-300"
def change_size_color(size) when size < 100, do: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
def change_size_color(size) when size < 500, do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def change_size_color(size) when size < 500,
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def change_size_color(_),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def change_size_color(_), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
@doc false
def insight_urgency_class("critical"), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def insight_urgency_class("warning"), do: "text-wheat-600 dark:text-wheat-400"
def insight_urgency_class(_), do: "text-cerulean-600 dark:text-cerulean-400"
def insight_urgency_class("critical"), do: "text-red-600 dark:text-red-400"
def insight_urgency_class("warning"), do: "text-yellow-600 dark:text-yellow-400"
def insight_urgency_class(_), do: "text-blue-600 dark:text-blue-400"
@doc false
def insight_urgency_icon("critical"), do: "hero-exclamation-triangle"
@ -250,13 +249,13 @@ defmodule ToweropsWeb.SiteLive.Show do
def format_capacity(_), do: "-"
@doc false
def utilization_bar_color(pct) when pct >= 90, do: "bg-sweet-salmon-500"
def utilization_bar_color(pct) when pct >= 70, do: "bg-wheat-500"
def utilization_bar_color(pct) when pct >= 90, do: "bg-red-500"
def utilization_bar_color(pct) when pct >= 70, do: "bg-yellow-500"
def utilization_bar_color(_pct), do: "bg-green-500"
@doc false
def utilization_text_color(pct) when pct >= 90, do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def utilization_text_color(pct) when pct >= 70, do: "text-wheat-600 dark:text-wheat-400"
def utilization_text_color(pct) when pct >= 90, do: "text-red-600 dark:text-red-400"
def utilization_text_color(pct) when pct >= 70, do: "text-yellow-600 dark:text-yellow-400"
def utilization_text_color(_pct), do: "text-green-600 dark:text-green-400"
# Enqueue discovery job - safe to call in test environment

View file

@ -11,11 +11,11 @@
]} />
<!-- Prominent site header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">{@site.name}</h1>
<p :if={@site.location} class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@site.name}</h1>
<p :if={@site.location} class="mt-1 text-sm text-gray-600 dark:text-gray-400">
<.icon name="hero-map-pin" class="h-4 w-4 inline" /> {@site.location}
<%= if @site.latitude && @site.longitude do %>
<span class="ml-2 font-mono text-xs text-cool-steel-400">
<span class="ml-2 font-mono text-xs text-gray-400">
({Float.round(@site.latitude, 4)}, {Float.round(@site.longitude, 4)})
</span>
<% end %>
@ -32,7 +32,7 @@
data-lng={@site.longitude}
data-zoom="14"
data-marker-title={@site.name}
class="h-48 w-full rounded-lg border border-cool-steel-200 dark:border-white/10 mb-6 z-0"
class="h-48 w-full rounded-lg border border-gray-200 dark:border-white/10 mb-6 z-0"
>
</div>
<% end %>
@ -77,35 +77,35 @@
<%!-- Summary Stat Cards --%>
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
<%!-- Subscribers --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscribers")}
</div>
<p class="mt-1 text-2xl font-bold text-cool-steel-900 dark:text-white font-mono">
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
{format_number(@site_summary.subscribers || 0)}
</p>
</div>
<%!-- Financial data: role-gated --%>
<%= if @can_view_financials do %>
<%!-- MRR --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
<.icon name="hero-currency-dollar" class="h-4 w-4" /> {t("Monthly Revenue")}
</div>
<p class="mt-1 text-2xl font-bold text-cool-steel-900 dark:text-white">
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{format_mrr(@site_summary.mrr)}
</p>
</div>
<% end %>
<%!-- Device Count --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
<.icon name="hero-server" class="h-4 w-4" /> {t("Devices")}
</div>
<p class="mt-1 text-2xl font-bold text-cool-steel-900 dark:text-white font-mono">
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
{format_number(@site_summary.device_count)}
<%= if @site_summary.devices_down > 0 do %>
<span class="text-sm font-medium text-sweet-salmon-600 dark:text-sweet-salmon-400">
<span class="text-sm font-medium text-red-600 dark:text-red-400">
({@site_summary.devices_down} {t("down")})
</span>
<% end %>
@ -113,20 +113,20 @@
</div>
<%!-- Active Alerts --%>
<div class={[
"rounded-lg border bg-white dark:bg-cool-steel-800/50 p-4 transition-shadow hover:shadow-sm",
"rounded-lg border bg-white dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm",
if(length(@active_alerts) > 0,
do: "border-sweet-salmon-200 dark:border-sweet-salmon-900/50",
else: "border-cool-steel-200 dark:border-white/10"
do: "border-red-200 dark:border-red-900/50",
else: "border-gray-200 dark:border-white/10"
)
]}>
<div class="flex items-center gap-2 text-sm font-medium text-cool-steel-500 dark:text-cool-steel-400">
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
<.icon name="hero-bell-alert" class="h-4 w-4" /> {t("Active Alerts")}
</div>
<p class={[
"mt-1 text-2xl font-bold font-mono",
if(length(@active_alerts) > 0,
do: "text-sweet-salmon-600 dark:text-sweet-salmon-400",
else: "text-cool-steel-900 dark:text-white"
do: "text-red-600 dark:text-red-400",
else: "text-gray-900 dark:text-white"
)
]}>
{length(@active_alerts)}
@ -136,17 +136,17 @@
<%!-- Backhaul Capacity Card --%>
<%= if @capacity_summary.interfaces != [] do %>
<div class="mt-6 rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50 p-4">
<div class="mt-6 rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
<.icon name="hero-signal" class="h-4 w-4 inline mr-1" />{t("Backhaul Capacity")}
</h3>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
{format_capacity(@capacity_summary.total_capacity_bps)} {t("total")}
</span>
</div>
<div class="flex items-center gap-3 mb-3">
<div class="flex-1 bg-cool-steel-200 dark:bg-cool-steel-700 rounded-full h-3 overflow-hidden">
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
<div
class={[
"h-full rounded-full transition-all",
@ -165,14 +165,14 @@
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div>
<span class="text-cool-steel-500 dark:text-cool-steel-400">{t("Throughput")}</span>
<p class="font-mono font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-500 dark:text-gray-400">{t("Throughput")}</span>
<p class="font-mono font-medium text-gray-900 dark:text-white">
{format_capacity(@capacity_summary.total_throughput_bps)}
</p>
</div>
<div>
<span class="text-cool-steel-500 dark:text-cool-steel-400">{t("Headroom")}</span>
<p class="font-mono font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-500 dark:text-gray-400">{t("Headroom")}</span>
<p class="font-mono font-medium text-gray-900 dark:text-white">
{format_capacity(
max(
0,
@ -182,8 +182,8 @@
</p>
</div>
<div>
<span class="text-cool-steel-500 dark:text-cool-steel-400">{t("Interfaces")}</span>
<p class="font-mono font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-500 dark:text-gray-400">{t("Interfaces")}</span>
<p class="font-mono font-medium text-gray-900 dark:text-white">
{length(@capacity_summary.interfaces)}
</p>
</div>
@ -196,7 +196,7 @@
<div class={["mt-6 rounded-lg border p-4", qoe_bg(@qoe_summary[:score])]}>
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
{t("Preseem QoE Score")}
</h3>
<p class={["mt-1 text-3xl font-bold", qoe_color(@qoe_summary[:score])]}>
@ -205,17 +205,15 @@
</div>
<%= if @qoe_summary[:capacity_score] do %>
<div class="text-right">
<p class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Capacity
</p>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Capacity</p>
<div class="mt-1 w-32">
<div class="h-2 w-full rounded-full bg-cool-steel-200 dark:bg-cool-steel-700">
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div
class={["h-2 rounded-full", capacity_bar_color(@qoe_summary[:capacity_score])]}
style={"width: #{min(@qoe_summary[:capacity_score], 100)}%"}
/>
</div>
<p class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{@qoe_summary[:capacity_score]}%
</p>
</div>
@ -227,13 +225,13 @@
<%!-- Subscriber Impact Breakdown by Device --%>
<%= if @site_impact && @site_impact.subscriber_count > 0 do %>
<div class="mt-6 rounded-lg border border-cerulean-200 bg-cerulean-50 dark:border-cerulean-800/50 dark:bg-cerulean-950/30">
<div class="px-4 py-3 border-b border-cerulean-200 dark:border-cerulean-800/50">
<div class="mt-6 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800/50 dark:bg-indigo-950/30">
<div class="px-4 py-3 border-b border-indigo-200 dark:border-indigo-800/50">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-cerulean-900 dark:text-cerulean-200 flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200 flex items-center gap-1.5">
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscriber Impact")}
</h3>
<div class="flex items-center gap-4 text-sm font-medium text-cerulean-900 dark:text-cerulean-200">
<div class="flex items-center gap-4 text-sm font-medium text-indigo-900 dark:text-indigo-200">
<span>{format_number(@site_impact.subscriber_count)} subscribers</span>
<%= if @can_view_financials && @site_impact.mrr do %>
<span>{format_mrr(@site_impact.mrr)}/mo</span>
@ -243,9 +241,9 @@
</div>
<%= if @site_impact.devices != [] do %>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-cerulean-200 dark:divide-cerulean-800/50">
<table class="min-w-full divide-y divide-indigo-200 dark:divide-indigo-800/50">
<thead>
<tr class="text-xs text-cerulean-700 dark:text-cerulean-300">
<tr class="text-xs text-indigo-700 dark:text-indigo-300">
<th class="px-4 py-2 text-left font-medium">{t("Device")}</th>
<th class="px-4 py-2 text-right font-medium">{t("Subscribers")}</th>
<%= if @can_view_financials do %>
@ -253,17 +251,17 @@
<% end %>
</tr>
</thead>
<tbody class="divide-y divide-cerulean-100 dark:divide-cerulean-900/30">
<tbody class="divide-y divide-indigo-100 dark:divide-indigo-900/30">
<%= for dev <- @site_impact.devices do %>
<tr class="text-sm">
<td class="px-4 py-2 text-cerulean-900 dark:text-cerulean-200 font-medium">
<td class="px-4 py-2 text-indigo-900 dark:text-indigo-200 font-medium">
{dev.name || dev.device_id}
</td>
<td class="px-4 py-2 text-right text-cerulean-800 dark:text-cerulean-300 font-mono">
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
{dev.count}
</td>
<%= if @can_view_financials do %>
<td class="px-4 py-2 text-right text-cerulean-800 dark:text-cerulean-300 font-mono">
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
{format_mrr(dev.mrr)}
</td>
<% end %>
@ -280,17 +278,15 @@
<%!-- Left Column --%>
<div class="space-y-6 min-w-0">
<%!-- Site Details --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-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">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
{t("Site Details")}
</h3>
<dl class="mt-2 space-y-3">
<%= if @site.parent_site do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Parent Site
</dt>
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Parent Site</dt>
<dd class="mt-0.5">
<.link
navigate={~p"/sites/#{@site.parent_site.id}"}
@ -303,28 +299,20 @@
<% end %>
<%= if @site.location do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Location
</dt>
<dd class="mt-0.5 text-sm text-cool-steel-900 dark:text-white">
{@site.location}
</dd>
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Location</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{@site.location}</dd>
</div>
<% end %>
<%= if @site.description do %>
<div>
<dt class="text-sm font-medium text-cool-steel-600 dark:text-cool-steel-400">
Description
</dt>
<dd class="mt-0.5 text-sm text-cool-steel-900 dark:text-white">
{@site.description}
</dd>
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Description</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{@site.description}</dd>
</div>
<% end %>
</dl>
<%= if @site.child_sites != [] do %>
<div class="mt-4 border-t border-cool-steel-300 dark:border-cool-steel-600 pt-3">
<h4 class="text-sm font-semibold text-cool-steel-900 dark:text-white">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Child Sites")}
</h4>
<ul class="mt-2 space-y-1">
@ -343,18 +331,18 @@
</div>
<%!-- Active Alerts Panel --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-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">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-bell-alert" class="h-5 w-5" /> Active Alerts
<%= if @active_alerts != [] do %>
<span class="ml-2 inline-flex items-center rounded-full bg-sweet-salmon-100 px-2 py-0.5 text-xs font-medium text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400">
<span class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
{length(@active_alerts)}
</span>
<% end %>
</h3>
<%= if @active_alerts == [] do %>
<div class="mt-2 text-center py-6 text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-2 text-center py-6 text-gray-500 dark:text-gray-400">
<.icon name="hero-check-circle" class="mx-auto h-8 w-8 text-green-500" />
<p class="mt-2 text-sm">No active alerts</p>
</div>
@ -362,7 +350,7 @@
<div class="mt-2 space-y-2">
<div
:for={alert <- @active_alerts}
class="flex items-start gap-3 rounded-lg border border-cool-steel-200 dark:border-white/10 p-3"
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
>
<.icon
name={insight_urgency_icon(alert_severity(alert.alert_type))}
@ -372,10 +360,8 @@
]}
/>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-cool-steel-900 dark:text-white">
{alert.message}
</p>
<p class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-sm font-medium text-gray-900 dark:text-white">{alert.message}</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{time_ago(alert.triggered_at)}
</p>
</div>
@ -386,26 +372,26 @@
</div>
<%!-- Recent Config Changes Panel — hidden, feature temporarily disabled --%>
<div class="hidden rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50">
<div class="hidden rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
<div class="p-4">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-document-text" class="h-5 w-5" /> Recent Config Changes
</h3>
<%= if @config_changes == [] do %>
<div class="mt-2 text-center py-6 text-cool-steel-500 dark:text-cool-steel-400">
<div class="mt-2 text-center py-6 text-gray-500 dark:text-gray-400">
<p class="text-sm">No config changes in the last 30 days</p>
</div>
<% else %>
<div class="mt-2 space-y-2">
<div
:for={change <- @config_changes}
class="flex items-center justify-between rounded-lg border border-cool-steel-200 dark:border-white/10 p-3"
class="flex items-center justify-between rounded-lg border border-gray-200 dark:border-white/10 p-3"
>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-cool-steel-900 dark:text-white truncate">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
{change.device_name || "Unknown device"}
</p>
<p class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{time_ago(change.detected_at)}
</p>
</div>
@ -425,10 +411,10 @@
<%!-- Right Column --%>
<div class="space-y-6 min-w-0">
<%!-- Device Health Grid --%>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-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="flex items-center justify-between">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-server" class="h-5 w-5" /> Device Health
</h3>
<.button
@ -440,15 +426,15 @@
</.button>
</div>
<%= if @device == [] do %>
<div class="mt-2 rounded-lg border-2 border-dashed border-cool-steel-300 dark:border-cool-steel-600 p-8 text-center">
<div class="mt-2 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 p-8 text-center">
<.icon
name="hero-server"
class="mx-auto h-12 w-12 text-cool-steel-400 dark:text-cool-steel-500"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h4 class="mt-4 text-base font-semibold text-cool-steel-900 dark:text-white">
<h4 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">
{t("Add your first device")}
</h4>
<p class="mt-2 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Start monitoring by adding network devices to this site.")}
</p>
<div class="mt-4">
@ -459,9 +445,9 @@
</div>
<% else %>
<div class="mt-2 overflow-x-auto">
<table class="min-w-full divide-y divide-cool-steel-200 dark:divide-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr class="text-xs text-cool-steel-500 dark:text-cool-steel-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="hidden sm:table-cell px-3 py-2 text-left font-medium">
{t("IP Address")}
@ -477,10 +463,10 @@
<th class="px-3 py-2 text-right font-medium">{t("Latency")}</th>
</tr>
</thead>
<tbody class="divide-y divide-cool-steel-100 dark:divide-white/5">
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
<tr
:for={eq <- @device}
class="hover:bg-cool-steel-50 dark:hover:bg-cool-steel-800 transition-colors cursor-pointer"
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
phx-click={JS.navigate(~p"/devices/#{eq.id}")}
>
<td class="px-3 py-2">
@ -489,15 +475,15 @@
"h-2 w-2 shrink-0 rounded-full",
status_dot_class(eq.status)
]} />
<span class="text-sm font-medium text-cool-steel-900 dark:text-white truncate">
<span class="text-sm font-medium text-gray-900 dark:text-white truncate">
{eq.name}
</span>
</div>
</td>
<td class="hidden sm:table-cell px-3 py-2 font-mono text-xs text-cool-steel-500 dark:text-cool-steel-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}
</td>
<td class="hidden sm:table-cell px-3 py-2 text-right font-mono text-xs text-cool-steel-700 dark:text-cool-steel-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.count > 0, do: di.count, else: "—"}
<% else %>
@ -505,7 +491,7 @@
<% end %>
</td>
<%= if @can_view_financials do %>
<td class="hidden md:table-cell px-3 py-2 text-right font-mono text-xs text-cool-steel-700 dark:text-cool-steel-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 Decimal.gt?(di.mrr, 0), do: format_mrr(di.mrr), else: "—"}
<% else %>
@ -516,10 +502,10 @@
<td class={[
"px-3 py-2 text-right font-mono text-xs",
case @response_times[eq.id] do
nil -> "text-cool-steel-400"
nil -> "text-gray-400"
ms when ms < 50 -> "text-green-600 dark:text-green-400"
ms when ms < 200 -> "text-wheat-600 dark:text-wheat-400"
_ -> "text-sweet-salmon-600 dark:text-sweet-salmon-400"
ms when ms < 200 -> "text-yellow-600 dark:text-yellow-400"
_ -> "text-red-600 dark:text-red-400"
end
]}>
{format_response_time(@response_times[eq.id])}
@ -534,26 +520,24 @@
<%!-- Insights Panel --%>
<%= if @insights != [] do %>
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-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">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-light-bulb" class="h-5 w-5" /> Preseem Insights
</h3>
<div class="mt-2 space-y-2">
<div
:for={insight <- @insights}
class="flex items-start gap-3 rounded-lg border border-cool-steel-200 dark:border-white/10 p-3"
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
>
<.icon
name={insight_urgency_icon(insight.urgency)}
class={["h-5 w-5 mt-0.5 shrink-0", insight_urgency_class(insight.urgency)]}
/>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-cool-steel-900 dark:text-white">
{insight.title}
</p>
<p class="text-sm font-medium text-gray-900 dark:text-white">{insight.title}</p>
<%= if insight.description do %>
<p class="mt-0.5 text-xs text-cool-steel-500 dark:text-cool-steel-400 line-clamp-2">
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
{insight.description}
</p>
<% end %>
@ -569,9 +553,9 @@
<%!-- Latency Chart --%>
<%= if @latency_chart_data do %>
<div class="mt-6">
<div class="rounded-lg border border-cool-steel-200 bg-white dark:border-white/10 dark:bg-cool-steel-800/50">
<div class="px-4 py-3 border-b border-cool-steel-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white flex items-center gap-1.5">
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-chart-bar" class="h-4 w-4" /> {t("Site Latency — Last 24 Hours")}
</h3>
</div>
@ -588,9 +572,9 @@
>
<div class="absolute inset-0 flex items-center justify-center" data-loading>
<div class="flex flex-col items-center gap-2">
<div class="h-8 w-8 animate-spin rounded-full border-2 border-cool-steel-200 dark:border-cool-steel-700 border-t-cerulean-500">
<div class="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 dark:border-gray-700 border-t-blue-500">
</div>
<span class="text-xs text-cool-steel-400">{t("Loading chart...")}</span>
<span class="text-xs text-gray-400">{t("Loading chart...")}</span>
</div>
</div>
<canvas></canvas>

View file

@ -105,11 +105,11 @@ defmodule ToweropsWeb.StatusPageLive do
@doc false
def overall_color(:operational), do: "bg-green-500"
def overall_color(:degraded), do: "bg-wheat-500"
def overall_color(:degraded), do: "bg-yellow-500"
def overall_color(:partial_outage), do: "bg-orange-500"
def overall_color(:major_outage), do: "bg-sweet-salmon-500"
def overall_color(:maintenance), do: "bg-cerulean-500"
def overall_color(_), do: "bg-cool-steel-500"
def overall_color(:major_outage), do: "bg-red-500"
def overall_color(:maintenance), do: "bg-blue-500"
def overall_color(_), do: "bg-gray-500"
@doc false
def overall_text(:operational), do: "All Systems Operational"
@ -121,11 +121,11 @@ defmodule ToweropsWeb.StatusPageLive do
@doc false
def component_color("operational"), do: "bg-green-500"
def component_color("degraded"), do: "bg-wheat-500"
def component_color("degraded"), do: "bg-yellow-500"
def component_color("partial_outage"), do: "bg-orange-500"
def component_color("major_outage"), do: "bg-sweet-salmon-500"
def component_color("maintenance"), do: "bg-cerulean-500"
def component_color(_), do: "bg-cool-steel-400"
def component_color("major_outage"), do: "bg-red-500"
def component_color("maintenance"), do: "bg-blue-500"
def component_color(_), do: "bg-gray-400"
@doc false
def component_label("operational"), do: "Operational"
@ -136,9 +136,9 @@ defmodule ToweropsWeb.StatusPageLive do
def component_label(s), do: s
@doc false
def severity_color("critical"), do: "text-sweet-salmon-600"
def severity_color("critical"), do: "text-red-600"
def severity_color("major"), do: "text-orange-600"
def severity_color(_), do: "text-wheat-600"
def severity_color(_), do: "text-yellow-600"
@doc false
def incident_status_label("investigating"), do: "Investigating"

View file

@ -5,10 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Status Page Not Found</title>
</head>
<body class="bg-cool-steel-50 flex items-center justify-center min-h-screen">
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
<div class="text-center">
<h1 class="text-2xl font-bold text-cool-steel-900">Status Page Not Found</h1>
<p class="mt-2 text-cool-steel-600">
<h1 class="text-2xl font-bold text-gray-900">Status Page Not Found</h1>
<p class="mt-2 text-gray-600">
The requested status page does not exist or is not enabled.
</p>
</div>
@ -29,14 +29,14 @@
</style>
<% end %>
</head>
<body class="bg-cool-steel-50 dark:bg-cool-steel-950 min-h-screen">
<body class="bg-gray-50 dark:bg-gray-950 min-h-screen">
<div class="max-w-3xl mx-auto px-4 py-8">
<%!-- Header --%>
<header class="text-center mb-8">
<%= if @config.logo_url do %>
<img src={@config.logo_url} alt={@config.company_name} class="h-10 mx-auto mb-4" />
<% end %>
<h1 class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
{@config.company_name || "System Status"}
</h1>
</header>
@ -52,28 +52,28 @@
<%!-- Active Incidents --%>
<%= if @active_incidents != [] do %>
<div class="mb-8">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Active Incidents
</h2>
<div class="space-y-4">
<%= for incident <- @active_incidents do %>
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-cool-steel-700 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div class="flex items-start justify-between">
<div>
<h3 class={"font-semibold #{severity_color(incident.severity)}"}>
{incident.title}
</h3>
<%= if incident.body do %>
<p class="mt-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{incident.body}
</p>
<% end %>
</div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-wheat-100 text-wheat-800 dark:bg-wheat-900/50 dark:text-wheat-300">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300">
{incident_status_label(incident.status)}
</span>
</div>
<p class="mt-2 text-xs text-cool-steel-500">
<p class="mt-2 text-xs text-gray-500">
Started: {Calendar.strftime(incident.started_at, "%b %d, %H:%M UTC")}
</p>
</div>
@ -84,23 +84,21 @@
<%!-- Components --%>
<div class="mb-8">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
Components
</h2>
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-cool-steel-700 divide-y divide-cool-steel-200 dark:divide-cool-steel-700">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Components</h2>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700">
<%= if @components == [] do %>
<div class="p-4 text-center text-cool-steel-500 dark:text-cool-steel-400">
<div class="p-4 text-center text-gray-500 dark:text-gray-400">
No components configured
</div>
<% else %>
<%= for component <- @components do %>
<div class="px-4 py-3 flex items-center justify-between">
<div>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{component.name}
</span>
<%= if component.description do %>
<p class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<p class="text-xs text-gray-500 dark:text-gray-400">
{component.description}
</p>
<% end %>
@ -108,7 +106,7 @@
<div class="flex items-center gap-2">
<div class={"w-2.5 h-2.5 rounded-full #{component_color(component.status)}"}>
</div>
<span class="text-sm text-cool-steel-600 dark:text-cool-steel-400">
<span class="text-sm text-gray-600 dark:text-gray-400">
{component_label(component.status)}
</span>
</div>
@ -120,26 +118,26 @@
<%!-- Recent Incidents --%>
<div class="mb-8">
<h2 class="text-lg font-semibold text-cool-steel-900 dark:text-white mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Recent Incidents
</h2>
<%= if @recent_incidents == [] do %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-cool-steel-700 p-4 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 text-center">
No recent incidents — looking good!
</p>
<% else %>
<div class="space-y-3">
<%= for incident <- @recent_incidents do %>
<div class="bg-white dark:bg-cool-steel-800 rounded-lg border border-cool-steel-200 dark:border-cool-steel-700 p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-cool-steel-900 dark:text-white">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{incident.title}
</h3>
<span class={"text-xs px-2 py-0.5 rounded font-medium " <> if(incident.status == "resolved", do: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300", else: "bg-wheat-100 text-wheat-700 dark:bg-wheat-900/50 dark:text-wheat-300")}>
<span class={"text-xs px-2 py-0.5 rounded font-medium " <> if(incident.status == "resolved", do: "bg-green-100 text-green-700 dark:bg-green-900/50 dark:text-green-300", else: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300")}>
{incident_status_label(incident.status)}
</span>
</div>
<p class="mt-1 text-xs text-cool-steel-500">
<p class="mt-1 text-xs text-gray-500">
{Calendar.strftime(incident.started_at, "%b %d, %Y")}
<%= if incident.resolved_at do %>
— Resolved {Calendar.strftime(incident.resolved_at, "%b %d, %H:%M UTC")}
@ -152,13 +150,13 @@
</div>
<%!-- Footer --%>
<footer class="text-center text-xs text-cool-steel-400 dark:text-cool-steel-600 mt-12">
<footer class="text-center text-xs text-gray-400 dark:text-gray-600 mt-12">
<%= if @config.support_email do %>
<p>
Need help? Contact
<a
href={"mailto:#{@config.support_email}"}
class="underline hover:text-cool-steel-600 dark:hover:text-cool-steel-400"
class="underline hover:text-gray-600 dark:hover:text-gray-400"
>
{@config.support_email}
</a>

View file

@ -54,8 +54,8 @@ defmodule ToweropsWeb.TraceLive.Index do
color =
case assigns.quality do
:good -> "border-green-200 dark:border-green-800"
:warning -> "border-wheat-200 dark:border-wheat-800"
:bad -> "border-sweet-salmon-200 dark:border-sweet-salmon-800"
:warning -> "border-yellow-200 dark:border-yellow-800"
:bad -> "border-red-200 dark:border-red-800"
_ -> "border-cool-steel-200 dark:border-cool-steel-700"
end
@ -140,12 +140,9 @@ defmodule ToweropsWeb.TraceLive.Index do
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
def type_badge_class(:inventory_item), do: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
def type_badge_class(:site), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def type_badge_class(:site), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def type_badge_class(:device), do: "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-400"
def type_badge_class(:access_point),
do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-400"
def type_badge_class(:access_point), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
def type_badge_class(_), do: "text-cool-steel-700 dark:text-cool-steel-300"
def type_badge_label(:account), do: "Account"
@ -157,7 +154,7 @@ defmodule ToweropsWeb.TraceLive.Index do
@doc false
def status_badge_class("active"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
def status_badge_class("suspended"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-400"
def status_badge_class("suspended"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
def status_badge_class("cancelled"),
do: "bg-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-400"
@ -166,8 +163,8 @@ defmodule ToweropsWeb.TraceLive.Index do
@doc false
def device_status_color(:up), do: "text-green-600 dark:text-green-400"
def device_status_color(:down), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def device_status_color(_), do: "text-cool-steel-500 dark:text-cool-steel-400"
def device_status_color(:down), do: "text-red-600 dark:text-red-400"
def device_status_color(_), do: "text-gray-500 dark:text-gray-400"
@doc false
def format_speed(nil), do: ""
@ -221,45 +218,45 @@ defmodule ToweropsWeb.TraceLive.Index do
def score_quality(_), do: :bad
@doc false
def score_color(nil), do: "text-cool-steel-400"
def score_color(nil), do: "text-gray-400"
def score_color(s) when s >= 80, do: "text-green-600 dark:text-green-400"
def score_color(s) when s >= 50, do: "text-wheat-600 dark:text-wheat-400"
def score_color(_), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def score_color(s) when s >= 50, do: "text-yellow-600 dark:text-yellow-400"
def score_color(_), do: "text-red-600 dark:text-red-400"
@doc false
def latency_color(nil), do: "text-cool-steel-400"
def latency_color(nil), do: "text-gray-400"
def latency_color(v) when v <= 30, do: "text-green-600 dark:text-green-400"
def latency_color(v) when v <= 80, do: "text-wheat-600 dark:text-wheat-400"
def latency_color(_), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def latency_color(v) when v <= 80, do: "text-yellow-600 dark:text-yellow-400"
def latency_color(_), do: "text-red-600 dark:text-red-400"
@doc false
def jitter_color(nil), do: "text-cool-steel-400"
def jitter_color(nil), do: "text-gray-400"
def jitter_color(v) when v <= 10, do: "text-green-600 dark:text-green-400"
def jitter_color(v) when v <= 30, do: "text-wheat-600 dark:text-wheat-400"
def jitter_color(_), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def jitter_color(v) when v <= 30, do: "text-yellow-600 dark:text-yellow-400"
def jitter_color(_), do: "text-red-600 dark:text-red-400"
@doc false
def loss_color(nil), do: "text-cool-steel-400"
def loss_color(nil), do: "text-gray-400"
def loss_color(v) when v <= 1, do: "text-green-600 dark:text-green-400"
def loss_color(v) when v <= 5, do: "text-wheat-600 dark:text-wheat-400"
def loss_color(_), do: "text-sweet-salmon-600 dark:text-sweet-salmon-400"
def loss_color(v) when v <= 5, do: "text-yellow-600 dark:text-yellow-400"
def loss_color(_), do: "text-red-600 dark:text-red-400"
@doc false
def device_status_dot(:up), do: "bg-green-500"
def device_status_dot(:down), do: "bg-sweet-salmon-500"
def device_status_dot(_), do: "bg-cool-steel-400"
def device_status_dot(:down), do: "bg-red-500"
def device_status_dot(_), do: "bg-gray-400"
@doc false
def alert_dot(%{severity: "critical"}), do: "bg-sweet-salmon-500"
def alert_dot(%{severity: "warning"}), do: "bg-wheat-500"
def alert_dot(%{alert_type: "down"}), do: "bg-sweet-salmon-500"
def alert_dot(%{severity: "critical"}), do: "bg-red-500"
def alert_dot(%{severity: "warning"}), do: "bg-yellow-500"
def alert_dot(%{alert_type: "down"}), do: "bg-red-500"
def alert_dot(_), do: "bg-orange-400"
@doc false
def urgency_dot("critical"), do: "bg-sweet-salmon-500"
def urgency_dot("critical"), do: "bg-red-500"
def urgency_dot("high"), do: "bg-orange-500"
def urgency_dot("medium"), do: "bg-wheat-500"
def urgency_dot(_), do: "bg-cerulean-400"
def urgency_dot("medium"), do: "bg-yellow-500"
def urgency_dot(_), do: "bg-blue-400"
@doc false
def airtime_quality(nil), do: :unknown

View file

@ -13,18 +13,18 @@
end
} />
<div class="mb-1">
<h1 class="text-xl font-bold text-cool-steel-900 dark:text-white">{t("Trace")}</h1>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<h1 class="text-xl font-bold text-gray-900 dark:text-white">{t("Trace")}</h1>
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Investigate subscriber impact, site health, and network path issues.")}
</p>
</div>
<div class="space-y-6">
<%!-- Search Bar --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<div class="flex items-center gap-3">
<.icon name="hero-magnifying-glass" class="h-6 w-6 text-cool-steel-400 flex-shrink-0" />
<.icon name="hero-magnifying-glass" class="h-6 w-6 text-gray-400 flex-shrink-0" />
<form phx-change="search" phx-submit="search" class="flex-1 flex gap-2">
<input
type="text"
@ -35,7 +35,7 @@
"Search by customer name, IP address, account ID, serial number, site name, or device name..."
)
}
class="w-full rounded-field border border-cool-steel-300 bg-cool-steel-50 px-3 py-2 text-sm text-cool-steel-900 placeholder:text-cool-steel-400 focus:border-cerulean-500 focus:ring-1 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-900 dark:text-cool-steel-100 dark:placeholder:text-cool-steel-500 w-full dark:bg-cool-steel-800 dark:border-white/10 dark:text-white"
class="w-full rounded-field border border-cool-steel-300 bg-cool-steel-50 px-3 py-2 text-sm text-cool-steel-900 placeholder:text-cool-steel-400 focus:border-cerulean-500 focus:ring-1 focus:ring-cerulean-500 dark:border-cool-steel-600 dark:bg-cool-steel-900 dark:text-cool-steel-100 dark:placeholder:text-cool-steel-500 w-full dark:bg-gray-800 dark:border-white/10 dark:text-white"
phx-debounce="300"
autofocus
/>
@ -54,7 +54,7 @@
<%!-- Search Results --%>
<div
:if={@results != [] && !@trace}
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900"
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
>
<div class="p-0">
<div class="divide-y divide-cool-steel-200 dark:divide-cool-steel-700 dark:divide-white/10">
@ -72,17 +72,12 @@
{type_badge_label(result.type)}
</span>
<div class="flex-1 min-w-0">
<div class="font-medium text-cool-steel-900 dark:text-white truncate">
{result.label}
</div>
<div
:if={result.sublabel}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 truncate"
>
<div class="font-medium text-gray-900 dark:text-white truncate">{result.label}</div>
<div :if={result.sublabel} class="text-sm text-gray-500 dark:text-gray-400 truncate">
{result.sublabel}
</div>
</div>
<.icon name="hero-chevron-right" class="h-4 w-4 text-cool-steel-400 flex-shrink-0" />
<.icon name="hero-chevron-right" class="h-4 w-4 text-gray-400 flex-shrink-0" />
</button>
</div>
</div>
@ -91,7 +86,7 @@
<%!-- No Results --%>
<div
:if={@query != "" && @results == [] && !@trace}
class="text-center py-12 text-cool-steel-500 dark:text-cool-steel-400"
class="text-center py-12 text-gray-500 dark:text-gray-400"
>
<.icon name="hero-magnifying-glass" class="h-12 w-12 mx-auto mb-3 opacity-50" />
<p class="text-lg font-medium">No results found</p>
@ -103,29 +98,23 @@
<%!-- Site Trace View --%>
<div :if={@trace && @trace_type == :site} class="space-y-4">
<%!-- Site Info --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="text-lg font-semibold text-cool-steel-900 dark:text-white flex items-center gap-2">
<.icon name="hero-building-office-2" class="h-5 w-5 text-cool-steel-400" />
<h3 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<.icon name="hero-building-office-2" class="h-5 w-5 text-gray-400" />
{@trace.site.name}
</h3>
<div class="mt-2 space-y-1 text-sm text-cool-steel-600 dark:text-cool-steel-400">
<div class="mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
<p :if={@trace.site.location}>
<span class="font-medium text-cool-steel-500 dark:text-cool-steel-400">
Location:
</span>
<span class="font-medium text-gray-500 dark:text-gray-400">Location:</span>
{@trace.site.location}
</p>
<p :if={@trace.site.address}>
<span class="font-medium text-cool-steel-500 dark:text-cool-steel-400">
Address:
</span>
<span class="font-medium text-gray-500 dark:text-gray-400">Address:</span>
{@trace.site.address}
</p>
<p>
<span class="font-medium text-cool-steel-500 dark:text-cool-steel-400">
Devices:
</span>
<span class="font-medium text-gray-500 dark:text-gray-400">Devices:</span>
{length(@trace.devices)}
</p>
</div>
@ -134,16 +123,16 @@
<%!-- Devices at Site --%>
<div
:if={length(@trace.devices) > 0}
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900"
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
>
<div class="p-4 sm:p-6">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white mb-3">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
{t("Devices")}
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="text-xs text-cool-steel-500 dark:text-cool-steel-400 uppercase">
<tr class="text-xs text-gray-500 dark:text-gray-400 uppercase">
<th>{t("Name")}</th>
<th>{t("IP Address")}</th>
<th>{t("Status")}</th>
@ -157,7 +146,7 @@
<td>
<.link
navigate={~p"/devices/#{device.id}"}
class="font-medium text-cerulean-600 dark:text-cerulean-400 hover:underline"
class="font-medium text-blue-600 dark:text-blue-400 hover:underline"
>
{device.name || device.ip_address}
</.link>
@ -181,10 +170,10 @@
<%!-- Recent Alerts --%>
<div
:if={@trace.alerts != []}
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900"
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
>
<div class="p-4 sm:p-6">
<h3 class="text-sm font-semibold text-cool-steel-900 dark:text-white mb-3">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-3">
{t("Recent Alerts")}
</h3>
<div class="space-y-2">
@ -196,13 +185,10 @@
</span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{format_alert_type(alert.alert_type)}
</span>
<span
:if={alert.device}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400"
>
<span :if={alert.device} class="text-xs text-gray-500 dark:text-gray-400">
{alert.device.name}
</span>
<span
@ -212,10 +198,10 @@
{t("Resolved")}
</span>
</div>
<p class="text-sm text-cool-steel-600 dark:text-cool-steel-400 truncate">
<p class="text-sm text-gray-600 dark:text-gray-400 truncate">
{alert.message}
</p>
<span class="text-xs text-cool-steel-400 dark:text-cool-steel-500">
<span class="text-xs text-gray-400 dark:text-gray-500">
{format_relative_time(alert.triggered_at)}
</span>
</div>
@ -233,20 +219,17 @@
<%!-- Subscriber + Connection --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<%!-- Subscriber Info --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-user" class="h-4 w-4 inline" /> Subscriber
</h3>
<%= if @trace.subscriber do %>
<div class="space-y-2">
<div class="text-xl font-bold text-cool-steel-900 dark:text-white">
<div class="text-xl font-bold text-gray-900 dark:text-white">
{@trace.subscriber.name}
</div>
<div
:if={@trace.subscriber.plan}
class="text-sm text-cool-steel-600 dark:text-cool-steel-300"
>
<div :if={@trace.subscriber.plan} class="text-sm text-gray-600 dark:text-gray-300">
{String.capitalize(@trace.subscriber.plan)}
<%!-- Financial data: role-gated --%>
<%= if @can_view_financials do %>
@ -260,24 +243,24 @@
</div>
<div
:if={@trace.subscriber.speed_download}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400"
class="text-sm text-gray-500 dark:text-gray-400"
>
↓ {format_speed(@trace.subscriber.speed_download)} / ↑ {format_speed(
@trace.subscriber.speed_upload
)}
</div>
<div class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="text-sm text-gray-500 dark:text-gray-400">
Account #: {@trace.subscriber.account_id}
</div>
<div
:if={@trace.subscriber.address}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400"
class="text-sm text-gray-500 dark:text-gray-400"
>
<.icon name="hero-map-pin" class="h-3.5 w-3.5 inline" /> {@trace.subscriber.address}
</div>
<div
:if={@trace.subscriber.customer_since}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400"
class="text-sm text-gray-500 dark:text-gray-400"
>
Customer since: {Calendar.strftime(@trace.subscriber.customer_since, "%b %Y")}
</div>
@ -292,7 +275,7 @@
<%= if @can_view_financials do %>
<span
:if={@trace.subscriber.mrr}
class="text-sm font-medium text-cool-steel-700 dark:text-cool-steel-300"
class="text-sm font-medium text-gray-700 dark:text-gray-300"
>
MRR: ${if(is_struct(@trace.subscriber.mrr, Decimal),
do: Decimal.round(@trace.subscriber.mrr, 2),
@ -303,7 +286,7 @@
</div>
</div>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
{t("No subscriber info available")}
</p>
<% end %>
@ -311,36 +294,31 @@
</div>
<%!-- Connection / Device Info --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-signal" class="h-4 w-4 inline" /> Connection
</h3>
<%= if @trace.device do %>
<div class="space-y-2">
<div class="text-xl font-bold text-cool-steel-900 dark:text-white">
<div class="text-xl font-bold text-gray-900 dark:text-white">
<.link
navigate={~p"/devices/#{@trace.device.id}"}
class="hover:text-cerulean-600 dark:hover:text-cerulean-400"
class="hover:text-blue-600 dark:hover:text-blue-400"
>
{@trace.device.name || @trace.device.ip_address}
</.link>
</div>
<div
:if={@trace.device.site}
class="text-sm text-cool-steel-600 dark:text-cool-steel-300"
>
<div :if={@trace.device.site} class="text-sm text-gray-600 dark:text-gray-300">
<.link
navigate={~p"/sites/#{@trace.device.site.id}"}
class="hover:text-cerulean-600"
class="hover:text-blue-600"
>
{@trace.device.site.name}
</.link>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
Status:
</span>
<span class="text-sm text-gray-500 dark:text-gray-400">Status:</span>
<span class={[
"inline-flex items-center gap-1 text-sm font-medium",
device_status_color(@trace.device.status)
@ -353,12 +331,12 @@
{String.capitalize(to_string(@trace.device.status))}
</span>
</div>
<div class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<div class="text-sm text-gray-500 dark:text-gray-400">
IP: {@trace.device.ip_address}
</div>
<div
:if={@trace.device.last_checked_at}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400"
class="text-sm text-gray-500 dark:text-gray-400"
>
Last checked: {format_relative_time(@trace.device.last_checked_at)}
</div>
@ -366,24 +344,22 @@
:if={@trace.access_point}
class="mt-2 pt-2 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10"
>
<div class="text-xs font-semibold uppercase tracking-wider text-cool-steel-400 mb-1">
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-1">
{t("Preseem AP")}
</div>
<div class="text-sm text-cool-steel-700 dark:text-cool-steel-300">
<div class="text-sm text-gray-700 dark:text-gray-300">
{@trace.access_point.name}
</div>
<div
:if={@trace.access_point.model}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400"
class="text-xs text-gray-500 dark:text-gray-400"
>
{@trace.access_point.model}
</div>
</div>
</div>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 italic">
No device linked
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">No device linked</p>
<% end %>
</div>
</div>
@ -392,9 +368,9 @@
<%!-- QoE Metrics + Peer Impact --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<%!-- QoE Metrics --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-chart-bar" class="h-4 w-4 inline" /> QoE Metrics
<span class="text-xs font-normal normal-case">(from Preseem)</span>
</h3>
@ -423,12 +399,12 @@
</div>
<%= if @trace.qoe_metrics.latest_metric do %>
<div class="mt-4 pt-3 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10">
<div class="text-xs font-semibold uppercase tracking-wider text-cool-steel-400 mb-2">
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
{t("Latest Metrics")}
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="flex justify-between">
<span class="text-cool-steel-500 dark:text-cool-steel-400">Latency</span>
<span class="text-gray-500 dark:text-gray-400">Latency</span>
<span class={[
"font-medium",
latency_color(@trace.qoe_metrics.latest_metric.avg_latency)
@ -437,7 +413,7 @@
</span>
</div>
<div class="flex justify-between">
<span class="text-cool-steel-500 dark:text-cool-steel-400">Jitter</span>
<span class="text-gray-500 dark:text-gray-400">Jitter</span>
<span class={[
"font-medium",
jitter_color(@trace.qoe_metrics.latest_metric.avg_jitter)
@ -446,7 +422,7 @@
</span>
</div>
<div class="flex justify-between">
<span class="text-cool-steel-500 dark:text-cool-steel-400">Loss</span>
<span class="text-gray-500 dark:text-gray-400">Loss</span>
<span class={[
"font-medium",
loss_color(@trace.qoe_metrics.latest_metric.avg_loss)
@ -455,8 +431,8 @@
</span>
</div>
<div class="flex justify-between">
<span class="text-cool-steel-500 dark:text-cool-steel-400">Throughput</span>
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="text-gray-500 dark:text-gray-400">Throughput</span>
<span class="font-medium text-gray-900 dark:text-white">
{format_throughput(@trace.qoe_metrics.latest_metric.avg_throughput)}
</span>
</div>
@ -464,7 +440,7 @@
</div>
<% end %>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
{t("No Preseem data available")}
</p>
<% end %>
@ -472,37 +448,37 @@
</div>
<%!-- Peer Impact --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-users" class="h-4 w-4 inline" /> Peer Impact
</h3>
<%= if @trace.peer_impact do %>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-sm text-gray-500 dark:text-gray-400">
{t("Subscribers on this AP")}
</span>
<span class="text-2xl font-bold text-cool-steel-900 dark:text-white">
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{@trace.peer_impact.subscriber_count}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-sm text-gray-500 dark:text-gray-400">
{t("Other APs on device")}
</span>
<span class="text-lg font-semibold text-cool-steel-700 dark:text-cool-steel-300">
<span class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{@trace.peer_impact.peer_ap_count}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-sm text-gray-500 dark:text-gray-400">
{t("Active alerts on device")}
</span>
<span class={[
"text-lg font-semibold",
if(@trace.peer_impact.active_alert_count > 0,
do: "text-sweet-salmon-600",
do: "text-red-600",
else: "text-green-600"
)
]}>
@ -513,7 +489,7 @@
:if={@trace.peer_impact.peer_aps != []}
class="mt-3 pt-3 border-t border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10"
>
<div class="text-xs font-semibold uppercase tracking-wider text-cool-steel-400 mb-2">
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
{t("Peer APs")}
</div>
<div class="space-y-1">
@ -521,11 +497,9 @@
:for={ap <- @trace.peer_impact.peer_aps}
class="flex items-center justify-between text-sm"
>
<span class="text-cool-steel-700 dark:text-cool-steel-300 truncate">
{ap.name}
</span>
<span class="text-gray-700 dark:text-gray-300 truncate">{ap.name}</span>
<div class="flex items-center gap-2">
<span class="text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-gray-500 dark:text-gray-400">
{ap.subscriber_count || 0} subs
</span>
<span
@ -540,7 +514,7 @@
</div>
</div>
<% else %>
<p class="text-sm text-cool-steel-500 dark:text-cool-steel-400 italic">
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
{t("No peer data available")}
</p>
<% end %>
@ -549,9 +523,9 @@
</div>
<%!-- Active Alerts --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-bell-alert" class="h-4 w-4 inline" /> Recent Alerts
</h3>
<%= if @trace.alerts == [] do %>
@ -571,17 +545,14 @@
]}>
</span>
<div class="flex-1 min-w-0">
<span class="text-sm font-medium text-cool-steel-900 dark:text-white">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{format_alert_type(alert.alert_type)}
</span>
<span
:if={alert.message}
class="text-sm text-cool-steel-500 dark:text-cool-steel-400 ml-1"
>
<span :if={alert.message} class="text-sm text-gray-500 dark:text-gray-400 ml-1">
— {alert.message}
</span>
</div>
<div class="text-xs text-cool-steel-500 dark:text-cool-steel-400 flex-shrink-0">
<div class="text-xs text-gray-500 dark:text-gray-400 flex-shrink-0">
{format_relative_time(alert.triggered_at)}
</div>
<span
@ -605,9 +576,9 @@
<%!-- Recent Activity: Config Changes + Insights --%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<%!-- Config Changes — hidden, feature temporarily disabled --%>
<div class="hidden card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="hidden card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-document-text" class="h-4 w-4 inline" /> Config Changes (24h)
</h3>
<%= if @trace.config_changes == [] do %>
@ -619,16 +590,16 @@
<div class="space-y-2">
<div :for={change <- @trace.config_changes} class="text-sm">
<div class="flex items-center justify-between">
<span class="font-medium text-cool-steel-900 dark:text-white">
<span class="font-medium text-gray-900 dark:text-white">
{Map.get(change, :changed_sections, []) |> Enum.join(", ")}
</span>
<span class="text-xs text-cool-steel-500 dark:text-cool-steel-400">
<span class="text-xs text-gray-500 dark:text-gray-400">
{format_relative_time(change.changed_at)}
</span>
</div>
<div
:if={Map.get(change, :diff_summary)}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400 mt-0.5 font-mono truncate"
class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 font-mono truncate"
>
{change.diff_summary}
</div>
@ -639,9 +610,9 @@
</div>
<%!-- Active Insights --%>
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900">
<div class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900">
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-light-bulb" class="h-4 w-4 inline" /> Active Insights
</h3>
<%= if @trace.insights == [] do %>
@ -658,12 +629,12 @@
]}>
</span>
<div class="min-w-0">
<div class="text-sm font-medium text-cool-steel-900 dark:text-white">
<div class="text-sm font-medium text-gray-900 dark:text-white">
{insight.title}
</div>
<div
:if={insight.description}
class="text-xs text-cool-steel-500 dark:text-cool-steel-400 line-clamp-2"
class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2"
>
{insight.description}
</div>
@ -678,16 +649,16 @@
<%!-- Inventory Items --%>
<div
:if={@trace.inventory_items != []}
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-cool-steel-900"
class="card shadow-sm border border-cool-steel-200 dark:border-cool-steel-700 dark:border-white/10 dark:bg-gray-900"
>
<div class="p-4 sm:p-6">
<h3 class="font-semibold text-sm uppercase tracking-wider text-cool-steel-500 dark:text-cool-steel-400 mb-3">
<h3 class="font-semibold text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-3">
<.icon name="hero-cube" class="h-4 w-4 inline" /> Equipment (from Gaiia)
</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead>
<tr class="text-xs uppercase text-cool-steel-500 dark:text-cool-steel-400">
<tr class="text-xs uppercase text-gray-500 dark:text-gray-400">
<th>Name</th>
<th>IP</th>
<th>Model</th>
@ -700,7 +671,7 @@
:for={item <- @trace.inventory_items}
class="hover:bg-cool-steel-100/50 dark:hover:bg-cool-steel-800/50"
>
<td class="font-medium text-cool-steel-900 dark:text-white">{item.name}</td>
<td class="font-medium text-gray-900 dark:text-white">{item.name}</td>
<td class="font-mono text-sm">{item.ip_address}</td>
<td>{item.model_name}</td>
<td class="font-mono text-xs">{item.serial_number}</td>
@ -724,12 +695,12 @@
<div :if={@query == "" && !@trace} class="text-center py-16">
<.icon
name="hero-magnifying-glass-circle"
class="h-16 w-16 mx-auto mb-4 text-cool-steel-300 dark:text-cool-steel-600"
class="h-16 w-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
/>
<h2 class="text-xl font-semibold text-cool-steel-900 dark:text-white mb-2">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{t("Subscriber Trace")}
</h2>
<p class="text-cool-steel-500 dark:text-cool-steel-400 max-w-md mx-auto">
<p class="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t(
"Search for a customer by name, IP address, account ID, site name, or device name to see their full network status at a glance."
)}

View file

@ -180,7 +180,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
Already registered?
<.link
navigate={~p"/users/log-in"}
class="font-semibold text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Log in
</.link>
@ -189,8 +189,8 @@ defmodule ToweropsWeb.UserRegistrationLive do
</.header>
<%= if @invitation do %>
<div class="mt-6 rounded-lg bg-cerulean-50 dark:bg-cerulean-900/20 p-4 border border-cerulean-200 dark:border-cerulean-800">
<p class="text-sm text-cerulean-900 dark:text-cerulean-100">
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4 border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-900 dark:text-blue-100">
You've been invited to join
<span class="font-semibold">{@invitation.organization.name}</span>
as
@ -198,11 +198,11 @@ defmodule ToweropsWeb.UserRegistrationLive do
</p>
</div>
<% else %>
<div class="mt-6 rounded-lg bg-cerulean-50 dark:bg-cerulean-900/20 p-4 border border-cerulean-200 dark:border-cerulean-800">
<p class="text-sm font-medium text-cerulean-900 dark:text-cerulean-100">
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4 border border-blue-200 dark:border-blue-800">
<p class="text-sm font-medium text-blue-900 dark:text-blue-100">
Free tier includes:
</p>
<ul class="mt-2 text-sm text-cerulean-800 dark:text-cerulean-200 space-y-1">
<ul class="mt-2 text-sm text-blue-800 dark:text-blue-200 space-y-1">
<li> Monitor up to 10 devices</li>
<li> Real-time alerts and notifications</li>
<li> Performance charts and historical data</li>
@ -250,16 +250,16 @@ defmodule ToweropsWeb.UserRegistrationLive do
/>
<%= if @password_breach_count && @password_breach_count > 0 do %>
<div class="mt-2 rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-4">
<div class="mt-2 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-wheat-400" />
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-wheat-800 dark:text-wheat-200">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Password found in data breaches
</h3>
<div class="mt-2 text-sm text-wheat-700 dark:text-wheat-300">
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
This password has appeared {@password_breach_count}
{if @password_breach_count == 1, do: "time", else: "times"} in known data breaches. We strongly recommend choosing a different,
@ -291,7 +291,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
type="checkbox"
checked={@privacy_policy_consent}
required
class="h-4 w-4 rounded border-zinc-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-cerulean-500 dark:checked:border-cerulean-500"
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
@ -303,12 +303,12 @@ defmodule ToweropsWeb.UserRegistrationLive do
<.link
href={~p"/privacy"}
target="_blank"
class="font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onclick="event.stopPropagation()"
>
Privacy Policy
</.link>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">*</span>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>
@ -321,7 +321,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
type="checkbox"
checked={@terms_of_service_consent}
required
class="h-4 w-4 rounded border-zinc-300 text-cerulean-600 focus:ring-cerulean-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-cerulean-500 dark:checked:border-cerulean-500"
class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
</div>
<div class="ml-3 text-sm">
@ -333,12 +333,12 @@ defmodule ToweropsWeb.UserRegistrationLive do
<.link
href={~p"/terms"}
target="_blank"
class="font-medium text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onclick="event.stopPropagation()"
>
Terms of Service
</.link>
<span class="text-sweet-salmon-600 dark:text-sweet-salmon-400">*</span>
<span class="text-red-600 dark:text-red-400">*</span>
</label>
</div>
</div>

View file

@ -123,16 +123,16 @@ defmodule ToweropsWeb.UserResetPasswordLive do
/>
<%= if @password_breach_count && @password_breach_count > 0 do %>
<div class="mt-2 rounded-md bg-wheat-50 dark:bg-wheat-900/20 p-4">
<div class="mt-2 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<div class="flex">
<div class="flex-shrink-0">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-wheat-400" />
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-wheat-800 dark:text-wheat-200">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Password found in data breaches
</h3>
<div class="mt-2 text-sm text-wheat-700 dark:text-wheat-300">
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
This password has appeared {@password_breach_count}
{if @password_breach_count == 1, do: "time", else: "times"} in known data breaches. We strongly recommend choosing a different,
@ -161,7 +161,7 @@ defmodule ToweropsWeb.UserResetPasswordLive do
<div class="mt-6 text-center">
<.link
navigate={~p"/users/log-in"}
class="text-sm font-semibold text-cerulean-600 hover:text-cerulean-700 dark:text-cerulean-400 dark:hover:text-cerulean-300"
class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Back to log in
</.link>

Some files were not shown because too many files have changed in this diff Show more