towerops/lib/towerops_web/live/account_live/my_data.ex
2026-01-30 16:47:11 -06:00

714 lines
30 KiB
Elixir

defmodule ToweropsWeb.AccountLive.MyData do
@moduledoc """
LiveView for displaying all user data (GDPR Right to Access)
"""
use ToweropsWeb, :live_view
alias Towerops.Accounts
alias Towerops.Accounts.Scope
alias Towerops.Admin
alias Towerops.Alerts
alias Towerops.Devices
alias Towerops.Organizations
@impl true
def handle_event("revoke_consent", %{"consent-id" => consent_id}, socket) do
case Accounts.revoke_consent(consent_id) do
{:ok, _consent} ->
user = socket.assigns.current_scope.user
socket =
socket
|> put_flash(:info, "Consent revoked successfully.")
|> assign(:user_data, Map.put(socket.assigns.user_data, :consents, get_user_consents(user)))
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to revoke consent.")}
end
end
@impl true
def mount(_params, _session, socket) do
user = socket.assigns.current_scope.user
# Get organizations and check ownership
organizations = get_user_organizations(user)
is_owner = Enum.any?(organizations, fn org -> org[:is_owner] end)
default_organization = organizations |> Enum.map(& &1[:organization]) |> List.first()
# Gather user data - only include org/devices/alerts if user is an owner
user_data = %{
profile: get_user_profile(user),
credentials: get_user_credentials(user),
consents: get_user_consents(user),
organizations: if(is_owner, do: organizations, else: []),
devices: if(is_owner, do: get_user_devices(user), else: []),
alerts: if(is_owner, do: get_user_alerts(user), else: []),
audit_logs: get_user_audit_logs(user)
}
# Update scope with organization
scope = Scope.put_organization(socket.assigns.current_scope, default_organization)
{:ok,
socket
|> assign(:current_scope, scope)
|> assign(:user_data, user_data)
|> assign(:page_title, "My Data")
|> assign(:is_owner, is_owner)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="settings"
>
<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-gray-500 dark:text-gray-400">
Manage your account email address, password, and security settings
</p>
</div>
<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-gray-500 sm:px-6 lg:px-8 dark:text-gray-400"
>
<li>
<.link
navigate={~p"/users/settings"}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Personal
</.link>
</li>
<li>
<.link
navigate={~p"/users/settings"}
phx-click={JS.push("switch_tab", value: %{tab: "account"})}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Account
</.link>
</li>
<li>
<.link
navigate={~p"/users/settings"}
phx-click={JS.push("switch_tab", value: %{tab: "security"})}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Security
</.link>
</li>
<li>
<.link
navigate={~p"/users/settings"}
phx-click={JS.push("switch_tab", value: %{tab: "sessions"})}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Sessions
</.link>
</li>
<li>
<.link
navigate={~p"/users/settings"}
phx-click={JS.push("switch_tab", value: %{tab: "api"})}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
API
</.link>
</li>
<li>
<.link
navigate={~p"/users/settings"}
phx-click={JS.push("switch_tab", value: %{tab: "notifications"})}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
Notifications
</.link>
</li>
<li>
<.link
navigate={~p"/users/my-data"}
class="text-indigo-600 dark:text-indigo-400"
>
My Data
</.link>
</li>
</ul>
</nav>
</header>
<div class="space-y-8 py-8">
<div>
<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-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-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-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@user_data.profile.name || "Not set"}
</dd>
</div>
<div>
<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-gray-500 dark:text-gray-400">Account Created</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{Calendar.strftime(@user_data.profile.inserted_at, "%B %d, %Y at %I:%M %p")}
</dd>
</div>
<div>
<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-gray-500 dark:text-gray-400">
({Calendar.strftime(@user_data.profile.confirmed_at, "%B %d, %Y")})
</span>
<% end %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Superuser</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{if @user_data.profile.is_superuser, do: "Yes", else: "No"}
</dd>
</div>
</dl>
</div>
</section>
<!-- WebAuthn Credentials -->
<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">
Security Credentials
</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
WebAuthn passkeys registered to your account
</p>
</div>
<div class="px-4 py-5 sm:p-6">
<%= if Enum.empty?(@user_data.credentials) do %>
<p class="text-sm text-gray-500 dark:text-gray-400">
No WebAuthn credentials registered.
</p>
<% else %>
<div class="space-y-4">
<%= for credential <- @user_data.credentials do %>
<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-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{credential.name}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Created</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{Calendar.strftime(credential.inserted_at, "%B %d, %Y")}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Last Used
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
<%= if credential.last_used_at do %>
{Calendar.strftime(credential.last_used_at, "%B %d, %Y at %I:%M %p")}
<% else %>
Never
<% end %>
</dd>
</div>
</dl>
</div>
<% end %>
</div>
<% end %>
</div>
</section>
<!-- Consent Records -->
<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-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-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-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-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-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-gray-500 dark:text-gray-400">
Granted
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{Calendar.strftime(consent.granted_at, "%B %d, %Y at %I:%M %p")}
</dd>
</div>
<div>
<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-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 {Calendar.strftime(consent.revoked_at, "%B %d, %Y")}
</span>
<% else %>
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
Active
</span>
<% end %>
</dd>
</div>
</dl>
<%= if !consent.revoked_at do %>
<div class="mt-4">
<button
type="button"
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-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Revoke Consent
</button>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
</section>
<%= if @is_owner do %>
<!-- Organizations -->
<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-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-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-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-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-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-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-gray-500 dark:text-gray-400">
Created
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{Calendar.strftime(org.inserted_at, "%B %d, %Y")}
</dd>
</div>
</dl>
</div>
<% end %>
</div>
<% end %>
</div>
</section>
<!-- Devices -->
<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-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-gray-500 dark:text-gray-400">No devices configured.</p>
<% else %>
<div class="overflow-x-auto">
<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-gray-500 dark:text-gray-400 uppercase tracking-wider">
Name
</th>
<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-gray-500 dark:text-gray-400 uppercase tracking-wider">
Organization
</th>
<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-gray-200 dark:divide-gray-700">
<%= for device <- @user_data.devices do %>
<tr>
<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-gray-900 dark:text-white">
{device.ip_address}
</td>
<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-gray-500 dark:text-gray-400">
{Calendar.strftime(device.inserted_at, "%b %d, %Y")}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
Total devices: {length(@user_data.devices)}
</p>
<% end %>
</div>
</section>
<!-- Alerts -->
<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-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-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-gray-200 dark:divide-gray-700">
<thead>
<tr>
<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-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
</th>
<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-gray-500 dark:text-gray-400 uppercase tracking-wider">
Created
</th>
</tr>
</thead>
<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-gray-900 dark:text-white">
{alert.device.name}
</td>
<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">
<span class={[
"inline-flex rounded-full px-2 text-xs font-semibold leading-5",
alert_status_class(alert)
]}>
{alert_status_text(alert)}
</span>
</td>
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
{Calendar.strftime(alert.inserted_at, "%b %d, %Y %I:%M %p")}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<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 %>
</div>
</section>
<% end %>
<!-- Audit Logs -->
<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-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-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-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-gray-900 dark:text-white">
{log.action}
</p>
<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-gray-500 dark:text-gray-500">
{Calendar.strftime(log.inserted_at, "%B %d, %Y at %I:%M %p")}
</p>
</div>
</div>
</div>
<% end %>
</div>
<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 %>
</div>
</section>
<!-- Export Data -->
<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-blue-600 dark:text-blue-400 flex-shrink-0"
/>
<div class="flex-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-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-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>
</div>
</div>
</section>
<!-- Data Processing Information -->
<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-gray-700 dark:text-gray-300">
<li>
<strong>Profile Information:</strong>
Used to identify your account and personalize your experience
</li>
<li>
<strong>Email Address:</strong>
Used for account notifications, password resets, and important service updates
</li>
<li>
<strong>Organizations:</strong> Determines which devices and alerts you can access
</li>
<li>
<strong>Device Data:</strong>
Stored to provide network monitoring and alerting services
</li>
<li>
<strong>Monitoring Data:</strong>
Time-series metrics used to track device health and performance
</li>
<li>
<strong>Audit Logs:</strong>
Security records of administrative actions for compliance and security purposes
</li>
</ul>
<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-blue-600 hover:text-blue-500 dark:text-blue-400"
>
Privacy Policy
</.link>.
</p>
</div>
</div>
</section>
</div>
</Layouts.authenticated>
"""
end
# Helper function to get user profile data
defp get_user_profile(user) do
%{
email: user.email,
name: user.name,
timezone: user.timezone,
inserted_at: user.inserted_at,
confirmed_at: user.confirmed_at,
is_superuser: user.is_superuser
}
end
# Helper function to get user's WebAuthn credentials
defp get_user_credentials(user) do
Accounts.list_user_credentials(user.id)
end
# Helper function to get user's organizations
defp get_user_organizations(user) do
user.id
|> Organizations.list_user_organizations()
|> Enum.map(fn org ->
# Organization has the user's membership preloaded (only their membership)
[membership] = org.memberships
%{
name: org.name,
slug: org.slug,
is_owner: membership.role == :owner,
inserted_at: org.inserted_at
}
end)
end
# Helper function to get devices from user's organizations
defp get_user_devices(user) do
org_ids =
user.id
|> Organizations.list_user_organizations()
|> Enum.map(& &1.id)
if Enum.empty?(org_ids) do
[]
else
org_ids
|> Devices.list_devices_for_organizations()
|> Towerops.Repo.preload(site: :organization)
end
end
# Helper function to get alerts for user's devices (last 90 days)
defp get_user_alerts(user) do
org_ids =
user.id
|> Organizations.list_user_organizations()
|> Enum.map(& &1.id)
if Enum.empty?(org_ids) do
[]
else
ninety_days_ago = DateTime.add(DateTime.utc_now(), -90, :day)
org_ids
|> Alerts.list_alerts_for_organizations(since: ninety_days_ago)
|> Towerops.Repo.preload(:device)
end
end
# Helper function to get audit logs related to the user
defp get_user_audit_logs(user) do
Admin.list_audit_logs_for_user(user.id)
end
# Helper function to get user's consent records
defp get_user_consents(user) do
Accounts.list_user_consents(user.id)
end
# Helper function for alert status badge styling
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-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
defp alert_status_text(alert) do
cond do
alert.resolved_at -> "Resolved"
alert.acknowledged_at -> "Acknowledged"
true -> "Active"
end
end
end