Add guest URL support for PVE hosts
Related to discussion #615 Add optional GuestURL field to PVE instances and cluster endpoints, allowing users to specify a separate guest-accessible URL for web UI navigation that differs from the internal management URL. Backend changes: - Add GuestURL field to PVEInstance and ClusterEndpoint structs - Add GuestURL field to Node model - Update cluster auto-discovery to preserve existing GuestURL values - Update node creation logic to populate GuestURL from config - Update API handlers to accept and persist GuestURL field Frontend changes: - Add GuestURL input field to NodeModal for configuration - Update NodeGroupHeader and NodeSummaryTable to use GuestURL for navigation - Add GuestURL to Node and PVENodeConfig TypeScript interfaces When GuestURL is configured, it will be used for navigation links instead of the Host URL, allowing users to access PVE hosts through a reverse proxy or different domain while maintaining internal API connections.
This commit is contained in:
parent
3194b10398
commit
b1831d7b3e
9 changed files with 66 additions and 10 deletions
|
|
@ -65,6 +65,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
|||
const getCleanFormData = (nodeType: 'pve' | 'pbs' | 'pmg' = props.nodeType) => ({
|
||||
name: '',
|
||||
host: '',
|
||||
guestURL: '',
|
||||
authType: nodeType === 'pmg' ? 'password' : ('token' as 'password' | 'token'),
|
||||
setupMode: 'auto' as 'auto' | 'manual',
|
||||
user: '',
|
||||
|
|
@ -172,6 +173,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
|||
setFormData({
|
||||
name: node.name || '',
|
||||
host: node.host || '',
|
||||
guestURL: ('guestURL' in node ? node.guestURL : '') || '',
|
||||
authType: node.hasPassword ? 'password' : 'token',
|
||||
setupMode: 'auto',
|
||||
user: username,
|
||||
|
|
@ -211,6 +213,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
|||
type: props.nodeType,
|
||||
name: normalizedName,
|
||||
host: data.host,
|
||||
guestURL: data.guestURL,
|
||||
fingerprint: data.fingerprint,
|
||||
verifySSL: data.verifySSL,
|
||||
};
|
||||
|
|
@ -489,6 +492,28 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
|||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={formField}>
|
||||
<label class={labelClass('flex items-center gap-1')}>
|
||||
Guest URL <span class="text-gray-500 text-xs font-normal">(Optional)</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData().guestURL}
|
||||
onInput={(e) => updateField('guestURL', e.currentTarget.value)}
|
||||
placeholder={
|
||||
props.nodeType === 'pve'
|
||||
? 'https://pve.yourdomain.com'
|
||||
: props.nodeType === 'pbs'
|
||||
? 'https://pbs.yourdomain.com'
|
||||
: 'https://pmg.yourdomain.com'
|
||||
}
|
||||
class={controlClass()}
|
||||
/>
|
||||
<p class={formHelpText}>
|
||||
Optional guest-accessible URL for navigation. If specified, this URL will be used when opening the web UI instead of the Host URL.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface NodeGroupHeaderProps {
|
|||
|
||||
export const NodeGroupHeader: Component<NodeGroupHeaderProps> = (props) => {
|
||||
const isOnline = () => props.node.status === 'online' && (props.node.uptime || 0) > 0;
|
||||
const nodeUrl = () => props.node.host || `https://${props.node.name}:8006`;
|
||||
const nodeUrl = () => props.node.guestURL || props.node.host || `https://${props.node.name}:8006`;
|
||||
const displayName = () => getNodeDisplayName(props.node);
|
||||
const showActualName = () => hasAlternateDisplayName(props.node);
|
||||
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
|
|||
<a
|
||||
href={
|
||||
isPVE
|
||||
? node!.host || `https://${node!.name}:8006`
|
||||
? node!.guestURL || node!.host || `https://${node!.name}:8006`
|
||||
: pbs!.host || `https://${pbs!.name}:8007`
|
||||
}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface Node {
|
|||
displayName?: string;
|
||||
instance: string;
|
||||
host: string;
|
||||
guestURL?: string; // Optional guest-accessible URL (for navigation)
|
||||
status: string;
|
||||
type: string;
|
||||
cpu: number;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface ClusterEndpoint {
|
|||
NodeID: string;
|
||||
NodeName: string;
|
||||
Host: string;
|
||||
GuestURL?: string;
|
||||
IP: string;
|
||||
Online: boolean;
|
||||
LastSeen: string;
|
||||
|
|
@ -15,6 +16,7 @@ export interface PVENodeConfig {
|
|||
id: string;
|
||||
name: string;
|
||||
host: string;
|
||||
guestURL?: string;
|
||||
user: string;
|
||||
hasPassword?: boolean;
|
||||
hasToken?: boolean;
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ func (h *ConfigHandlers) maybeRefreshClusterInfo(instance *config.PVEInstance) {
|
|||
h.clusterDetectMutex.Unlock()
|
||||
|
||||
clientConfig := config.CreateProxmoxConfig(instance)
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, instance.Name)
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, instance.Name, instance.ClusterEndpoints)
|
||||
if !isCluster || len(clusterEndpoints) == 0 {
|
||||
log.Debug().
|
||||
Str("instance", instance.Name).
|
||||
|
|
@ -366,6 +366,7 @@ type NodeConfigRequest struct {
|
|||
Type string `json:"type"` // "pve", "pbs", or "pmg"
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
GuestURL string `json:"guestURL,omitempty"` // Optional guest-accessible URL (for navigation)
|
||||
User string `json:"user,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
|
|
@ -395,6 +396,7 @@ type NodeResponse struct {
|
|||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
GuestURL string `json:"guestURL,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
HasPassword bool `json:"hasPassword"`
|
||||
TokenName string `json:"tokenName,omitempty"`
|
||||
|
|
@ -569,8 +571,19 @@ func validateNodeAPI(clusterNode proxmox.ClusterStatus, baseConfig proxmox.Clien
|
|||
return true
|
||||
}
|
||||
|
||||
// findExistingGuestURL looks up the GuestURL for a node from existing endpoints
|
||||
func findExistingGuestURL(nodeName string, existingEndpoints []config.ClusterEndpoint) string {
|
||||
for _, ep := range existingEndpoints {
|
||||
if ep.NodeName == nodeName {
|
||||
return ep.GuestURL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// detectPVECluster checks if a PVE node is part of a cluster and returns cluster information
|
||||
func detectPVECluster(clientConfig proxmox.ClientConfig, nodeName string) (isCluster bool, clusterName string, clusterEndpoints []config.ClusterEndpoint) {
|
||||
// If existingEndpoints is provided, GuestURL values will be preserved for matching nodes
|
||||
func detectPVECluster(clientConfig proxmox.ClientConfig, nodeName string, existingEndpoints []config.ClusterEndpoint) (isCluster bool, clusterName string, clusterEndpoints []config.ClusterEndpoint) {
|
||||
tempClient, err := proxmox.NewClient(clientConfig)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to create client for cluster detection")
|
||||
|
|
@ -664,6 +677,7 @@ func detectPVECluster(clientConfig proxmox.ClientConfig, nodeName string) (isClu
|
|||
NodeID: clusterNode.ID,
|
||||
NodeName: clusterNode.Name,
|
||||
Host: schemePrefix + nodeHost,
|
||||
GuestURL: findExistingGuestURL(clusterNode.Name, existingEndpoints),
|
||||
Online: clusterNode.Online == 1,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
|
@ -695,6 +709,7 @@ func detectPVECluster(clientConfig proxmox.ClientConfig, nodeName string) (isClu
|
|||
NodeID: clusterNode.ID,
|
||||
NodeName: clusterNode.Name,
|
||||
Host: schemePrefix + nodeHost,
|
||||
GuestURL: findExistingGuestURL(clusterNode.Name, existingEndpoints),
|
||||
Online: clusterNode.Online == 1,
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
|
|
@ -737,6 +752,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
|
|||
Type: "pve",
|
||||
Name: pve.Name,
|
||||
Host: pve.Host,
|
||||
GuestURL: pve.GuestURL,
|
||||
User: pve.User,
|
||||
HasPassword: pve.Password != "",
|
||||
TokenName: pve.TokenName,
|
||||
|
|
@ -1152,7 +1168,7 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
|
|||
verifySSL = *req.VerifySSL
|
||||
}
|
||||
clientConfig := config.CreateProxmoxConfigFromFields(host, req.User, req.Password, req.TokenName, req.TokenValue, req.Fingerprint, verifySSL)
|
||||
isCluster, clusterName, clusterEndpoints = detectPVECluster(clientConfig, req.Name)
|
||||
isCluster, clusterName, clusterEndpoints = detectPVECluster(clientConfig, req.Name, nil)
|
||||
}
|
||||
|
||||
if isCluster {
|
||||
|
|
@ -1187,6 +1203,7 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
|
|||
pve := config.PVEInstance{
|
||||
Name: req.Name,
|
||||
Host: host, // Use normalized host
|
||||
GuestURL: req.GuestURL,
|
||||
User: req.User,
|
||||
Password: req.Password,
|
||||
TokenName: req.TokenName,
|
||||
|
|
@ -1534,7 +1551,7 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
|
|||
return
|
||||
}
|
||||
|
||||
isCluster, _, clusterEndpoints := detectPVECluster(clientConfig, req.Name)
|
||||
isCluster, _, clusterEndpoints := detectPVECluster(clientConfig, req.Name, nil)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": "success",
|
||||
|
|
@ -1810,6 +1827,9 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
|
|||
pve.Host = host
|
||||
}
|
||||
|
||||
// Update GuestURL if provided
|
||||
pve.GuestURL = req.GuestURL
|
||||
|
||||
// Handle authentication updates - only switch auth method if explicitly provided
|
||||
if req.TokenName != "" || req.TokenValue != "" {
|
||||
// Switching to or updating token authentication
|
||||
|
|
@ -5414,7 +5434,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
|
|||
VerifySSL: instance.VerifySSL,
|
||||
}
|
||||
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, instance.Name)
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, instance.Name, instance.ClusterEndpoints)
|
||||
if isCluster {
|
||||
instance.IsCluster = true
|
||||
instance.ClusterName = clusterName
|
||||
|
|
@ -5456,7 +5476,7 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
|
|||
VerifySSL: verifySSL,
|
||||
}
|
||||
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, nodeConfig.Name)
|
||||
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, nodeConfig.Name, nil)
|
||||
|
||||
monitorVMs := true
|
||||
if nodeConfig.MonitorVMs != nil {
|
||||
|
|
|
|||
|
|
@ -388,7 +388,8 @@ func splitAndTrim(value string) []string {
|
|||
// PVEInstance represents a Proxmox VE connection
|
||||
type PVEInstance struct {
|
||||
Name string
|
||||
Host string // Primary endpoint (user-provided)
|
||||
Host string // Primary endpoint (user-provided)
|
||||
GuestURL string // Optional guest-accessible URL (for navigation)
|
||||
User string
|
||||
Password string
|
||||
TokenName string
|
||||
|
|
@ -414,6 +415,7 @@ type ClusterEndpoint struct {
|
|||
NodeID string // Node ID in cluster
|
||||
NodeName string // Node name
|
||||
Host string // Full URL (e.g., https://node1.lan:8006)
|
||||
GuestURL string // Optional guest-accessible URL (for navigation)
|
||||
IP string // IP address
|
||||
Online bool // Current online status
|
||||
LastSeen time.Time // Last successful connection
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ type Node struct {
|
|||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Instance string `json:"instance"`
|
||||
Host string `json:"host"` // Full host URL from config
|
||||
Host string `json:"host"` // Full host URL from config
|
||||
GuestURL string `json:"guestURL"` // Optional guest-accessible URL (for navigation)
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
CPU float64 `json:"cpu"`
|
||||
|
|
|
|||
|
|
@ -4885,12 +4885,16 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
|||
nodeStart := time.Now()
|
||||
displayName := getNodeDisplayName(instanceCfg, node.Node)
|
||||
connectionHost := instanceCfg.Host
|
||||
guestURL := instanceCfg.GuestURL
|
||||
if instanceCfg.IsCluster && len(instanceCfg.ClusterEndpoints) > 0 {
|
||||
for _, ep := range instanceCfg.ClusterEndpoints {
|
||||
if strings.EqualFold(ep.NodeName, node.Node) {
|
||||
if effective := clusterEndpointEffectiveURL(ep); effective != "" {
|
||||
connectionHost = effective
|
||||
}
|
||||
if ep.GuestURL != "" {
|
||||
guestURL = ep.GuestURL
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -4939,6 +4943,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
|
|||
DisplayName: displayName,
|
||||
Instance: instanceName,
|
||||
Host: connectionHost,
|
||||
GuestURL: guestURL,
|
||||
Status: effectiveStatus,
|
||||
Type: "node",
|
||||
CPU: safeFloat(node.CPU), // Already in percentage
|
||||
|
|
|
|||
Loading…
Reference in a new issue