Frontend build:
- combobox.tsx: DropdownMenuContent doesn't expose onOpenAutoFocus (Radix
Popper primitive, unlike Dialog/Popover) — tsc failed. Focus the search
input from an effect on `open` instead.
Backend data bugs (JSON parsing / validation):
- pve.JournalEntry: "n" is sometimes sent quoted by PVE ("n":"1"), which
failed the whole unmarshal and blanked the node Journal tab. Decoded
loosely, same as the existing cpuinfo.mhz string/number quirk.
- pve.ClusterConfigNode: pve_addr was typed int but PVE always sends a
string (an IP) — populated it 502'd Cluster & SDN > Members. Fixed to
string.
- ClusterPage: PVE also errors /cluster/config/nodes outright on a
standalone (non-clustered) node — that hit the same 502 error card
instead of the existing "not part of a cluster" message.
- ai_providers.go: updateAIProvider rejected the built-in Needle 2
provider's "needle://local" baseUrl on every save (missing the
needle.IsBuiltin bypass createAIProvider already had), so it could only
ever be disabled, never edited. Deleting it only lasted until the next
restart (the seeder always re-created it) — now tracked via a
"dismissed" flag in the settings table so a delete sticks.
UI polish:
- Added amber/rose/teal accent color presets (theme.tsx, AppearanceCard,
index.css, backend whitelist).
- StatusDot's glow was clipped on one side wherever it sat inside a
`truncate` (overflow-hidden) flex row — moved truncation to just the
text sibling (ConnectionsPage, AlertActivityWidget).
- ResourceAreaChart: a wide Y-axis tick ("47.68 MB/s") wraps onto two
lines in recharts, and the chart only had 8px of top margin — the first
line rendered off the top edge. Widened the axis gutter and margin.
- AlertActivityWidget required *both* of two independent queries to fail
before showing an error, so one broken endpoint alone rendered stale
counts instead of the error state.
- DonutChart: the hover tooltip followed the cursor by default, which on
a compact ring collided with the centered value/label text. Pinned it
below the ring instead.
- NodeDetailPage storage rows: a variable number of badges before the
usage bar made every row's bar start at a different x. Switched to a
fixed-width grid, same pattern already used for two other bar lists.
- RunningTasksWidget: a failed task's PVE status is a full sentence, not
a short word — stuffing it into a badge blew the row's layout up.
Collapsed to "Failed" with the full text as a hover title.
- Alert value/threshold formatting assumed every metric is a 0-100
percent; three built-in alerts (cert_expiry/connection_stale in days,
storage_orphan_disk in bytes) printed nonsense like "67108864.0%".
Added a shared formatAlertValue() and used it everywhere alert
instances are rendered (AlertsPage, OverviewPage, NotificationBell).
README: added a Features section summarizing the fleet management,
automation, integration, and access-control capabilities.
12 KiB
Ferrum
Fleet control for Proxmox VE — a single dashboard for every cluster and standalone node you run, with live inventory, dashboards, backups, HA, firewall, alerting, and more.
- Repository: https://github.com/anand34577/ferrum
- Backend: Go (chi router), SQLite or PostgreSQL
- Frontend: React + TypeScript, Vite, Tailwind CSS v4
Features
- Fleet-wide overview — every connection (PVE cluster, standalone node, or PBS remote) rolled up into one dashboard: node/guest counts, CPU/memory/storage, active alerts, and a drag-and-drop customizable dashboard with 20+ widgets.
- Inventory & operations — nodes, VMs, and LXCs with live consoles/shells (noVNC + xterm.js), snapshots, guest agent file browser, bulk start/stop/migrate, and cross-cluster guest migration.
- Storage & backups — pool usage and Ceph health across every connection, backup job status and replication, and PBS remote integration alongside native PVE storage.
- High availability, firewall & SDN — HA groups/resources, cluster and per-node firewall rules, and SDN zones/VNets/subnets, all per connection.
- Alerting & automation — threshold-based alerts (CPU/memory/disk/guest), config drift detection, guest lifecycle policies, capacity forecasting, a fleet health score, scheduled health-digest emails, and Terraform/Ansible inventory export.
- Integrations — outbound webhooks for real-time events, a REST API and MCP server (scoped API keys, so any MCP-capable agent or script can drive Ferrum), and a built-in AI Assistant that can use any OpenAI-compatible provider — including a zero-config local model (Needle 2) with no API key or network required.
- Access & auditing — per-user roles, optional OIDC single sign-on, session/certificate monitoring, and a full audit log of every mutating action across the UI, REST API, and MCP.
- A dozen look-and-feel presets — Enterprise, Proxmox-native, Terminal, Glass Flight Deck, Midnight, Paper, Glassmorphism, Neumorphism, Brutalist, Solarized, High Contrast, and Aurora — each with light/dark and a choice of accent colors.
Screenshots
Captured against a mock Proxmox cluster (prod-cluster: 3 nodes, 16 VMs/LXCs, Ceph + NFS storage) to show the UI populated the way it looks on a real fleet. Click any thumbnail for the full-size image.
Fleet overview |
Inventory |
Topology |
Storage |
Backups & replication |
High availability |
Firewall |
Look & feel — Enterprise / Proxmox-native / Terminal |
Connections |
First-run setup |
Getting started
go build ./cmd/ferrum
./ferrum -config config.example.yaml
The web UI is served from the same binary (see web/embed.go). For frontend development:
cd web
npm install
npm run dev
On first run, open the UI and create the initial admin account, then add a Proxmox connection (host, port, and either an API token or username/password) from Connections.
Docker
Prebuilt multi-arch (amd64/arm64) images are published to GHCR on every release:
docker run -p 8080:8080 -v ferrum-data:/app/data ghcr.io/anand34577/ferrum:latest
Or build locally from source:
docker build -t ferrum .
docker run -p 8080:8080 -v ferrum-data:/app/data ferrum
Deploying a release build
Every GitHub release ships prebuilt, statically-linked archives for Linux (amd64/arm64), Windows (amd64/arm64), and macOS (amd64/arm64) — no Go toolchain or CGO dependencies needed on the target machine. Each archive bundles the binary, config.example.yaml, and the install script for its OS.
Linux (systemd)
One-liner, same idea as get.docker.com — downloads the latest release for your architecture, verifies its checksum, and installs it as a systemd service:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/get.sh | sudo sh
Pin a specific version with FERRUM_VERSION:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/get.sh | FERRUM_VERSION=v1.2.3 sudo sh
Or do it by hand from a downloaded archive — scripts/get.sh just automates these same steps:
tar -xzf ferrum_*_linux_amd64.tar.gz
cd ferrum_*_linux_amd64
sudo ./install.sh
Either way, this creates a dedicated ferrum system user, installs the binary to /usr/local/bin/ferrum, seeds /etc/ferrum/config.yaml, and enables + starts the ferrum.service systemd unit (packaging/systemd/ferrum.service) — data lives in /var/lib/ferrum, logs go to journalctl -u ferrum -f.
To uninstall, grab scripts/linux/uninstall.sh and run it as root — add -- --purge to also remove config/data:
curl -fsSL https://raw.githubusercontent.com/anand34577/ferrum/main/scripts/linux/uninstall.sh | sudo bash -s --
Windows (Windows Service)
Expand-Archive ferrum_*_windows_amd64.zip
cd ferrum_*_windows_amd64
.\install-service.ps1 # run as Administrator
This installs the binary to %ProgramFiles%\Ferrum, seeds %ProgramData%\Ferrum\config.yaml, and registers a "Ferrum" Windows service — ferrum.exe detects it's running under the Service Control Manager and manages its own start/stop lifecycle, no wrapper (NSSM etc.) needed. Since Windows services don't capture stdout/stderr the way systemd does, logs are written to %ProgramData%\Ferrum\ferrum.log. Uninstall with .\uninstall-service.ps1 (add -Purge to also remove config/data).
Building from source
scripts/build.sh # current platform only, output in dist/
scripts/build.sh linux/amd64 windows/amd64
scripts/build.sh all # every platform the release workflow builds
.\scripts\build.ps1 # Windows-native equivalent, current platform only
Both build the frontend, cross-compile with version info baked in (ferrum -version), and package each target as a .tar.gz/.zip with its install script — the same thing .github/workflows/release.yml runs when a vX.Y.Z tag is pushed, publishing the resulting archives (and a checksums.txt) as GitHub Release assets.
Configuration
Copy config.example.yaml to config.yaml and adjust as needed, or set the equivalent FERRUM_* environment variables (see the comments in that file for the full list, including SQLite/PostgreSQL, TLS cookie behavior, reverse-proxy support, and optional OIDC single sign-on).
Everything else — notifications, SSO details, security policy, system settings, AI providers, and the REST API/MCP enable switches below — is configured from the admin Settings UI once Ferrum is running, not from environment variables.
Built-in LLM (Needle 2)
The AI Assistant and MCP tool-calling loop can use any OpenAI-chat-completions-compatible provider (OpenAI, Ollama, LM Studio, LocalAI, OpenRouter, ...) configured under Settings > AI Providers. There's also an optional zero-config, no-API-key, fully local option backed by Needle 2 — a small (45M-parameter) tool-calling model that runs as a self-contained CLI binary with no GPU and no network access required at inference time.
Needle 2 is Apache-2.0 licensed, so on Windows, Linux, and macOS (amd64 or arm64) Ferrum ships its official CLI binary baked into the ferrum binary itself (internal/needle/bundled_*.go, one per platform via go:embed) — nothing to download, nothing to configure. On a fresh install (no AI provider configured yet), Ferrum extracts it to a cache file and registers it automatically as the default assistant the first time it starts — no manual "Add provider" step needed. If you've already configured a provider, or want to add/re-add it yourself, use Settings > AI Providers > "Add provider" > the Needle 2 (built-in, local) preset.
On any other platform (32-bit, RISC-V, Windows/ARM64, ...) there's no bundled binary — Ferrum still never fetches executable content from the network on its own. To enable it there:
- Download the
needleCLI binary for your platform from the Needle 2 files. - Point Ferrum at it: set
FERRUM_NEEDLE_BIN=/path/to/needle(orneedleBinPathinconfig.yaml) before starting Ferrum. This also overrides the bundled binary on a supported platform, if you'd rather run a different build.
Ferrum starts the binary itself (as a local subprocess, 127.0.0.1-only) the first time it's used, and stops it on shutdown. If no binary is bundled for the platform and FERRUM_NEEDLE_BIN isn't set (or doesn't exist), this provider simply isn't usable — every other provider is unaffected.
Troubleshooting on small ARM boards (Raspberry Pi and similar SBCs): Needle does a one-time "tool retrieval" pass over Ferrum's full tool catalog (~60 tools) the first time it's used, which briefly uses noticeably more memory and CPU than steady-state chat — Ferrum already waits up to 60s for that first startup. On a board with very little RAM this pass can get the subprocess killed by the kernel's OOM-killer instead; the error will say needle process exited: signal: killed. If you hit that, set FERRUM_NEEDLE_MAX_TOOLS=<n> to cap how many tools Needle is given (e.g. 10 keeps just the original read-only lookup tools), trading away the newer create/manage tools for a much lighter embedding pass. If it still fails, the board likely doesn't have enough RAM to run even this small a model reliably — use a real OpenAI-compatible provider instead (a cheap/free hosted API, or Ollama on a separate, beefier machine).
API access, MCP, and audit logging
Any user can generate long-lived API keys under Profile > API Keys — scoped to either the general REST API (Authorization: Bearer <key> against /api/v1/..., for 3rd-party integrations and scripts) or the MCP endpoint only (/mcp, for Claude Code/Desktop or any other MCP-capable agent — see Profile > MCP integration for ready-to-paste config). Both surfaces are off by default and must be turned on by an admin under Settings > API & MCP, which also caps how many tool calls the AI Assistant's agent loop can make per message. Every mutating action — through the UI, the REST API, or MCP — is recorded with who, what, and when under Audit Log (admin-only).









