The problem: two dashboards, only one that works
The post Cluster dashboard with Homepage + Glances documented Homepage + Glances as the “auto-discovered” entry point to the homelab. In practice, that dashboard had been coexisting for months with a second, personal hub — the one this post is about — and Homepage had become unreliable and less and less consulted: badly discovered services, widgets timing out, a YAML config file I had to reopen for every new app just to find the right syntax again.
Rather than repairing it one more time, the decision was blunter: full decommission. No “better auto-discovery tool” replacement — just plain removal from the cluster.
index.html statique dans une ConfigMap : zéro paquet,
sauf sur l'unique route /admin/api qui proxie vers homelab-admin.
Homepage kept polling continuously and eventually timed out, duplicating the personal hub — decommissioned rather than fixed one more time.
The hub that remains is a static index.html in a ConfigMap: zero packets,
except on the single /admin/api route that proxies to homelab-admin.
The choice: static rather than dynamic
The remaining hub runs on a deliberately impoverished principle: a single index.html stored in a Kubernetes ConfigMap (hub-static-html, namespace yb-personal), served by a plain nginx:1.27-alpine with a volume mount:
# yb-personal/deploy-hub-static.yaml (excerpt)
containers:
- image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- mountPath: /usr/share/nginx/html
name: html-volume
volumes:
- configMap:
name: hub-static-html
name: html-volume
No runtime dependency, no pod that can crash for lack of a connection to a third-party API, no YAML DSL to learn in order to add a service. Editing the hub means modifying an HTML file in the GitOps repo, committing, and letting ArgoCD synchronise — the same gesture as for any other component of the cluster. For a strictly personal dashboard, Homepage’s “automatic discovery” solved a problem I did not really have: the list of services changes once every two weeks, not every hour.
A “Pro Ops” design system
The palette starts from a near-black background rather than the classic gradient dark theme: #0a0e14 for the canvas, #10151d for surfaces, #1f2733 for borders. A single accent — a teal #2dd4bf — and the three classic semantic colours reserved exclusively for statuses: green #3fb950 (ok), amber #d29922 (warn), red #f85149 (err). Two typefaces: Inter for the interface, JetBrains Mono for anything that is a value, an IP or a metric — the typographic distinction helps you spot at a glance what is raw data versus what is a label.
The layout is designed for a wall display, not a laptop. Tailwind’s default breakpoints stop at 2xl (1536px); I added two beyond that:
screens: {
'3xl': '2200px',
'4xl': '2800px',
},
The service catalogue is a masonry layout built on CSS columns, widening with the screen: one column on mobile, up to six in 4K.
<main class="columns-1 md:columns-2 xl:columns-3 2xl:columns-4 3xl:columns-5 4xl:columns-6 gap-5 2xl:gap-6">
The main container follows the same logic (max-w-[2560px], 4xl:max-w-[3360px]): on a 4K screen hanging on the wall, the hub genuinely uses the available width instead of staying frozen in a narrow central column.
The only dynamic part: an API proxy for the live data
All the HTML is static — except for the few widgets that need real-time data (node status, and eventually live cpu/mem). Rather than duplicating an API inside the hub itself, those calls are routed to the already existing homelab-admin service, through a dedicated Traefik IngressRoute:
# homelab-admin/ingressroute-hub-proxy.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: hub-admin-proxy
namespace: homelab-admin
spec:
routes:
- match: Host(`hub.olig-homelab-2025.win`) && PathPrefix(`/admin/api`)
kind: Rule
priority: 1000
services:
- name: homelab-admin
namespace: homelab-admin
port: 8080
The detail that matters here is priority: 1000. The hub’s standard Kubernetes Ingress already routes / to hub-static-svc. Without an explicitly higher priority on the /admin/api rule, Traefik could let the broader rule (the root prefix) intercept requests that should go to homelab-admin. A priority forced to 1000 removes all ambiguity: the API prefix always wins, whatever the declaration order of the two resources in the cluster.
Sections added as things happened
The hub was not shipped frozen one day and then forgotten — it reflects the infrastructure as that infrastructure changes. Deploying the document scanning pipeline (Paperless-ngx, Paperless-AI, scanservjs) immediately led to adding a “Scan & OCR” section to the catalogue. The next day, once Homepage’s decommission was settled, its link in the “Sites & Apps” section was removed in turn — two separate commits, each reacting to a real change in the infrastructure rather than anticipating a plan frozen in advance.
That is the practical difference between an “intelligent” dashboard that should in theory keep up on its own, and a versioned static file: the latter follows nothing automatically, but every change is a readable, dated, deliberate Git diff.
Transferable lesson
For a personal dashboard with a low rate of change, a static HTML file versioned in Git almost always beats one more “intelligent” tool to operate. Less failure surface — no extra pod that can crash, no automatic discovery that picks the wrong service. A readable Git history rather than state hidden in a config database. And the real cost, the time spent editing HTML by hand, stays marginal compared to the time another round of debugging Homepage would have cost.
The hub runs today behind hub.olig-homelab-2025.win, GitOps via ArgoCD like the rest of the cluster, with homelab-admin as its only dynamic dependency. Not one more dashboard — one dashboard fewer, and the one that remains is simpler than the two it replaces.