feat: infrastructure dashboard (Homepage) generated from the service registry
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s

playbooks/dashboard.yml deploys Homepage as a second compose stack on the
monitoring LXC (CT 155) next to Uptime Kuma and renders its config from
homelab_services: one tile per service, link to its UI, grouped by Proxmox
node. Adding a service to the registry is enough — no second service list.

- new registry consumer: playbooks/dashboard.yml + playbooks/templates/homepage-*.j2
- homelab_dashboard_* vars in group_vars/all/services.yml (top-level, like
  homelab_reverse_proxy_*); image pinned by digest, floating tag needs an
  explicit -e dashboard_allow_floating_tag=true
- bootstrap-dashboard-pve-token.yml: read-only homepage@pve!dashboard token
  (PVEAuditor) for the Proxmox widget, secret in the root .env as DASHBOARD_PVE_*
- Makefile: dashboard, dry-dashboard, bootstrap-dashboard-token
- container binds the LAN address only (192.168.1.30:8082), not published via Caddy
- docs: architecture.md Monitoring section, plan.md active task, consumer lists

Deployed to CT 155 on 2026-09-03: container healthy, http://192.168.1.30:8082/
returns 200, `make dashboard` idempotent, `make validate` and `make lint` green.
Pending operator steps: `make bootstrap-dashboard-token` (blocked in the agent
session as credential creation) and an Uptime Kuma status page with slug homelab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
This commit is contained in:
Dmitry
2026-09-03 09:17:16 +03:00
co-authored by Claude Sonnet 5
parent ca48ef2696
commit 05d8c748ab
13 changed files with 484 additions and 2 deletions
@@ -0,0 +1,16 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Статические админ-ссылки (не из реестра). Рендерится playbooks/dashboard.yml.
- Управление:
- Proxmox cloud-pc:
- abbr: PVE
href: https://192.168.1.5:8006
- Proxmox mini-pc:
- abbr: PVE
href: https://192.168.1.10:8006
- Proxmox Backup Server:
- abbr: PBS
href: https://192.168.1.20:8007
- Uptime Kuma:
- abbr: UK
href: http://{{ dash_bind }}:3001
@@ -0,0 +1,21 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Рендерится playbooks/dashboard.yml (роль compose_service). Правь шаблон,
# а не файл на хосте: следующий прогон перезапишет.
services:
homepage:
image: {{ dash_image }}
container_name: homelab-homepage
restart: unless-stopped
env_file:
- ./homepage.env
environment:
HOMEPAGE_ALLOWED_HOSTS: "{{ dash_allowed_hosts | trim }}"
ports:
# Только LAN-адрес CT 155 -> контейнерный 3000. Наружу не публикуется.
- "{{ dash_bind }}:{{ dash_port }}:3000"
volumes:
- ./config:/app/config
# Docker-сокет намеренно НЕ монтируется: сервисы живут на других хостах,
# а лишний доступ к сокету на LAN-видимом контейнере не нужен.
networks: {}
@@ -0,0 +1,70 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Рендерится playbooks/dashboard.yml ИЗ реестра homelab_services. НЕ править
# на хосте: меняй сервисы в ansible/inventory/group_vars/all/services.yml,
# затем `make dashboard`.
#
# Правило ссылки на сервис:
# * есть proxy.domain -> https://<domain> (+ siteMonitor)
# * иначе первый веб-порт -> http(s)://<ip>:<port> (+ siteMonitor для http)
# приоритет имён портов: http > ui > setup > uptime-kuma > grafana >
# controller > pbs-api
# * ни того, ни другого -> группа Headless, только ICMP ping по ip
{% set port_priority = ['http', 'ui', 'setup', 'uptime-kuma', 'grafana', 'controller', 'pbs-api'] %}
{% if homelab_dashboard_proxmox_url | default('') | length > 0 %}
- Infrastructure:
- Proxmox кластер:
description: cloud-pc + mini-pc
widget:
type: proxmox
url: {{ homelab_dashboard_proxmox_url }}
username: "{{ '{{HOMEPAGE_VAR_PROXMOX_USER}}' }}"
password: "{{ '{{HOMEPAGE_VAR_PROXMOX_TOKEN}}' }}"
{% endif %}
{% for node in dashboard_nodes %}
- {{ node }}:
{% for name, svc in homelab_services | dictsort %}
{% if svc.node == node %}
{% set proxy = svc.proxy | default({}) %}
{% set ns = namespace(scheme='', port=0) %}
{% for pref in port_priority %}
{% for p in svc.ports | default([]) %}
{% if ns.port == 0 and p.name == pref %}
{% set ns.port = p.port %}
{% set ns.scheme = 'https' if pref == 'pbs-api' else 'http' %}
{% endif %}
{% endfor %}
{% endfor %}
{% if proxy.domain is defined %}
- {{ name }}:
href: https://{{ proxy.domain }}
description: {{ svc.role }}
siteMonitor: https://{{ proxy.domain }}
{% elif ns.port > 0 %}
- {{ name }}:
href: {{ ns.scheme }}://{{ svc.ip }}:{{ ns.port }}
description: {{ svc.role }}
{% if ns.scheme == 'http' %}
siteMonitor: http://{{ svc.ip }}:{{ ns.port }}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
- Headless:
{% for name, svc in homelab_services | dictsort %}
{% set proxy = svc.proxy | default({}) %}
{% set ns = namespace(web=false) %}
{% for pref in port_priority %}
{% for p in svc.ports | default([]) %}
{% if p.name == pref %}
{% set ns.web = true %}
{% endif %}
{% endfor %}
{% endfor %}
{% if proxy.domain is not defined and not ns.web %}
- {{ name }}:
description: {{ svc.role }}
ping: {{ svc.ip }}
{% endif %}
{% endfor %}
@@ -0,0 +1,21 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Рендерится playbooks/dashboard.yml. Порядок и имена групп ОБЯЗАНЫ совпадать
# с группами в services.yaml — иначе Homepage покажет их в алфавитном порядке.
title: HomeLab
headerStyle: boxed
color: slate
layout:
{% if homelab_dashboard_proxmox_url | default('') | length > 0 %}
Infrastructure:
style: row
columns: 3
{% endif %}
{% for node in dashboard_nodes %}
{{ node }}:
style: row
columns: 4
{% endfor %}
Headless:
style: row
columns: 4
@@ -0,0 +1,18 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Верхняя панель Homepage. Рендерится playbooks/dashboard.yml.
- datetime:
text_size: xl
format:
timeStyle: short
dateStyle: short
- resources:
label: monitoring CT
cpu: true
memory: true
disk: /
- uptimekuma:
# Требует опубликованной Status Page в Uptime Kuma с этим slug
# (мониторы и статус-страницы Kuma живут только в её UI).
url: http://{{ dash_bind }}:3001
slug: {{ homelab_dashboard_kuma_slug | default('homelab') }}
@@ -0,0 +1,12 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
# {{ ansible_managed }}
# Секреты виджетов Homepage. mode 0600, в Git не попадает. Значения берутся
# из корневого .env через lookup('env', ...) в playbooks/dashboard.yml.
# Homepage подставляет их в widgets/services по имени {{ '{{HOMEPAGE_VAR_*}}' }}.
HOMEPAGE_VAR_PROXMOX_URL={{ homelab_dashboard_proxmox_url | default('') }}
{% if dash_pve_user | length > 0 and dash_pve_token_id | length > 0 %}
HOMEPAGE_VAR_PROXMOX_USER={{ dash_pve_user }}!{{ dash_pve_token_id }}
{% else %}
HOMEPAGE_VAR_PROXMOX_USER=
{% endif %}
HOMEPAGE_VAR_PROXMOX_TOKEN={{ dash_pve_token_secret | default('') }}