feat: make PVE polling interval configurable (related to #467)
This commit is contained in:
parent
274c45799f
commit
3f46d35a81
13 changed files with 357 additions and 69 deletions
|
|
@ -396,7 +396,7 @@ The UI detects your deployment type and reminds you which command sequence to ru
|
|||
|
||||
Quick start - most settings are in the web UI:
|
||||
- **Settings → Nodes**: Add/remove Proxmox instances
|
||||
- **Settings → System**: Polling intervals, timeouts, update settings
|
||||
- **Settings → System**: Monitor cadence (PVE polling), timeouts, update settings
|
||||
- **Settings → Security**: Authentication and API tokens
|
||||
- **Alerts**: Thresholds and notifications
|
||||
|
||||
|
|
|
|||
|
|
@ -252,22 +252,14 @@ func runServer() {
|
|||
configWatcher.ReloadConfig()
|
||||
}
|
||||
|
||||
// Reload system.json
|
||||
persistence := config.NewConfigPersistence(cfg.DataPath)
|
||||
if persistence != nil {
|
||||
if _, err := persistence.LoadSystemSettings(); err == nil {
|
||||
// Note: Polling interval is now hardcoded to 10s, no longer configurable
|
||||
// Could reload other system.json settings here
|
||||
log.Info().Msg("Reloaded system configuration")
|
||||
if reloadFunc != nil {
|
||||
if err := reloadFunc(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to reload monitor after SIGHUP")
|
||||
} else {
|
||||
log.Error().Err(err).Msg("Failed to reload system.json")
|
||||
log.Info().Msg("Runtime configuration reloaded")
|
||||
}
|
||||
}
|
||||
|
||||
// Could reload other configs here (alerts.json, webhooks.json, etc.)
|
||||
|
||||
log.Info().Msg("Configuration reload complete")
|
||||
|
||||
case <-sigChan:
|
||||
log.Info().Msg("Shutting down server...")
|
||||
goto shutdown
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout
|
|||
**Contents:**
|
||||
```json
|
||||
{
|
||||
"pbsPollingInterval": 60, // Seconds between PBS refreshes (PVE polling fixed at 10s)
|
||||
"pvePollingInterval": 10, // Seconds between PVE refreshes (10s default, 10–3600 allowed)
|
||||
"pbsPollingInterval": 60, // Seconds between PBS refreshes
|
||||
"pmgPollingInterval": 60, // Seconds between PMG refreshes (mail analytics and health)
|
||||
"connectionTimeout": 60, // Seconds before node connection timeout
|
||||
"autoUpdateEnabled": false, // Systemd timer toggle for automatic updates
|
||||
|
|
@ -142,6 +143,7 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout
|
|||
- Can be safely backed up without exposing secrets
|
||||
- Missing file results in defaults being used
|
||||
- Changes take effect immediately (no restart required)
|
||||
- `pvePollingInterval` controls how often Pulse polls all Proxmox VE nodes. Configure it in **Settings → System → General → Monitoring cadence** (10–3600 seconds).
|
||||
- API tokens are no longer managed in system.json (moved to .env in v4.3.9+)
|
||||
- **Adaptive polling controls** (`adaptivePollingEnabled`, `adaptivePolling*Interval`) map directly to the Scheduler Health API and adjust queue/backoff behaviour in real time.
|
||||
- **Runtime logging controls** (`logLevel`, `logFormat`, `logFile`, `logMaxSize`, `logMaxAge`, `logCompress`) can be tuned from the UI or system.json; updates are applied immediately so you can raise verbosity, switch to structured JSON, or stream logs to disk without restarting Pulse.
|
||||
|
|
|
|||
|
|
@ -406,6 +406,17 @@ const BACKUP_INTERVAL_OPTIONS = [
|
|||
|
||||
const BACKUP_INTERVAL_MAX_MINUTES = 7 * 24 * 60; // 7 days
|
||||
|
||||
const PVE_POLLING_MIN_SECONDS = 10;
|
||||
const PVE_POLLING_MAX_SECONDS = 3600;
|
||||
const PVE_POLLING_PRESETS = [
|
||||
{ label: '10 seconds (default)', value: 10 },
|
||||
{ label: '15 seconds', value: 15 },
|
||||
{ label: '30 seconds', value: 30 },
|
||||
{ label: '60 seconds', value: 60 },
|
||||
{ label: '2 minutes', value: 120 },
|
||||
{ label: '5 minutes', value: 300 },
|
||||
];
|
||||
|
||||
// Node with UI-specific fields
|
||||
type NodeConfigWithStatus = NodeConfig & {
|
||||
hasPassword?: boolean;
|
||||
|
|
@ -610,7 +621,11 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
const pmgNodes = createMemo(() => nodes().filter((n) => n.type === 'pmg'));
|
||||
|
||||
// System settings
|
||||
// PBS polling interval removed - fixed at 10 seconds
|
||||
const [pvePollingInterval, setPVEPollingInterval] = createSignal<number>(PVE_POLLING_MIN_SECONDS);
|
||||
const [pvePollingSelection, setPVEPollingSelection] = createSignal<number | 'custom'>(
|
||||
PVE_POLLING_MIN_SECONDS,
|
||||
);
|
||||
const [pvePollingCustomSeconds, setPVEPollingCustomSeconds] = createSignal(30);
|
||||
const [allowedOrigins, setAllowedOrigins] = createSignal('*');
|
||||
const [discoveryEnabled, setDiscoveryEnabled] = createSignal(false);
|
||||
const [discoverySubnet, setDiscoverySubnet] = createSignal('auto');
|
||||
|
|
@ -627,6 +642,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
Boolean(
|
||||
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
|
||||
);
|
||||
const pvePollingEnvLocked = () =>
|
||||
Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL);
|
||||
let discoverySubnetInputRef: HTMLInputElement | undefined;
|
||||
|
||||
const parseSubnetList = (value: string) => {
|
||||
|
|
@ -1912,7 +1929,22 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
// Load system settings
|
||||
try {
|
||||
const systemSettings = await SettingsAPI.getSystemSettings();
|
||||
// PBS polling interval is now fixed at 10 seconds
|
||||
const rawPVESecs =
|
||||
typeof systemSettings.pvePollingInterval === 'number'
|
||||
? Math.round(systemSettings.pvePollingInterval)
|
||||
: PVE_POLLING_MIN_SECONDS;
|
||||
const clampedPVESecs = Math.min(
|
||||
PVE_POLLING_MAX_SECONDS,
|
||||
Math.max(PVE_POLLING_MIN_SECONDS, rawPVESecs),
|
||||
);
|
||||
setPVEPollingInterval(clampedPVESecs);
|
||||
const presetMatch = PVE_POLLING_PRESETS.find((opt) => opt.value === clampedPVESecs);
|
||||
if (presetMatch) {
|
||||
setPVEPollingSelection(presetMatch.value);
|
||||
} else {
|
||||
setPVEPollingSelection('custom');
|
||||
setPVEPollingCustomSeconds(clampedPVESecs);
|
||||
}
|
||||
setAllowedOrigins(systemSettings.allowedOrigins || '*');
|
||||
// Connection timeout is backend-only
|
||||
// Load discovery settings (default to false when unset)
|
||||
|
|
@ -2027,7 +2059,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
) {
|
||||
// Save system settings using typed API
|
||||
await SettingsAPI.updateSystemSettings({
|
||||
// PBS polling interval is now fixed at 10 seconds
|
||||
pvePollingInterval: pvePollingInterval(),
|
||||
allowedOrigins: allowedOrigins(),
|
||||
// Connection timeout is backend-only
|
||||
// Discovery settings are saved immediately on toggle
|
||||
|
|
@ -3523,6 +3555,133 @@ const Settings: Component<SettingsProps> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
padding="none"
|
||||
class="overflow-hidden border border-gray-200 dark:border-gray-700"
|
||||
border={false}
|
||||
>
|
||||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-blue-100 dark:bg-blue-900/40 rounded-lg">
|
||||
<Activity class="w-5 h-5 text-blue-600 dark:text-blue-300" strokeWidth={2} />
|
||||
</div>
|
||||
<SectionHeader
|
||||
title="Monitoring cadence"
|
||||
description="Control how frequently Pulse polls Proxmox VE nodes."
|
||||
size="sm"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 space-y-5">
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Shorter intervals provide near-real-time updates at the cost of higher API
|
||||
and CPU usage on each node. Set a longer interval to reduce load on busy
|
||||
clusters.
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Current cadence: {pvePollingInterval()} seconds (
|
||||
{pvePollingInterval() >= 60
|
||||
? `${(pvePollingInterval() / 60).toFixed(
|
||||
pvePollingInterval() % 60 === 0 ? 0 : 1,
|
||||
)} minute${pvePollingInterval() / 60 === 1 ? '' : 's'}`
|
||||
: 'under a minute'}
|
||||
).
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-2 sm:grid-cols-3">
|
||||
<For each={PVE_POLLING_PRESETS}>
|
||||
{(option) => (
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
|
||||
pvePollingSelection() === option.value
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/30 dark:text-blue-100'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-900/50 dark:text-gray-200'
|
||||
} ${pvePollingEnvLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
disabled={pvePollingEnvLocked()}
|
||||
onClick={() => {
|
||||
if (pvePollingEnvLocked()) return;
|
||||
setPVEPollingSelection(option.value);
|
||||
setPVEPollingInterval(option.value);
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<button
|
||||
type="button"
|
||||
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
|
||||
pvePollingSelection() === 'custom'
|
||||
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/30 dark:text-blue-100'
|
||||
: 'border-gray-200 bg-white text-gray-700 hover:border-blue-400 hover:text-blue-600 dark:border-gray-600 dark:bg-gray-900/50 dark:text-gray-200'
|
||||
} ${pvePollingEnvLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
||||
disabled={pvePollingEnvLocked()}
|
||||
onClick={() => {
|
||||
if (pvePollingEnvLocked()) return;
|
||||
setPVEPollingSelection('custom');
|
||||
setPVEPollingInterval(pvePollingCustomSeconds());
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
>
|
||||
Custom interval
|
||||
</button>
|
||||
</div>
|
||||
<Show when={pvePollingSelection() === 'custom'}>
|
||||
<div class="space-y-2 rounded-md border border-dashed border-gray-300 p-4 dark:border-gray-600">
|
||||
<label class="text-xs font-medium text-gray-700 dark:text-gray-200">
|
||||
Custom polling interval (10–3600 seconds)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={PVE_POLLING_MIN_SECONDS}
|
||||
max={PVE_POLLING_MAX_SECONDS}
|
||||
value={pvePollingCustomSeconds()}
|
||||
class="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-900/60"
|
||||
disabled={pvePollingEnvLocked()}
|
||||
onInput={(e) => {
|
||||
if (pvePollingEnvLocked()) return;
|
||||
const parsed = Math.floor(Number(e.currentTarget.value));
|
||||
if (Number.isNaN(parsed)) {
|
||||
return;
|
||||
}
|
||||
const clamped = Math.min(
|
||||
PVE_POLLING_MAX_SECONDS,
|
||||
Math.max(PVE_POLLING_MIN_SECONDS, parsed),
|
||||
);
|
||||
setPVEPollingCustomSeconds(clamped);
|
||||
setPVEPollingInterval(clamped);
|
||||
setHasUnsavedChanges(true);
|
||||
}}
|
||||
/>
|
||||
<p class="text-[0.68rem] text-gray-500 dark:text-gray-400">
|
||||
Applies to all PVE clusters and standalone nodes.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={pvePollingEnvLocked()}>
|
||||
<div class="flex items-center gap-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800 dark:border-amber-800 dark:bg-amber-900/30 dark:text-amber-200">
|
||||
<svg
|
||||
class="h-4 w-4 flex-shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<circle cx="12" cy="16" r="0.5" />
|
||||
</svg>
|
||||
<span>Managed via environment variable PVE_POLLING_INTERVAL.</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface AuthConfig {
|
|||
* These are application behavior settings
|
||||
*/
|
||||
export interface SystemConfig {
|
||||
// Note: PVE polling is hardcoded to 10s (Proxmox cluster/resources updates every 10s)
|
||||
pvePollingInterval?: number; // PVE polling interval in seconds
|
||||
pbsPollingInterval?: number; // PBS polling interval in seconds
|
||||
connectionTimeout?: number; // Seconds before timeout (default: 10)
|
||||
autoUpdateEnabled: boolean; // Enable auto-updates
|
||||
|
|
|
|||
|
|
@ -2876,6 +2876,7 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
|||
|
||||
// Get current values from running config
|
||||
settings := *persistedSettings
|
||||
settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds())
|
||||
settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds())
|
||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||
settings.BackendPort = h.config.BackendPort
|
||||
|
|
|
|||
|
|
@ -977,7 +977,7 @@ func (r *Router) setupRoutes() {
|
|||
}))
|
||||
|
||||
// System settings and API token management
|
||||
r.systemSettingsHandler = NewSystemSettingsHandler(r.config, r.persistence, r.wsHub, r.monitor, r.reloadSystemSettings)
|
||||
r.systemSettingsHandler = NewSystemSettingsHandler(r.config, r.persistence, r.wsHub, r.monitor, r.reloadSystemSettings, r.reloadFunc)
|
||||
r.mux.HandleFunc("/api/system/settings", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.systemSettingsHandler.HandleGetSystemSettings)))
|
||||
r.mux.HandleFunc("/api/system/settings/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.systemSettingsHandler.HandleUpdateSystemSettings)))
|
||||
r.mux.HandleFunc("/api/system/ssh-config", r.handleSSHConfig)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type SystemSettingsHandler struct {
|
|||
persistence *config.ConfigPersistence
|
||||
wsHub *websocket.Hub
|
||||
reloadSystemSettingsFunc func() // Function to reload cached system settings
|
||||
reloadMonitorFunc func() error
|
||||
monitor interface {
|
||||
GetDiscoveryService() *discovery.Service
|
||||
StartDiscoveryService(ctx context.Context, wsHub *websocket.Hub, subnet string)
|
||||
|
|
@ -48,13 +49,14 @@ func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPers
|
|||
DisableTemperatureMonitoring()
|
||||
GetNotificationManager() *notifications.NotificationManager
|
||||
HasSocketTemperatureProxy() bool
|
||||
}, reloadSystemSettingsFunc func()) *SystemSettingsHandler {
|
||||
}, reloadSystemSettingsFunc func(), reloadMonitorFunc func() error) *SystemSettingsHandler {
|
||||
return &SystemSettingsHandler{
|
||||
config: cfg,
|
||||
persistence: persistence,
|
||||
wsHub: wsHub,
|
||||
monitor: monitor,
|
||||
reloadSystemSettingsFunc: reloadSystemSettingsFunc,
|
||||
reloadMonitorFunc: reloadMonitorFunc,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,8 +120,21 @@ func discoveryConfigMap(raw map[string]interface{}) (map[string]interface{}, boo
|
|||
|
||||
// validateSystemSettings validates settings before applying them
|
||||
func validateSystemSettings(settings *config.SystemSettings, rawRequest map[string]interface{}) error {
|
||||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
||||
// Legacy polling interval fields are ignored if provided
|
||||
if val, ok := rawRequest["pvePollingInterval"]; ok {
|
||||
if interval, ok := val.(float64); ok {
|
||||
if interval <= 0 {
|
||||
return fmt.Errorf("PVE polling interval must be positive (minimum 10 seconds)")
|
||||
}
|
||||
if interval < 10 {
|
||||
return fmt.Errorf("PVE polling interval must be at least 10 seconds")
|
||||
}
|
||||
if interval > 3600 {
|
||||
return fmt.Errorf("PVE polling interval cannot exceed 3600 seconds (1 hour)")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("PVE polling interval must be a number")
|
||||
}
|
||||
}
|
||||
|
||||
if val, ok := rawRequest["pbsPollingInterval"]; ok {
|
||||
if interval, ok := val.(float64); ok {
|
||||
|
|
@ -382,6 +397,7 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
|
|||
Msg("Loaded system settings for API response")
|
||||
|
||||
// Always expose effective backup polling configuration
|
||||
settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds())
|
||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||
enabled := h.config.EnableBackupPolling
|
||||
settings.BackupPollingEnabled = &enabled
|
||||
|
|
@ -485,9 +501,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
discoveryConfigUpdated := false
|
||||
prevTempEnabled := h.config.TemperatureMonitoringEnabled
|
||||
tempToggleRequested := false
|
||||
pveIntervalChanged := false
|
||||
|
||||
// Only update fields that were provided in the request
|
||||
// Note: PVE polling is hardcoded to 10s, legacy polling fields are ignored
|
||||
if _, ok := rawRequest["pvePollingInterval"]; ok {
|
||||
settings.PVEPollingInterval = updates.PVEPollingInterval
|
||||
}
|
||||
if _, ok := rawRequest["pbsPollingInterval"]; ok {
|
||||
settings.PBSPollingInterval = updates.PBSPollingInterval
|
||||
}
|
||||
|
|
@ -608,7 +627,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
}
|
||||
|
||||
// Update the config
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
if _, ok := rawRequest["pvePollingInterval"]; ok && settings.PVEPollingInterval > 0 {
|
||||
newInterval := time.Duration(settings.PVEPollingInterval) * time.Second
|
||||
if newInterval < 10*time.Second {
|
||||
newInterval = 10 * time.Second
|
||||
}
|
||||
if h.config.PVEPollingInterval != newInterval {
|
||||
pveIntervalChanged = true
|
||||
}
|
||||
h.config.PVEPollingInterval = newInterval
|
||||
}
|
||||
if settings.AllowedOrigins != "" {
|
||||
h.config.AllowedOrigins = settings.AllowedOrigins
|
||||
}
|
||||
|
|
@ -718,6 +746,14 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
|||
h.reloadSystemSettingsFunc()
|
||||
}
|
||||
|
||||
if pveIntervalChanged && h.reloadMonitorFunc != nil {
|
||||
if err := h.reloadMonitorFunc(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to reload monitor after PVE polling interval change")
|
||||
http.Error(w, "Configuration saved but failed to reload monitor", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msg("System settings updated")
|
||||
|
||||
// Broadcast theme change to all connected clients if theme was updated
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
|
@ -14,7 +16,7 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) {
|
|||
t.Setenv("PULSE_DOCKER", "true")
|
||||
cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir, PVEInstances: []config.PVEInstance{{Name: "pve-a"}}}
|
||||
persistence := config.NewConfigPersistence(tempDir)
|
||||
handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil)
|
||||
handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", bytes.NewBufferString(`{"temperatureMonitoringEnabled":true}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
|
@ -25,3 +27,53 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) {
|
|||
t.Fatalf("expected status 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateSystemSettingsRejectsInvalidPVEPollingInterval(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
DataPath: tempDir,
|
||||
ConfigPath: tempDir,
|
||||
PVEPollingInterval: 10 * time.Second,
|
||||
}
|
||||
persistence := config.NewConfigPersistence(tempDir)
|
||||
handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", strings.NewReader(`{"pvePollingInterval":5}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleUpdateSystemSettings(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateSystemSettingsUpdatesPVEPollingInterval(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cfg := &config.Config{
|
||||
DataPath: tempDir,
|
||||
ConfigPath: tempDir,
|
||||
PVEPollingInterval: 10 * time.Second,
|
||||
}
|
||||
persistence := config.NewConfigPersistence(tempDir)
|
||||
reloaded := false
|
||||
handler := NewSystemSettingsHandler(cfg, persistence, nil, nil, nil, func() error {
|
||||
reloaded = true
|
||||
return nil
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", strings.NewReader(`{"pvePollingInterval":30}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleUpdateSystemSettings(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
if cfg.PVEPollingInterval != 30*time.Second {
|
||||
t.Fatalf("expected interval to be 30s, got %v", cfg.PVEPollingInterval)
|
||||
}
|
||||
if !reloaded {
|
||||
t.Fatal("expected reload to be triggered")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ type Config struct {
|
|||
PMGInstances []PMGInstance
|
||||
|
||||
// Monitoring settings
|
||||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
||||
PVEPollingInterval time.Duration `envconfig:"PVE_POLLING_INTERVAL"` // PVE polling interval (10s default)
|
||||
PBSPollingInterval time.Duration `envconfig:"PBS_POLLING_INTERVAL"` // PBS polling interval (60s default)
|
||||
PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default)
|
||||
ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"`
|
||||
|
|
@ -529,6 +529,7 @@ func Load() (*Config, error) {
|
|||
BackupPollingCycles: 10,
|
||||
BackupPollingInterval: 0,
|
||||
EnableBackupPolling: true,
|
||||
PVEPollingInterval: 10 * time.Second,
|
||||
WebhookBatchDelay: 10 * time.Second,
|
||||
AdaptivePollingEnabled: false,
|
||||
AdaptivePollingBaseInterval: 10 * time.Second,
|
||||
|
|
@ -578,7 +579,10 @@ func Load() (*Config, error) {
|
|||
|
||||
// Load system configuration
|
||||
if systemSettings, err := persistence.LoadSystemSettings(); err == nil && systemSettings != nil {
|
||||
// Load PBS polling interval if configured
|
||||
// Load polling intervals if configured
|
||||
if systemSettings.PVEPollingInterval > 0 {
|
||||
cfg.PVEPollingInterval = time.Duration(systemSettings.PVEPollingInterval) * time.Second
|
||||
}
|
||||
if systemSettings.PBSPollingInterval > 0 {
|
||||
cfg.PBSPollingInterval = time.Duration(systemSettings.PBSPollingInterval) * time.Second
|
||||
}
|
||||
|
|
@ -675,8 +679,13 @@ func Load() (*Config, error) {
|
|||
log.Warn().Err(err).Msg("Failed to load API tokens from persistence")
|
||||
}
|
||||
|
||||
// Ensure PBS polling interval has default if not set
|
||||
// Note: PVE polling is hardcoded to 10s in monitor.go
|
||||
// Ensure polling intervals have sane defaults if not set
|
||||
if cfg.PVEPollingInterval <= 0 {
|
||||
cfg.PVEPollingInterval = 10 * time.Second
|
||||
}
|
||||
if cfg.PVEPollingInterval > time.Hour {
|
||||
cfg.PVEPollingInterval = time.Hour
|
||||
}
|
||||
if cfg.PBSPollingInterval == 0 {
|
||||
cfg.PBSPollingInterval = 60 * time.Second
|
||||
}
|
||||
|
|
@ -723,6 +732,28 @@ func Load() (*Config, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if intervalStr := utils.GetenvTrim("PVE_POLLING_INTERVAL"); intervalStr != "" {
|
||||
if dur, err := time.ParseDuration(intervalStr); err == nil {
|
||||
if dur < 10*time.Second {
|
||||
log.Warn().Dur("interval", dur).Msg("Ignoring PVE_POLLING_INTERVAL below 10s from environment")
|
||||
} else {
|
||||
cfg.PVEPollingInterval = dur
|
||||
cfg.EnvOverrides["PVE_POLLING_INTERVAL"] = true
|
||||
log.Info().Dur("interval", dur).Msg("Overriding PVE polling interval from environment")
|
||||
}
|
||||
} else if seconds, err := strconv.Atoi(intervalStr); err == nil {
|
||||
if seconds < 10 {
|
||||
log.Warn().Int("seconds", seconds).Msg("Ignoring PVE_POLLING_INTERVAL below 10s from environment")
|
||||
} else {
|
||||
cfg.PVEPollingInterval = time.Duration(seconds) * time.Second
|
||||
cfg.EnvOverrides["PVE_POLLING_INTERVAL"] = true
|
||||
log.Info().Int("seconds", seconds).Msg("Overriding PVE polling interval (seconds) from environment")
|
||||
}
|
||||
} else {
|
||||
log.Warn().Str("value", intervalStr).Msg("Invalid PVE_POLLING_INTERVAL value, expected duration or seconds")
|
||||
}
|
||||
}
|
||||
|
||||
if enabledStr := utils.GetenvTrim("ENABLE_TEMPERATURE_MONITORING"); enabledStr != "" {
|
||||
if enabled, err := strconv.ParseBool(enabledStr); err == nil {
|
||||
cfg.TemperatureMonitoringEnabled = enabled
|
||||
|
|
@ -1295,7 +1326,7 @@ func SaveConfig(cfg *Config) error {
|
|||
// Save system configuration
|
||||
adaptiveEnabled := cfg.AdaptivePollingEnabled
|
||||
systemSettings := SystemSettings{
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
PVEPollingInterval: int(cfg.PVEPollingInterval / time.Second),
|
||||
UpdateChannel: cfg.UpdateChannel,
|
||||
AutoUpdateEnabled: cfg.AutoUpdateEnabled,
|
||||
AutoUpdateCheckInterval: int(cfg.AutoUpdateCheckInterval.Hours()),
|
||||
|
|
@ -1349,7 +1380,12 @@ func (c *Config) Validate() error {
|
|||
}
|
||||
|
||||
// Validate monitoring settings
|
||||
// Note: PVE polling is hardcoded to 10s
|
||||
if c.PVEPollingInterval < 10*time.Second {
|
||||
return fmt.Errorf("PVE polling interval must be at least 10 seconds")
|
||||
}
|
||||
if c.PVEPollingInterval > time.Hour {
|
||||
return fmt.Errorf("PVE polling interval cannot exceed 1 hour")
|
||||
}
|
||||
if c.ConnectionTimeout < time.Second {
|
||||
return fmt.Errorf("connection timeout must be at least 1 second")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -803,33 +803,33 @@ type NodesConfig struct {
|
|||
|
||||
// SystemSettings represents system configuration settings
|
||||
type SystemSettings struct {
|
||||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
||||
PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds
|
||||
PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds
|
||||
BackupPollingInterval int `json:"backupPollingInterval,omitempty"`
|
||||
BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"`
|
||||
AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"`
|
||||
AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"`
|
||||
AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"`
|
||||
AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"`
|
||||
BackendPort int `json:"backendPort,omitempty"`
|
||||
FrontendPort int `json:"frontendPort,omitempty"`
|
||||
AllowedOrigins string `json:"allowedOrigins,omitempty"`
|
||||
ConnectionTimeout int `json:"connectionTimeout,omitempty"`
|
||||
UpdateChannel string `json:"updateChannel,omitempty"`
|
||||
AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved
|
||||
AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"`
|
||||
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
|
||||
LogLevel string `json:"logLevel,omitempty"`
|
||||
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
||||
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
||||
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
||||
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
||||
PVEPollingInterval int `json:"pvePollingInterval"` // PVE polling interval in seconds
|
||||
PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds
|
||||
PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds
|
||||
BackupPollingInterval int `json:"backupPollingInterval,omitempty"`
|
||||
BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"`
|
||||
AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"`
|
||||
AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"`
|
||||
AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"`
|
||||
AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"`
|
||||
BackendPort int `json:"backendPort,omitempty"`
|
||||
FrontendPort int `json:"frontendPort,omitempty"`
|
||||
AllowedOrigins string `json:"allowedOrigins,omitempty"`
|
||||
ConnectionTimeout int `json:"connectionTimeout,omitempty"`
|
||||
UpdateChannel string `json:"updateChannel,omitempty"`
|
||||
AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved
|
||||
AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"`
|
||||
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
|
||||
LogLevel string `json:"logLevel,omitempty"`
|
||||
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
||||
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
||||
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
||||
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
||||
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
||||
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
|
||||
DNSCacheTimeout int `json:"dnsCacheTimeout,omitempty"` // DNS cache timeout in seconds (0 = default 5 minutes)
|
||||
SSHPort int `json:"sshPort,omitempty"` // Default SSH port for temperature monitoring (0 = use 22)
|
||||
DNSCacheTimeout int `json:"dnsCacheTimeout,omitempty"` // DNS cache timeout in seconds (0 = default 5 minutes)
|
||||
SSHPort int `json:"sshPort,omitempty"` // Default SSH port for temperature monitoring (0 = use 22)
|
||||
WebhookAllowedPrivateCIDRs string `json:"webhookAllowedPrivateCIDRs,omitempty"` // Comma-separated list of private CIDR ranges allowed for webhooks (e.g., "192.168.1.0/24,10.0.0.0/8")
|
||||
// APIToken removed - now handled via .env file only
|
||||
}
|
||||
|
|
@ -838,6 +838,7 @@ type SystemSettings struct {
|
|||
func DefaultSystemSettings() *SystemSettings {
|
||||
defaultDiscovery := DefaultDiscoveryConfig()
|
||||
return &SystemSettings{
|
||||
PVEPollingInterval: 10,
|
||||
PBSPollingInterval: 60,
|
||||
PMGPollingInterval: 60,
|
||||
AutoUpdateEnabled: false,
|
||||
|
|
@ -1361,9 +1362,8 @@ func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSetting
|
|||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Skip POLLING_INTERVAL lines - deprecated
|
||||
// Skip legacy POLLING_INTERVAL lines - replaced by system.json PVE_POLLING_INTERVAL
|
||||
if strings.HasPrefix(line, "POLLING_INTERVAL=") {
|
||||
// Skip this line, polling interval is now hardcoded
|
||||
continue
|
||||
} else if strings.HasPrefix(line, "UPDATE_CHANNEL=") && settings.UpdateChannel != "" {
|
||||
lines = append(lines, fmt.Sprintf("UPDATE_CHANNEL=%s", settings.UpdateChannel))
|
||||
|
|
@ -1381,7 +1381,7 @@ func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSetting
|
|||
return err
|
||||
}
|
||||
|
||||
// Note: POLLING_INTERVAL is deprecated and no longer written
|
||||
// Note: legacy POLLING_INTERVAL is deprecated and no longer written
|
||||
|
||||
// Build the new content
|
||||
content := strings.Join(lines, "\n")
|
||||
|
|
|
|||
|
|
@ -4019,10 +4019,28 @@ func (m *Monitor) getExecutor() PollExecutor {
|
|||
return exec
|
||||
}
|
||||
|
||||
func (m *Monitor) effectivePVEPollingInterval() time.Duration {
|
||||
const minInterval = 10 * time.Second
|
||||
const maxInterval = time.Hour
|
||||
|
||||
interval := minInterval
|
||||
if m != nil && m.config != nil && m.config.PVEPollingInterval > 0 {
|
||||
interval = m.config.PVEPollingInterval
|
||||
}
|
||||
if interval < minInterval {
|
||||
interval = minInterval
|
||||
}
|
||||
if interval > maxInterval {
|
||||
interval = maxInterval
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
// Start begins the monitoring loop
|
||||
func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||
pollingInterval := m.effectivePVEPollingInterval()
|
||||
log.Info().
|
||||
Dur("pollingInterval", 10*time.Second).
|
||||
Dur("pollingInterval", pollingInterval).
|
||||
Msg("Starting monitoring loop")
|
||||
|
||||
m.mu.Lock()
|
||||
|
|
@ -4113,9 +4131,7 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
|||
wsHub.BroadcastAlert(alert)
|
||||
})
|
||||
|
||||
// Create separate tickers for polling and broadcasting
|
||||
// Hardcoded to 10 seconds since Proxmox updates cluster/resources every 10 seconds
|
||||
const pollingInterval = 10 * time.Second
|
||||
// Create separate tickers for polling and broadcasting using the configured cadence
|
||||
|
||||
workerCount := len(m.pveClients) + len(m.pbsClients) + len(m.pmgClients)
|
||||
m.startTaskWorkers(ctx, workerCount)
|
||||
|
|
|
|||
|
|
@ -90,9 +90,7 @@ func (rm *ReloadableMonitor) doReload() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Note: Polling interval is now hardcoded to 10s, no special handling needed
|
||||
|
||||
// For other changes, do a full reload
|
||||
// Polling interval changes and other settings require a full reload
|
||||
log.Info().Msg("Performing full monitor reload")
|
||||
|
||||
// Cancel current monitor
|
||||
|
|
@ -123,8 +121,6 @@ func (rm *ReloadableMonitor) doReload() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Note: Polling interval change detection removed - now hardcoded to 10s
|
||||
|
||||
// GetMonitor returns the current monitor instance
|
||||
func (rm *ReloadableMonitor) GetMonitor() *Monitor {
|
||||
rm.mu.RLock()
|
||||
|
|
@ -160,5 +156,3 @@ func (rm *ReloadableMonitor) Stop() {
|
|||
rm.monitor.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Note: UpdatePollingInterval removed - polling interval is now hardcoded to 10s
|
||||
|
|
|
|||
Loading…
Reference in a new issue