design v5: echte Logos der Dienste (selfh.st/icons, CC BY 4.0)

181 gängige Homelab-Dienste als SVG unter /logos (440 KB), geladen mit
frontend/scripts/logos_laden.py (User-Ja 25.09.2026). 33 davon in der hellen
Variante, weil das Standard-Logo auf dem dunklen Grund verschwände (gemessen:
jedes Logo auf den Kachelgrund gezeichnet). Das Gerätesymbol zeigt das Logo,
wo es eines gibt (Name, feste Zuordnung für NPMplus, PBS, PVE Scripts), sonst
das gezeichnete Symbol; mit Logo wird die Kachel neutral, nur der Rand trägt
den Bereich. KI-Box: Ubuntu, llama.cpp, Hermes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-25 15:32:00 +02:00
co-authored by Claude Opus 5.5
parent 9d56de457d
commit d524e03c68
187 changed files with 514 additions and 33 deletions
@@ -1,26 +1,32 @@
import { cn } from "cn"
import { type Geraetebild, PUNKT, type Punktzustand } from "@/lib/geraetebild"
// Kachel in der Farbe des Bereichs; Zustände trägt nur der Punkt (grün/bernstein/rot/cyan) — nie das Symbol.
// Kachel in der Farbe des Bereichs; Zustände trägt nur der Punkt (grün/bernstein/rot/cyan) — nie das Bild.
// Mit echtem Logo wird die Kachel neutral (sonst färbte das Violett die Markenfarben), nur der Rand behält den Bereich.
const BEREICH = {
box: "from-box/30 to-box/[0.05] ring-box/35 text-box-text",
homelab: "from-lab/30 to-lab/[0.05] ring-lab/35 text-lab-text",
} as const
const LOGO_KACHEL = {
box: "from-white/[0.1] to-white/[0.03] ring-box/30",
homelab: "from-white/[0.1] to-white/[0.03] ring-lab/30",
} as const
const LEUCHTEN = {
box: "shadow-[0_0_28px_-6px_var(--box)]",
homelab: "shadow-[0_0_28px_-6px_var(--lab)]",
} as const
const GROESSE = {
sm: { kachel: "size-8 rounded-[10px]", icon: "size-4", punkt: "size-2.5" },
md: { kachel: "size-10 rounded-xl", icon: "size-5", punkt: "size-2.5" },
lg: { kachel: "size-12 rounded-2xl", icon: "size-6", punkt: "size-3" },
sm: { kachel: "size-8 rounded-[10px]", icon: "size-4", logo: "size-5", punkt: "size-2.5" },
md: { kachel: "size-10 rounded-xl", icon: "size-5", logo: "size-6", punkt: "size-2.5" },
lg: { kachel: "size-12 rounded-2xl", icon: "size-6", logo: "size-7", punkt: "size-3" },
} as const
/**
* Das Symbol eines Geräts oder Dienstes als kleine Kachel, optional mit Zustandspunkt unten rechts. Rein
* schmückend (aria-hidden) — Name und Zustand stehen daneben als Text.
* Das Bild eines Geräts oder Dienstes als kleine Kachel — das echte Logo, wo es eines gibt, sonst ein Symbol —,
* optional mit Zustandspunkt unten rechts. Rein schmückend (aria-hidden): Name und Zustand stehen daneben als Text.
*/
export function Geraetesymbol({ bild, groesse = "md", punkt, leuchtet = false, className }: {
bild: Geraetebild
@@ -36,10 +42,13 @@ export function Geraetesymbol({ bild, groesse = "md", punkt, leuchtet = false, c
aria-hidden
className={cn(
"relative inline-flex shrink-0 items-center justify-center bg-gradient-to-br ring-1 ring-inset",
BEREICH[bild.bereich], g.kachel, leuchtet && LEUCHTEN[bild.bereich], className,
bild.logo ? LOGO_KACHEL[bild.bereich] : BEREICH[bild.bereich], g.kachel, leuchtet && LEUCHTEN[bild.bereich],
className,
)}
>
<bild.Icon className={g.icon} strokeWidth={1.75} />
{bild.logo
? <img src={`/logos/${bild.logo}.svg`} alt="" className={cn("object-contain", g.logo)} loading="lazy" draggable={false} />
: <bild.Icon className={g.icon} strokeWidth={1.75} />}
{punkt && (
<span className={cn("absolute -right-0.5 -bottom-0.5 rounded-full ring-2 ring-[#0b0f16]", g.punkt, PUNKT[punkt])} />
)}
+47 -24
View File
@@ -2,15 +2,19 @@ import {
Bird, Bot, Box, BrainCircuit, Clapperboard, Code, Container, DatabaseBackup, FileCode2, GitBranch, HardDrive, House,
Layers, type LucideIcon, Monitor, Route, Server, ShieldCheck, Shuffle, Workflow, Zap,
} from "lucide-react"
import { LOGOS } from "@/lib/logos-liste"
import type { Bereich } from "@/lib/navigation"
// Jedes Gerät bekommt ein eigenes Symbol (Design v5, 25.09.2026) — wie bei Heimdall, Dashy oder Homepage erkennt
// man es so auf einen Blick. Die Farbe bleibt die des Bereichs (Indigo KI-Box, Violett Homelab): Farben für
// Zustände (grün, bernstein, rot, cyan) tragen nur Punkte und Schildchen, nie das Symbol.
// Jedes Gerät bekommt ein eigenes Bild (Design v5, 25.09.2026) — wie bei Heimdall, Dashy oder Homepage erkennt man
// es so auf einen Blick: das echte Logo (selfh.st/icons, CC BY 4.0, mitgeliefert unter /logos), sonst ein
// gezeichnetes Symbol in der Farbe des Bereichs. Farben für Zustände (grün, bernstein, rot, cyan) tragen nur Punkte
// und Schildchen, nie das Bild.
export interface Geraetebild {
Icon: LucideIcon
bereich: Bereich
/** Name eines mitgelieferten Logos (/logos/<name>.svg), wo es eines gibt. */
logo?: string
}
export type Punktzustand = "ok" | "warn" | "rot" | "info" | "aus"
@@ -24,44 +28,63 @@ export const PUNKT: Record<Punktzustand, string> = {
aus: "bg-text-3",
}
// Nach dem Namen, weil die ID nur die Nummer kennt. Reihenfolge zählt: Spezielles vor Allgemeinem.
const NACH_NAME: [RegExp, LucideIcon][] = [
// Nach dem Namen, weil die ID nur die Nummer kennt. Reihenfolge zählt: Spezielles vor Allgemeinem. Der dritte Wert
// nennt das Logo, wo der Name allein nicht darauf führt.
const NACH_NAME: [RegExp, LucideIcon, string?][] = [
[/adguard|pi-?hole/i, ShieldCheck],
[/npm|nginx proxy|traefik|caddy/i, Route],
[/npm|nginx proxy/i, Route, "nginx-proxy-manager"],
[/traefik|caddy/i, Route],
[/netbird|wireguard|tailscale/i, Bird],
[/gitea|forgejo|gitlab/i, GitBranch],
[/backup server|\bpbs\b/i, DatabaseBackup],
// Proxmox Backup Server trägt das Proxmox-Zeichen.
[/backup server|\bpbs\b/i, DatabaseBackup, "proxmox"],
[/arcane|docker|portainer|dockge|komodo/i, Container],
[/pve scripts|helper.?scripts|community.?scripts/i, FileCode2, "proxmox-helper-scripts"],
[/scripts/i, FileCode2],
[/home ?assistant/i, House],
[/jellyfin|plex|emby/i, Clapperboard],
[/\bnas\b|qnap|truenas|synology/i, HardDrive],
]
/** Das Symbol eines Geräts im Ziel-Modell (KI-Box, Proxmox-Host, Container, VM). */
/** „Arcane (Docker)“ → „arcane“, „AdGuard Home“ → „adguard-home“. */
export function logoName(name: string): string {
return name.toLowerCase().replace(/\(.*?\)/g, "").trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
}
/** Das Logo zu einem Namen, wo es eines gibt: fester Eintrag der Liste oben, sonst der Name selbst. */
function logoFuer(name: string, fest?: string): string | undefined {
if (fest && LOGOS.has(fest)) return fest
const kandidat = logoName(name)
if (LOGOS.has(kandidat)) return kandidat
const erstesWort = kandidat.split("-")[0]
return erstesWort.length > 2 && LOGOS.has(erstesWort) ? erstesWort : undefined
}
/** Das Bild eines Geräts im Ziel-Modell (KI-Box, Proxmox-Host, Container, VM). */
export function geraetebild(ziel: { art: string; name: string }): Geraetebild {
if (ziel.art === "ki-box") return { Icon: BrainCircuit, bereich: "box" }
if (ziel.art === "proxmox-host") return { Icon: Server, bereich: "homelab" }
if (ziel.art === "proxmox-host") return { Icon: Server, bereich: "homelab", logo: logoFuer("proxmox") }
const treffer = NACH_NAME.find(([muster]) => muster.test(ziel.name))
if (treffer) return { Icon: treffer[1], bereich: "homelab" }
return { Icon: ziel.art === "vm" ? Monitor : Box, bereich: "homelab" }
const Icon = treffer?.[1] ?? (ziel.art === "vm" ? Monitor : Box)
return { Icon, bereich: "homelab", logo: logoFuer(ziel.name, treffer?.[2]) }
}
// Die Bausteine der KI-Box (GET /api/ziele) und die Lampen der Startdaten (GET /api/start).
const KI_BOX: Record<string, LucideIcon> = {
os: Layers,
engine: Zap,
motor: Zap,
swap: Shuffle,
hermes: Bot,
hirn: BrainCircuit,
coder: Code,
jobs: Workflow,
sicherung: DatabaseBackup,
platte: HardDrive,
const KI_BOX: Record<string, [LucideIcon, string?]> = {
os: [Layers, "ubuntu"],
engine: [Zap, "llama-cpp"],
motor: [Zap, "llama-cpp"],
swap: [Shuffle],
hermes: [Bot, "hermes-agent"],
hirn: [BrainCircuit],
coder: [Code],
jobs: [Workflow],
sicherung: [DatabaseBackup],
platte: [HardDrive],
}
/** Das Symbol eines Bausteins oder einer Lampe der KI-Box. */
/** Das Bild eines Bausteins oder einer Lampe der KI-Box. */
export function boxbild(id: string): Geraetebild {
return { Icon: KI_BOX[id] ?? Box, bereich: "box" }
const [Icon, logo] = KI_BOX[id] ?? [Box]
return { Icon, bereich: "box", logo: logo && LOGOS.has(logo) ? logo : undefined }
}
+185
View File
@@ -0,0 +1,185 @@
// Erzeugt von frontend/scripts/logos_laden.py: die Logos unter /logos/<name>.svg (selfh.st/icons, CC BY 4.0).
// Nicht von Hand pflegen — neue Logos mit demselben Skript holen.
export const LOGOS: ReadonlySet<string> = new Set([
"actual-budget",
"adguard-home",
"affine",
"alpine-linux",
"ansible",
"appflowy",
"apprise",
"arcane",
"audiobookshelf",
"authelia",
"authentik",
"baserow",
"bazarr",
"beszel",
"bitwarden",
"bookstack",
"borgmatic",
"caddy",
"calibre-web",
"changedetection",
"checkmk",
"claude",
"cloudflare",
"cockpit",
"comfyui",
"crafty-controller",
"crowdsec",
"debian",
"deluge",
"docker",
"dockge",
"docmost",
"dozzle",
"draw-io",
"duplicati",
"elasticsearch",
"element",
"emby",
"esphome",
"excalidraw",
"firefly-iii",
"flatnotes",
"forgejo",
"freshrss",
"frigate",
"gatus",
"gitea",
"github",
"gitlab",
"glance",
"glances",
"go2rtc",
"gotify",
"grafana",
"grocy",
"harbor",
"headscale",
"healthchecks",
"hedgedoc",
"heimdall",
"hermes-agent",
"homarr",
"home-assistant",
"homebridge",
"homepage",
"immich",
"immich-kiosk",
"influxdb",
"it-tools",
"jellyfin",
"jellyseerr",
"jenkins",
"jitsi-meet",
"joplin",
"kavita",
"keycloak",
"komga",
"komodo",
"kopia",
"kubernetes",
"librenms",
"librespeed",
"lidarr",
"linkding",
"linux",
"linux-containers-lxc",
"linuxserver-io",
"llama-cpp",
"logseq",
"loki",
"mariadb",
"matomo",
"matrix",
"mattermost",
"mealie",
"minecraft",
"miniflux",
"minio",
"mongodb",
"mosquitto",
"n8n",
"navidrome",
"netbird",
"netdata",
"nextcloud",
"nginx",
"nginx-proxy-manager",
"nocodb",
"node-red",
"ntfy",
"nzbget",
"obsidian",
"ollama",
"onlyoffice",
"open-webui",
"openai",
"openmediavault",
"openwrt",
"opnsense",
"outline",
"overseerr",
"paperless-ngx",
"penpot",
"pfsense",
"photoprism",
"pi-hole",
"pingvin-share",
"piwigo",
"planka",
"plausible",
"plex",
"portainer",
"postgresql",
"privatebin",
"prometheus",
"prowlarr",
"proxmox",
"proxmox-helper-scripts",
"pterodactyl",
"qbittorrent",
"qnap",
"rabbitmq",
"radarr",
"rancher",
"rclone",
"readarr",
"redis",
"rocket-chat",
"rustdesk",
"sabnzbd",
"scrutiny",
"scrypted",
"seafile",
"searxng",
"semaphore",
"sonarr",
"speedtest-tracker",
"stirling-pdf",
"syncthing",
"synology",
"tailscale",
"tautulli",
"telegram",
"traefik",
"transmission",
"ubuntu",
"umami",
"unraid",
"uptime-kuma",
"vaultwarden",
"vikunja",
"wallabag",
"watchtower",
"webmin",
"wekan",
"wiki-js",
"wireguard",
"woodpecker-ci",
"wordpress",
"zabbix",
"zigbee2mqtt",
])
+2 -1
View File
@@ -198,7 +198,8 @@ function Bereichskarte({ bereich, verbindung, unterzeile, i, children }: {
}) {
const b = BEREICH[bereich]
const v = VERBINDUNG[verbindung]
const bild = geraetebild({ art: bereich === "box" ? "ki-box" : "proxmox-host", name: b.name })
// Der Kopf trägt das Zeichen des Bereichs (wie die Seitenleiste), nicht das Proxmox-Logo.
const bild = { ...geraetebild({ art: bereich === "box" ? "ki-box" : "proxmox-host", name: b.name }), logo: undefined }
return (
<section aria-label={b.name} className={cn("auftauchen flaeche relative flex min-w-0 flex-col overflow-hidden rounded-3xl", b.rand)} style={reihe(i)}>
<div aria-hidden className="pointer-events-none absolute -top-44 -left-36 size-[460px] rounded-full"