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:
co-authored by
Claude Opus 5.5
parent
9d56de457d
commit
d524e03c68
@@ -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])} />
|
||||
)}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
])
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user