Fixes regressions from the previous audit-fixes commit (SSH host-key TOFU
race/silent-swallow, webhook durability, guest-switch exec race, dashboard
save dirty-flag misattribution) plus new findings across both frontend and
backend: AI Assistant streaming into the wrong conversation on switch,
NodeSystemPanel forms getting clobbered by background refetch, stale
ClusterPage vnet/connection selection, NaN-producing numeric form fields,
PBS oversized-response parity with the PVE client, gauge/sparkline gradient
ID collisions, PBS GC progress surviving tab switches, digest fleet-average
skew from fully-offline connections, unbounded console/SSH sessions, and
Postgres "?" rebinding corrupting literal "?" in string literals.
Also fixes three UI bugs reported directly: the dashboard meter glow being
clipped on one side, the "Available widgets" sticky label showing scrolled
content through its padding gap, and ClusterActivityWidget's generic error
message hiding the real upstream failure reason.
Separately: fixes the Release workflow's docker job hanging for GitHub's
360-minute hard cap — the web/go build stages were running under QEMU
emulation for the linux/arm64 target instead of natively, which is known to
hang Node/npm outright. Pinned both stages to --platform=$BUILDPLATFORM
(Go cross-compiles without needing to execute target-arch code) and added
a 30-minute job timeout so a real hang fails fast instead of burning hours.
Backend: gate Proxmox access/ACL routes behind requireAdmin, pin SSH host
keys with trust-on-first-use instead of ignoring them, cap concurrent
alert-notification goroutines, propagate shutdown context to live webhook
delivery, and auto-enable secure cookies when TLS is self-terminated.
Frontend: reset per-guest form/exec state when GuestDetailDialog's guest
prop swaps without closing, toast on blocked console/shell popups, retry
dashboard layout saves that fail instead of losing them silently, add
private-key auth to the ad hoc SSH dialog, add inline CIDR validation on
firewall aliases, add a stacked mobile layout for the cluster comparison
table, and assorted smaller consistency/accessibility fixes.
net/smtp.PlainAuth refuses to send credentials unless the connection is TLS
or literally "localhost" — a hardcoded stdlib guard, not something the
UseTLS toggle controls. So an admin who deliberately configured a
plaintext relay with a username/password (a trusted internal network) got
every send rejected client-side with "authenticating: unencrypted
connection" before a single byte reached the server, surfacing as a 502
from the test-notification endpoint.
sendPlain now authenticates via a small unencryptedPlainAuth (PLAIN
without the TLS-or-localhost check) instead of smtp.PlainAuth — used only
on the explicitly-no-TLS path; the STARTTLS and implicit-TLS paths keep
the normal stdlib guard. Covered by a new test against an in-process fake
SMTP server that verifies the AUTH PLAIN payload actually reaches the
server over a real, non-localhost loopback connection.
- Fleet Overview's Storage KPI and per-connection totals (internal/api/
overview.go) were summing shared storage once per node that mounts it,
since PVE's own Shared flag is unreliable for storage added per-node
without ticking it. New dedupeSharedStorage collapses those by an exact
(name, total, used) match within a connection; local-only plugin types
are never touched. Mirrored on the Storage page, which also now
disambiguates same-named local pools (e.g. every node's "local-lvm") by
node so a bar chart's category axis never gets duplicate labels.
- Sign-out: React Query keeps a query's last-successful data through a
failed refetch, so the cached signed-in user survived the post-logout
401 and the app never noticed. signOut() now pins ["auth","me"] to null
directly instead of invalidating-and-hoping a refetch lands in time.
- SMTP: net/smtp.SendMail opportunistically attempts STARTTLS whenever the
server advertises it regardless of the admin's "Use STARTTLS" toggle, so
an internal relay with a bad cert failed sends the admin explicitly asked
to be plaintext. A dedicated sendPlain path never attempts TLS.
- OIDC: added an "Auto-create new accounts" toggle — when off, a
first-time SSO login for an unrecognized identity is refused instead of
silently provisioning one. Also added RP-Initiated Logout: signing out of
an SSO session now also ends the session at the identity provider when it
advertises support for it (end_session_endpoint), via a stored id_token
on OIDC sessions (new sessions.oidc_id_token column).
- ResourceAreaChart tooltips now fall back to the same yTickFormatter given
to the axis when a series has no formatter of its own — fixes raw
unformatted numbers in Fleet Trend and every RRD chart built on it
(Node/Guest detail) that only ever set the axis formatter.
- 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).