feat: dynamic favicon reflects real-time system health status
Favicon changes to green/yellow/red based on device and alert status. LiveView computes the status server-side and passes it to a minimal JS hook that generates PNG favicons via canvas.
This commit is contained in:
parent
ade7e2fe15
commit
b56cdf4e9f
4 changed files with 47 additions and 21 deletions
|
|
@ -885,35 +885,50 @@ const NetworkMap = {
|
|||
}
|
||||
}
|
||||
|
||||
// Dynamic Favicon - alternates favicon between status indicator SVGs
|
||||
// Dynamic Favicon - sets favicon color based on data-status from LiveView
|
||||
const DynamicFavicon = {
|
||||
interval: null as ReturnType<typeof setInterval> | null,
|
||||
isGreen: true,
|
||||
statusColors: { green: '#22c55e', yellow: '#eab308', red: '#ef4444' } as Record<string, string>,
|
||||
cache: {} as Record<string, string>,
|
||||
originalHref: null as string | null,
|
||||
|
||||
mounted(this: any) {
|
||||
const greenSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><radialGradient id="g" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#4ade80"/><stop offset="60%" stop-color="#22c55e"/><stop offset="100%" stop-color="#16a34a"/></radialGradient><filter id="glow"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><circle cx="16" cy="16" r="12" fill="url(#g)" filter="url(#glow)"/><circle cx="13" cy="13" r="3" fill="white" opacity="0.3"/></svg>`
|
||||
const redSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><radialGradient id="r" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#f87171"/><stop offset="60%" stop-color="#ef4444"/><stop offset="100%" stop-color="#dc2626"/></radialGradient><filter id="glow"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><circle cx="16" cy="16" r="12" fill="url(#r)" filter="url(#glow)"/><circle cx="13" cy="13" r="3" fill="white" opacity="0.3"/></svg>`
|
||||
|
||||
const greenDataUri = 'data:image/svg+xml,' + encodeURIComponent(greenSvg)
|
||||
const redDataUri = 'data:image/svg+xml,' + encodeURIComponent(redSvg)
|
||||
getFaviconPng(this: any, status: string): string {
|
||||
if (this.cache[status]) return this.cache[status]
|
||||
const color = this.statusColors[status] || this.statusColors.green
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 64
|
||||
canvas.height = 64
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.beginPath()
|
||||
ctx.arc(32, 32, 32, 0, Math.PI * 2)
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
this.cache[status] = canvas.toDataURL('image/png')
|
||||
return this.cache[status]
|
||||
},
|
||||
|
||||
applyStatus(this: any) {
|
||||
const faviconEl = document.getElementById('favicon') as HTMLLinkElement
|
||||
if (!faviconEl) return
|
||||
const status = this.el.dataset.status || 'green'
|
||||
faviconEl.type = 'image/png'
|
||||
faviconEl.href = this.getFaviconPng(status)
|
||||
},
|
||||
|
||||
this.isGreen = true
|
||||
faviconEl.type = 'image/svg+xml'
|
||||
faviconEl.href = greenDataUri
|
||||
mounted(this: any) {
|
||||
const faviconEl = document.getElementById('favicon') as HTMLLinkElement
|
||||
if (faviconEl) this.originalHref = faviconEl.href
|
||||
this.applyStatus()
|
||||
},
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.isGreen = !this.isGreen
|
||||
faviconEl.href = this.isGreen ? greenDataUri : redDataUri
|
||||
}, 1000)
|
||||
updated(this: any) {
|
||||
this.applyStatus()
|
||||
},
|
||||
|
||||
destroyed(this: any) {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval)
|
||||
this.interval = null
|
||||
const faviconEl = document.getElementById('favicon') as HTMLLinkElement
|
||||
if (faviconEl && this.originalHref) {
|
||||
faviconEl.type = 'image/png'
|
||||
faviconEl.href = this.originalHref
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,12 @@
|
|||
{t_auth("You are running the local mail adapter.")}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-blue-700 dark:text-blue-200">
|
||||
<%= t_auth("To see sent emails, visit %{link}.", link: raw(~s(<a href="/dev/mailbox" class="font-medium underline">#{t_auth("the mailbox page")}</a>))) %>
|
||||
{t_auth("To see sent emails, visit %{link}.",
|
||||
link:
|
||||
raw(
|
||||
~s(<a href="/dev/mailbox" class="font-medium underline">#{t_auth("the mailbox page")}</a>)
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ defmodule ToweropsWeb.DashboardLive do
|
|||
|> assign(:uptime_percentage, uptime_percentage)
|
||||
|> assign(:recent_activity, recent_activity)
|
||||
|> assign(:last_updated, DateTime.utc_now())
|
||||
|> assign(:favicon_status, favicon_status(Map.get(status_counts, :down, 0), length(active_alerts)))
|
||||
|> load_insights(organization_id)
|
||||
|> load_recent_config_changes(organization_id)
|
||||
end
|
||||
|
|
@ -185,6 +186,10 @@ defmodule ToweropsWeb.DashboardLive do
|
|||
assign(socket, :insights, insights)
|
||||
end
|
||||
|
||||
defp favicon_status(devices_down, _alert_count) when devices_down > 0, do: "red"
|
||||
defp favicon_status(_devices_down, alert_count) when alert_count > 0, do: "yellow"
|
||||
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-yellow-600 dark:text-yellow-400"
|
||||
defp health_score_color(_score), do: "text-red-600 dark:text-red-400"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
current_scope={@current_scope}
|
||||
active_page="dashboard"
|
||||
>
|
||||
<div id="dynamic-favicon" phx-hook="DynamicFavicon" class="hidden"></div>
|
||||
<div id="dynamic-favicon" phx-hook="DynamicFavicon" data-status={@favicon_status} class="hidden">
|
||||
</div>
|
||||
<%!-- ═══════════════════════════════════════════════
|
||||
NOC HEADER — dense, zero chrome
|
||||
═══════════════════════════════════════════════ --%>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue