feat: add Pokemon sprites to Proxmox nodes based on IP last octet

Each Proxmox server and VM/CT now shows a Pokemon sprite matching its
IP's last number (e.g. .3 = Venusaur, .185 = Sudowoodo). VMs also
display their IP suffix for quick identification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 02:29:03 +00:00
parent 27907a2e39
commit 34655d09e3

View File

@@ -11,17 +11,38 @@ const ICON_MAP: Record<string, string> = {
phone: "📞", camera: "📷", device: "📱", phone: "📞", camera: "📷", device: "📱",
}; };
function pokemonId(ip: string): number {
const last = parseInt(ip.split(".").pop() || "0", 10);
return last > 0 ? last : 1;
}
function PokemonSprite({ ip, size = 32 }: { ip: string; size?: number }) {
const id = pokemonId(ip);
return (
<img
src={`https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`}
alt={`#${id}`}
width={size}
height={size}
className="pixelated"
style={{ imageRendering: "pixelated" }}
/>
);
}
function VmPill({ node }: { node: NetworkNode }) { function VmPill({ node }: { node: NetworkNode }) {
const dot = node.status === "up" ? "bg-success" : "bg-danger"; const dot = node.status === "up" ? "bg-success" : "bg-danger";
return ( return (
<div className="flex items-center gap-1 bg-bg-card/80 border border-border rounded px-1.5 py-0.5"> <div className="flex items-center gap-1 bg-bg-card/80 border border-border rounded px-1.5 py-0.5">
<PokemonSprite ip={node.ip} size={20} />
<span className={`w-1.5 h-1.5 rounded-full ${dot} shrink-0`} /> <span className={`w-1.5 h-1.5 rounded-full ${dot} shrink-0`} />
<span className="text-xs text-text-primary truncate">{node.name}</span> <span className="text-xs text-text-primary truncate">{node.name}</span>
<span className="text-xs font-mono text-text-muted ml-auto shrink-0">.{node.ip.split(".").pop()}</span>
</div> </div>
); );
} }
function InfraCard({ node }: { node: NetworkNode }) { function InfraCard({ node, showPokemon }: { node: NetworkNode; showPokemon?: boolean }) {
const [showPass, setShowPass] = useState(false); const [showPass, setShowPass] = useState(false);
const isUp = node.status === "up"; const isUp = node.status === "up";
const border = isUp ? "border-success/30" : "border-danger/30"; const border = isUp ? "border-success/30" : "border-danger/30";
@@ -34,7 +55,11 @@ function InfraCard({ node }: { node: NetworkNode }) {
onClick={() => setShowPass((p) => !p)} onClick={() => setShowPass((p) => !p)}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{showPokemon ? (
<PokemonSprite ip={node.ip} size={36} />
) : (
<span className="text-lg">{ICON_MAP[node.icon] || "📦"}</span> <span className="text-lg">{ICON_MAP[node.icon] || "📦"}</span>
)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-sm font-bold text-text-primary leading-none">{node.name}</p> <p className="text-sm font-bold text-text-primary leading-none">{node.name}</p>
<p className="text-xs font-mono text-text-secondary leading-none mt-0.5">{node.ip}</p> <p className="text-xs font-mono text-text-secondary leading-none mt-0.5">{node.ip}</p>
@@ -59,7 +84,7 @@ function ProxmoxCol({ server, vms }: { server: NetworkNode; vms: NetworkNode[] }
return ( return (
<div className="flex flex-col items-center flex-1"> <div className="flex flex-col items-center flex-1">
<div className="w-px h-3 bg-border-light" /> <div className="w-px h-3 bg-border-light" />
<InfraCard node={server} /> <InfraCard node={server} showPokemon />
<div className="w-px h-2 bg-border-light" /> <div className="w-px h-2 bg-border-light" />
<span className="text-xs text-text-muted mb-1">{up}/{vms.length}</span> <span className="text-xs text-text-muted mb-1">{up}/{vms.length}</span>
<div className="grid grid-cols-3 gap-1 w-full"> <div className="grid grid-cols-3 gap-1 w-full">