Initial commit: homelab dashboard
Elixir + Phoenix dashboard displaying public and internal homelab services. Internal services are shown only when the request originates from the local network, detected server-side via conn.remote_ip with Traefik trusted as a proxy (RemoteIp plug). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
defmodule HomelabDashboardWeb.ErrorHTML do
|
||||
@moduledoc """
|
||||
This module is invoked by your endpoint in case of errors on HTML requests.
|
||||
|
||||
See config/config.exs.
|
||||
"""
|
||||
use HomelabDashboardWeb, :html
|
||||
|
||||
# If you want to customize your error pages,
|
||||
# uncomment the embed_templates/1 call below
|
||||
# and add pages to the error directory:
|
||||
#
|
||||
# * lib/homelab_dashboard_web/controllers/error_html/404.html.heex
|
||||
# * lib/homelab_dashboard_web/controllers/error_html/500.html.heex
|
||||
#
|
||||
# embed_templates "error_html/*"
|
||||
|
||||
# The default is to render a plain text page based on
|
||||
# the template name. For example, "404.html" becomes
|
||||
# "Not Found".
|
||||
def render(template, _assigns) do
|
||||
Phoenix.Controller.status_message_from_template(template)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule HomelabDashboardWeb.ErrorJSON do
|
||||
@moduledoc """
|
||||
This module is invoked by your endpoint in case of errors on JSON requests.
|
||||
|
||||
See config/config.exs.
|
||||
"""
|
||||
|
||||
# If you want to customize a particular status code,
|
||||
# you may add your own clauses, such as:
|
||||
#
|
||||
# def render("500.json", _assigns) do
|
||||
# %{errors: %{detail: "Internal Server Error"}}
|
||||
# end
|
||||
|
||||
# By default, Phoenix returns the status message from
|
||||
# the template name. For example, "404.json" becomes
|
||||
# "Not Found".
|
||||
def render(template, _assigns) do
|
||||
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
defmodule HomelabDashboardWeb.PageController do
|
||||
use HomelabDashboardWeb, :controller
|
||||
import Bitwise
|
||||
|
||||
@public_services [
|
||||
%{
|
||||
name: "MTG",
|
||||
url: "https://adyrem.duckdns.org/mtg",
|
||||
description: "Magic: The Gathering collection",
|
||||
icon: "hero-puzzle-piece"
|
||||
},
|
||||
%{
|
||||
name: "Gitea",
|
||||
url: "https://adyrem.duckdns.org/git",
|
||||
description: "Git repository hosting",
|
||||
icon: "hero-code-bracket"
|
||||
}
|
||||
]
|
||||
|
||||
@internal_services [
|
||||
%{
|
||||
name: "Proxmox",
|
||||
url: "https://proxmox.homelab",
|
||||
description: "Virtualization & container management",
|
||||
icon: "hero-server"
|
||||
},
|
||||
%{
|
||||
name: "Grafana",
|
||||
url: "https://grafana.homelab",
|
||||
description: "Metrics & monitoring dashboards",
|
||||
icon: "hero-chart-bar"
|
||||
},
|
||||
%{
|
||||
name: "Pi-hole",
|
||||
url: "https://pihole.homelab/admin",
|
||||
description: "Network-wide DNS ad blocker",
|
||||
icon: "hero-shield-check"
|
||||
},
|
||||
%{
|
||||
name: "Jellyfin",
|
||||
url: "https://jellyfin.homelab",
|
||||
description: "Media server",
|
||||
icon: "hero-film"
|
||||
},
|
||||
%{
|
||||
name: "Jellyseerr",
|
||||
url: "https://jellyseerr.homelab",
|
||||
description: "Media request management",
|
||||
icon: "hero-ticket"
|
||||
},
|
||||
%{
|
||||
name: "Radarr",
|
||||
url: "https://radarr.homelab",
|
||||
description: "Movie collection manager",
|
||||
icon: "hero-video-camera"
|
||||
},
|
||||
%{
|
||||
name: "Sonarr",
|
||||
url: "https://sonarr.homelab",
|
||||
description: "TV series collection manager",
|
||||
icon: "hero-tv"
|
||||
},
|
||||
%{
|
||||
name: "Prowlarr",
|
||||
url: "https://prowlarr.homelab",
|
||||
description: "Indexer manager",
|
||||
icon: "hero-magnifying-glass"
|
||||
},
|
||||
%{
|
||||
name: "qBittorrent",
|
||||
url: "https://qbittorrent.homelab",
|
||||
description: "Torrent client",
|
||||
icon: "hero-arrow-down-tray"
|
||||
}
|
||||
]
|
||||
|
||||
def home(conn, _params) do
|
||||
render(conn, :home,
|
||||
public_services: @public_services,
|
||||
internal_services: @internal_services,
|
||||
show_internal: internal_network?(conn.remote_ip)
|
||||
)
|
||||
end
|
||||
|
||||
defp internal_network?({127, _, _, _}), do: true
|
||||
defp internal_network?({10, _, _, _}), do: true
|
||||
defp internal_network?({192, 168, _, _}), do: true
|
||||
defp internal_network?({172, b, _, _}) when b in 16..31, do: true
|
||||
defp internal_network?({0, 0, 0, 0, 0, 0, 0, 1}), do: true
|
||||
# IPv6-mapped IPv4: ::ffff:x.x.x.x — decode and re-check
|
||||
defp internal_network?({0, 0, 0, 0, 0, 65535, ab, cd}) do
|
||||
internal_network?({ab >>> 8, ab &&& 0xFF, cd >>> 8, cd &&& 0xFF})
|
||||
end
|
||||
defp internal_network?(_), do: false
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule HomelabDashboardWeb.PageHTML do
|
||||
@moduledoc """
|
||||
This module contains pages rendered by PageController.
|
||||
|
||||
See the `page_html` directory for all templates available.
|
||||
"""
|
||||
use HomelabDashboardWeb, :html
|
||||
|
||||
embed_templates "page_html/*"
|
||||
end
|
||||
@@ -0,0 +1,76 @@
|
||||
<Layouts.flash_group flash={@flash} />
|
||||
<div class="min-h-screen bg-base-100">
|
||||
<header class="border-b border-base-300 bg-base-200">
|
||||
<div class="mx-auto max-w-5xl px-6 py-6 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="hero-home h-7 w-7 text-primary"></span>
|
||||
<h1 class="text-xl font-bold tracking-tight">Homelab Dashboard</h1>
|
||||
</div>
|
||||
<Layouts.theme_toggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="mx-auto max-w-5xl px-6 py-10 space-y-10">
|
||||
<section>
|
||||
<h2 class="text-xs font-semibold uppercase tracking-widest text-base-content/50 mb-4">
|
||||
Public Services
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<%= for service <- @public_services do %>
|
||||
<a
|
||||
href={service.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group flex items-start gap-4 rounded-box bg-base-200 p-5 transition hover:bg-base-300 hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-btn bg-primary/10 text-primary group-hover:bg-primary/20 transition">
|
||||
<span class={[service.icon, "h-5 w-5"]}></span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold"><%= service.name %></span>
|
||||
<span class="hero-arrow-top-right-on-square h-3.5 w-3.5 text-base-content/30 group-hover:text-primary transition"></span>
|
||||
</div>
|
||||
<p class="mt-0.5 text-sm text-base-content/60 leading-snug"><%= service.description %></p>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%= if @show_internal do %>
|
||||
<section>
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-widest text-base-content/50">
|
||||
Internal Services
|
||||
</h2>
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<span class="inline-block h-1.5 w-1.5 rounded-full bg-success-content"></span>
|
||||
Local network
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<%= for service <- @internal_services do %>
|
||||
<a
|
||||
href={service.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group flex items-start gap-4 rounded-box bg-base-200 p-5 transition hover:bg-base-300 hover:shadow-md"
|
||||
>
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-btn bg-secondary/10 text-secondary group-hover:bg-secondary/20 transition">
|
||||
<span class={[service.icon, "h-5 w-5"]}></span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold"><%= service.name %></span>
|
||||
<span class="hero-arrow-top-right-on-square h-3.5 w-3.5 text-base-content/30 group-hover:text-secondary transition"></span>
|
||||
</div>
|
||||
<p class="mt-0.5 text-sm text-base-content/60 leading-snug"><%= service.description %></p>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
<% end %>
|
||||
</main>
|
||||
</div>
|
||||
Reference in New Issue
Block a user