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.
- User-scoped API keys (Profile > API Keys) for 3rd-party REST API access
and MCP clients, each locked to one scope at creation, with expiry,
revocation, and last-used tracking.
- A hand-rolled MCP (Model Context Protocol) server exposing the fleet
(connections, nodes, guests, storage, pools, alerts, cluster status) as
read tools plus one admin-gated power-action tool, so Claude Code/Desktop
or any other MCP client can query and operate the fleet directly.
- Both the REST API and MCP are off by default and toggleable instance-wide
from Settings > API & MCP, enforced live on every request.
- Admin-managed AI providers (any OpenAI-chat-completions-compatible
endpoint) backing the AI Assistant's tool-calling loop, replacing the
single hardcoded provider.
- A built-in, zero-config, no-API-key local provider backed by Needle 2
(internal/needle) for fully offline tool-calling, wired in as a one-click
preset. Requires the operator to separately download the Needle 2 binary
and point FERRUM_NEEDLE_BIN at it -- Ferrum never fetches executable
content from the network itself; see README "Built-in LLM (Needle 2)".
- System settings (CORS allow-list, instance-wide toggles) moved to the
admin Settings UI; environment variables are now scoped to true
bootstrap-level config only (listen address, TLS, DB connection, secret,
optional Needle binary path).
- Fixed: node Journal tab 502'ing with "unexpected end of JSON input" on an
empty response, and separately with a decode error on PVE versions that
return a bare-string journal line instead of the documented {n,t} object.
- Fixed: bottom content padding disappearing on every page except the AI
Assistant (an unconditional h-full on the content wrapper let overflowing
content bleed through where the padding should render).
- Fixed: Profile page felt cramped despite a wide viewport (stray max-w-2xl
cap not present on the equivalent Settings page).
- Test coverage added for the previously-untested MCP package and the new
Needle adapter (20 new Go tests), plus a regression test for the journal
decode fix.
- Default all queries to a 20s poll + refetch-on-focus (main.tsx) instead of
a per-page opt-in, so every page/widget stays live without manual tuning.
- New internal/notify package: Gotify and SMTP (stdlib net/smtp, STARTTLS
and implicit-TLS-on-465) notifications, each independently optional. Fires
from the alert evaluator on new alert triggers; admin-configurable from
Settings with a send-test-notification action per channel.
- OIDC/SSO moved from config.yaml-only to a DB-backed, admin-editable
Settings card — swaps the live client with no restart. config.yaml is
used to seed the database once on first boot after upgrading.
- New Security settings: session TTL, login lockout policy, and a real
"require 2FA for admins" enforcement (requireTOTPEnrolled middleware)
that blocks non-enrolled admins from everything but /profile and logout.
- New org-wide default preferences (theme/accent/look/landing page) for
brand-new accounts, plus a personal landing-page picker and an
email-me-alerts opt-in on Profile.
- Storage page: separate Local vs Shared/External storage tables and
capacity donuts, fixing shared-storage totals that were being summed once
per node that mounts them (e.g. a 2TB NFS share on 4 nodes read as 8TB).
- RankedBarChart: stop the longest bar's value label wrapping onto two
lines (recharts auto-wraps LabelList when space is tight).
- Add glassmorphism, neumorphism, and brutalist looks (backend validation,
theme.tsx types, AppearanceCard picker) alongside the existing six.
- Give looks real structural knobs, not just color: sidebar width/border,
header height/blur are now CSS vars AppShell reads, and card shadow/hover
states are fully token-driven instead of a hardcoded dark-mode override.
- Sidebar nav active state drops its hard box border on glass/neumorphic
looks (glow pill / inset shadow) instead of always drawing a box.
- Fix RankedBarChart's value label wrapping onto two lines on the longest
bar (recharts auto-wraps LabelList when space gets tight) by rendering
it as a plain unconstrained <text> with a wider right margin.
The GitHub merge of feature/glass-flight-deck and feature/theme-selector
left :root/.dark (the "no [data-look] override" baseline) set to Glass
Flight Deck's tokens instead of the plain Enterprise baseline, so every
look inherited its Oswald type, tight radii, persistent caution bar, and
corner brackets regardless of which one was selected — the themes were
indistinguishable because they were all secretly the same rejected design
underneath. Restores :root/.dark to a clean Enterprise baseline and moves
every Glass-Flight-Deck-specific token into its own explicit
[data-look="glassFlightDeck"] block, same as the others.
Also makes .panel-label's tracked-caps transform and .corner-frame's
visible border look-gated (glassFlightDeck only) instead of global, and
MasterCautionBar look-aware (persistent nameplate only in Glass Flight
Deck; a conventional reactive banner — hidden when nominal — everywhere
else), so no look can leak another look's structural signature.
Expands the six-value Look enum/backend whitelist with two new designs:
- Midnight: modern dev-tool dark (Nord/GitHub-dark-adjacent), cool
blue-slate ground with a visible glow instead of a hard shadow.
- Paper: ultra-minimal and warm, off-white ground, zero shadow, hairline
borders only, with a genuinely warm (not blue-black) dark variant.
All six verified live end-to-end (build/typecheck clean, each rendered and
screenshotted in both themes where applicable).
Adds a whole-app visual-register switch, orthogonal to the light/dark
toggle and accent color, via a new [data-look] token layer in index.css:
- Enterprise (default): unchanged clean SaaS baseline.
- Proxmox-native: utilitarian and dense — plain system font, smaller root
scale, zero radius, no shadows, flat bordered panels.
- Terminal: quiet and precise — the whole UI (not just data) sets in
monospace, no display face, flat near-zero-radius panels.
Persisted server-side alongside theme/accent (new `look` column on
user_preferences, migration 00008) with a live-scoped preview per option
in Settings > Appearance.