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:
|
Quick start - most settings are in the web UI:
|
||||||
- **Settings → Nodes**: Add/remove Proxmox instances
|
- **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
|
- **Settings → Security**: Authentication and API tokens
|
||||||
- **Alerts**: Thresholds and notifications
|
- **Alerts**: Thresholds and notifications
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,22 +252,14 @@ func runServer() {
|
||||||
configWatcher.ReloadConfig()
|
configWatcher.ReloadConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload system.json
|
if reloadFunc != nil {
|
||||||
persistence := config.NewConfigPersistence(cfg.DataPath)
|
if err := reloadFunc(); err != nil {
|
||||||
if persistence != nil {
|
log.Error().Err(err).Msg("Failed to reload monitor after SIGHUP")
|
||||||
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")
|
|
||||||
} else {
|
} 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:
|
case <-sigChan:
|
||||||
log.Info().Msg("Shutting down server...")
|
log.Info().Msg("Shutting down server...")
|
||||||
goto shutdown
|
goto shutdown
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,8 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout
|
||||||
**Contents:**
|
**Contents:**
|
||||||
```json
|
```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)
|
"pmgPollingInterval": 60, // Seconds between PMG refreshes (mail analytics and health)
|
||||||
"connectionTimeout": 60, // Seconds before node connection timeout
|
"connectionTimeout": 60, // Seconds before node connection timeout
|
||||||
"autoUpdateEnabled": false, // Systemd timer toggle for automatic updates
|
"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
|
- Can be safely backed up without exposing secrets
|
||||||
- Missing file results in defaults being used
|
- Missing file results in defaults being used
|
||||||
- Changes take effect immediately (no restart required)
|
- 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+)
|
- 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.
|
- **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.
|
- **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 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
|
// Node with UI-specific fields
|
||||||
type NodeConfigWithStatus = NodeConfig & {
|
type NodeConfigWithStatus = NodeConfig & {
|
||||||
hasPassword?: boolean;
|
hasPassword?: boolean;
|
||||||
|
|
@ -610,7 +621,11 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
const pmgNodes = createMemo(() => nodes().filter((n) => n.type === 'pmg'));
|
const pmgNodes = createMemo(() => nodes().filter((n) => n.type === 'pmg'));
|
||||||
|
|
||||||
// System settings
|
// 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 [allowedOrigins, setAllowedOrigins] = createSignal('*');
|
||||||
const [discoveryEnabled, setDiscoveryEnabled] = createSignal(false);
|
const [discoveryEnabled, setDiscoveryEnabled] = createSignal(false);
|
||||||
const [discoverySubnet, setDiscoverySubnet] = createSignal('auto');
|
const [discoverySubnet, setDiscoverySubnet] = createSignal('auto');
|
||||||
|
|
@ -627,6 +642,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Boolean(
|
Boolean(
|
||||||
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
|
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
|
||||||
);
|
);
|
||||||
|
const pvePollingEnvLocked = () =>
|
||||||
|
Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL);
|
||||||
let discoverySubnetInputRef: HTMLInputElement | undefined;
|
let discoverySubnetInputRef: HTMLInputElement | undefined;
|
||||||
|
|
||||||
const parseSubnetList = (value: string) => {
|
const parseSubnetList = (value: string) => {
|
||||||
|
|
@ -1912,7 +1929,22 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
// Load system settings
|
// Load system settings
|
||||||
try {
|
try {
|
||||||
const systemSettings = await SettingsAPI.getSystemSettings();
|
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 || '*');
|
setAllowedOrigins(systemSettings.allowedOrigins || '*');
|
||||||
// Connection timeout is backend-only
|
// Connection timeout is backend-only
|
||||||
// Load discovery settings (default to false when unset)
|
// Load discovery settings (default to false when unset)
|
||||||
|
|
@ -2027,7 +2059,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
) {
|
) {
|
||||||
// Save system settings using typed API
|
// Save system settings using typed API
|
||||||
await SettingsAPI.updateSystemSettings({
|
await SettingsAPI.updateSystemSettings({
|
||||||
// PBS polling interval is now fixed at 10 seconds
|
pvePollingInterval: pvePollingInterval(),
|
||||||
allowedOrigins: allowedOrigins(),
|
allowedOrigins: allowedOrigins(),
|
||||||
// Connection timeout is backend-only
|
// Connection timeout is backend-only
|
||||||
// Discovery settings are saved immediately on toggle
|
// Discovery settings are saved immediately on toggle
|
||||||
|
|
@ -3523,6 +3555,133 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export interface AuthConfig {
|
||||||
* These are application behavior settings
|
* These are application behavior settings
|
||||||
*/
|
*/
|
||||||
export interface SystemConfig {
|
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
|
pbsPollingInterval?: number; // PBS polling interval in seconds
|
||||||
connectionTimeout?: number; // Seconds before timeout (default: 10)
|
connectionTimeout?: number; // Seconds before timeout (default: 10)
|
||||||
autoUpdateEnabled: boolean; // Enable auto-updates
|
autoUpdateEnabled: boolean; // Enable auto-updates
|
||||||
|
|
|
||||||
|
|
@ -2876,6 +2876,7 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
||||||
|
|
||||||
// Get current values from running config
|
// Get current values from running config
|
||||||
settings := *persistedSettings
|
settings := *persistedSettings
|
||||||
|
settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds())
|
||||||
settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds())
|
settings.PBSPollingInterval = int(h.config.PBSPollingInterval.Seconds())
|
||||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||||
settings.BackendPort = h.config.BackendPort
|
settings.BackendPort = h.config.BackendPort
|
||||||
|
|
|
||||||
|
|
@ -977,7 +977,7 @@ func (r *Router) setupRoutes() {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// System settings and API token management
|
// 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", 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/settings/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.systemSettingsHandler.HandleUpdateSystemSettings)))
|
||||||
r.mux.HandleFunc("/api/system/ssh-config", r.handleSSHConfig)
|
r.mux.HandleFunc("/api/system/ssh-config", r.handleSSHConfig)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ type SystemSettingsHandler struct {
|
||||||
persistence *config.ConfigPersistence
|
persistence *config.ConfigPersistence
|
||||||
wsHub *websocket.Hub
|
wsHub *websocket.Hub
|
||||||
reloadSystemSettingsFunc func() // Function to reload cached system settings
|
reloadSystemSettingsFunc func() // Function to reload cached system settings
|
||||||
|
reloadMonitorFunc func() error
|
||||||
monitor interface {
|
monitor interface {
|
||||||
GetDiscoveryService() *discovery.Service
|
GetDiscoveryService() *discovery.Service
|
||||||
StartDiscoveryService(ctx context.Context, wsHub *websocket.Hub, subnet string)
|
StartDiscoveryService(ctx context.Context, wsHub *websocket.Hub, subnet string)
|
||||||
|
|
@ -48,13 +49,14 @@ func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPers
|
||||||
DisableTemperatureMonitoring()
|
DisableTemperatureMonitoring()
|
||||||
GetNotificationManager() *notifications.NotificationManager
|
GetNotificationManager() *notifications.NotificationManager
|
||||||
HasSocketTemperatureProxy() bool
|
HasSocketTemperatureProxy() bool
|
||||||
}, reloadSystemSettingsFunc func()) *SystemSettingsHandler {
|
}, reloadSystemSettingsFunc func(), reloadMonitorFunc func() error) *SystemSettingsHandler {
|
||||||
return &SystemSettingsHandler{
|
return &SystemSettingsHandler{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
persistence: persistence,
|
persistence: persistence,
|
||||||
wsHub: wsHub,
|
wsHub: wsHub,
|
||||||
monitor: monitor,
|
monitor: monitor,
|
||||||
reloadSystemSettingsFunc: reloadSystemSettingsFunc,
|
reloadSystemSettingsFunc: reloadSystemSettingsFunc,
|
||||||
|
reloadMonitorFunc: reloadMonitorFunc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,8 +120,21 @@ func discoveryConfigMap(raw map[string]interface{}) (map[string]interface{}, boo
|
||||||
|
|
||||||
// validateSystemSettings validates settings before applying them
|
// validateSystemSettings validates settings before applying them
|
||||||
func validateSystemSettings(settings *config.SystemSettings, rawRequest map[string]interface{}) error {
|
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
|
if val, ok := rawRequest["pvePollingInterval"]; ok {
|
||||||
// Legacy polling interval fields are ignored if provided
|
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 val, ok := rawRequest["pbsPollingInterval"]; ok {
|
||||||
if interval, ok := val.(float64); 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")
|
Msg("Loaded system settings for API response")
|
||||||
|
|
||||||
// Always expose effective backup polling configuration
|
// Always expose effective backup polling configuration
|
||||||
|
settings.PVEPollingInterval = int(h.config.PVEPollingInterval.Seconds())
|
||||||
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
settings.BackupPollingInterval = int(h.config.BackupPollingInterval.Seconds())
|
||||||
enabled := h.config.EnableBackupPolling
|
enabled := h.config.EnableBackupPolling
|
||||||
settings.BackupPollingEnabled = &enabled
|
settings.BackupPollingEnabled = &enabled
|
||||||
|
|
@ -485,9 +501,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
discoveryConfigUpdated := false
|
discoveryConfigUpdated := false
|
||||||
prevTempEnabled := h.config.TemperatureMonitoringEnabled
|
prevTempEnabled := h.config.TemperatureMonitoringEnabled
|
||||||
tempToggleRequested := false
|
tempToggleRequested := false
|
||||||
|
pveIntervalChanged := false
|
||||||
|
|
||||||
// Only update fields that were provided in the request
|
// 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 {
|
if _, ok := rawRequest["pbsPollingInterval"]; ok {
|
||||||
settings.PBSPollingInterval = updates.PBSPollingInterval
|
settings.PBSPollingInterval = updates.PBSPollingInterval
|
||||||
}
|
}
|
||||||
|
|
@ -608,7 +627,16 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the config
|
// 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 != "" {
|
if settings.AllowedOrigins != "" {
|
||||||
h.config.AllowedOrigins = settings.AllowedOrigins
|
h.config.AllowedOrigins = settings.AllowedOrigins
|
||||||
}
|
}
|
||||||
|
|
@ -718,6 +746,14 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
|
||||||
h.reloadSystemSettingsFunc()
|
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")
|
log.Info().Msg("System settings updated")
|
||||||
|
|
||||||
// Broadcast theme change to all connected clients if theme was updated
|
// Broadcast theme change to all connected clients if theme was updated
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
)
|
)
|
||||||
|
|
@ -14,7 +16,7 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) {
|
||||||
t.Setenv("PULSE_DOCKER", "true")
|
t.Setenv("PULSE_DOCKER", "true")
|
||||||
cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir, PVEInstances: []config.PVEInstance{{Name: "pve-a"}}}
|
cfg := &config.Config{DataPath: tempDir, ConfigPath: tempDir, PVEInstances: []config.PVEInstance{{Name: "pve-a"}}}
|
||||||
persistence := config.NewConfigPersistence(tempDir)
|
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}`))
|
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", bytes.NewBufferString(`{"temperatureMonitoringEnabled":true}`))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
@ -25,3 +27,53 @@ func TestHandleUpdateSystemSettingsRejectsTempsWithoutTransport(t *testing.T) {
|
||||||
t.Fatalf("expected status 400, got %d", rec.Code)
|
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
|
PMGInstances []PMGInstance
|
||||||
|
|
||||||
// Monitoring settings
|
// 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)
|
PBSPollingInterval time.Duration `envconfig:"PBS_POLLING_INTERVAL"` // PBS polling interval (60s default)
|
||||||
PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default)
|
PMGPollingInterval time.Duration `envconfig:"PMG_POLLING_INTERVAL"` // PMG polling interval (60s default)
|
||||||
ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"`
|
ConcurrentPolling bool `envconfig:"CONCURRENT_POLLING" default:"true"`
|
||||||
|
|
@ -529,6 +529,7 @@ func Load() (*Config, error) {
|
||||||
BackupPollingCycles: 10,
|
BackupPollingCycles: 10,
|
||||||
BackupPollingInterval: 0,
|
BackupPollingInterval: 0,
|
||||||
EnableBackupPolling: true,
|
EnableBackupPolling: true,
|
||||||
|
PVEPollingInterval: 10 * time.Second,
|
||||||
WebhookBatchDelay: 10 * time.Second,
|
WebhookBatchDelay: 10 * time.Second,
|
||||||
AdaptivePollingEnabled: false,
|
AdaptivePollingEnabled: false,
|
||||||
AdaptivePollingBaseInterval: 10 * time.Second,
|
AdaptivePollingBaseInterval: 10 * time.Second,
|
||||||
|
|
@ -578,7 +579,10 @@ func Load() (*Config, error) {
|
||||||
|
|
||||||
// Load system configuration
|
// Load system configuration
|
||||||
if systemSettings, err := persistence.LoadSystemSettings(); err == nil && systemSettings != nil {
|
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 {
|
if systemSettings.PBSPollingInterval > 0 {
|
||||||
cfg.PBSPollingInterval = time.Duration(systemSettings.PBSPollingInterval) * time.Second
|
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")
|
log.Warn().Err(err).Msg("Failed to load API tokens from persistence")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure PBS polling interval has default if not set
|
// Ensure polling intervals have sane defaults if not set
|
||||||
// Note: PVE polling is hardcoded to 10s in monitor.go
|
if cfg.PVEPollingInterval <= 0 {
|
||||||
|
cfg.PVEPollingInterval = 10 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.PVEPollingInterval > time.Hour {
|
||||||
|
cfg.PVEPollingInterval = time.Hour
|
||||||
|
}
|
||||||
if cfg.PBSPollingInterval == 0 {
|
if cfg.PBSPollingInterval == 0 {
|
||||||
cfg.PBSPollingInterval = 60 * time.Second
|
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 enabledStr := utils.GetenvTrim("ENABLE_TEMPERATURE_MONITORING"); enabledStr != "" {
|
||||||
if enabled, err := strconv.ParseBool(enabledStr); err == nil {
|
if enabled, err := strconv.ParseBool(enabledStr); err == nil {
|
||||||
cfg.TemperatureMonitoringEnabled = enabled
|
cfg.TemperatureMonitoringEnabled = enabled
|
||||||
|
|
@ -1295,7 +1326,7 @@ func SaveConfig(cfg *Config) error {
|
||||||
// Save system configuration
|
// Save system configuration
|
||||||
adaptiveEnabled := cfg.AdaptivePollingEnabled
|
adaptiveEnabled := cfg.AdaptivePollingEnabled
|
||||||
systemSettings := SystemSettings{
|
systemSettings := SystemSettings{
|
||||||
// Note: PVE polling is hardcoded to 10s
|
PVEPollingInterval: int(cfg.PVEPollingInterval / time.Second),
|
||||||
UpdateChannel: cfg.UpdateChannel,
|
UpdateChannel: cfg.UpdateChannel,
|
||||||
AutoUpdateEnabled: cfg.AutoUpdateEnabled,
|
AutoUpdateEnabled: cfg.AutoUpdateEnabled,
|
||||||
AutoUpdateCheckInterval: int(cfg.AutoUpdateCheckInterval.Hours()),
|
AutoUpdateCheckInterval: int(cfg.AutoUpdateCheckInterval.Hours()),
|
||||||
|
|
@ -1349,7 +1380,12 @@ func (c *Config) Validate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate monitoring settings
|
// 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 {
|
if c.ConnectionTimeout < time.Second {
|
||||||
return fmt.Errorf("connection timeout must be at least 1 second")
|
return fmt.Errorf("connection timeout must be at least 1 second")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -803,33 +803,33 @@ type NodesConfig struct {
|
||||||
|
|
||||||
// SystemSettings represents system configuration settings
|
// SystemSettings represents system configuration settings
|
||||||
type SystemSettings struct {
|
type SystemSettings struct {
|
||||||
// Note: PVE polling is hardcoded to 10s since Proxmox cluster/resources endpoint only updates every 10s
|
PVEPollingInterval int `json:"pvePollingInterval"` // PVE polling interval in seconds
|
||||||
PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds
|
PBSPollingInterval int `json:"pbsPollingInterval"` // PBS polling interval in seconds
|
||||||
PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds
|
PMGPollingInterval int `json:"pmgPollingInterval"` // PMG polling interval in seconds
|
||||||
BackupPollingInterval int `json:"backupPollingInterval,omitempty"`
|
BackupPollingInterval int `json:"backupPollingInterval,omitempty"`
|
||||||
BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"`
|
BackupPollingEnabled *bool `json:"backupPollingEnabled,omitempty"`
|
||||||
AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"`
|
AdaptivePollingEnabled *bool `json:"adaptivePollingEnabled,omitempty"`
|
||||||
AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"`
|
AdaptivePollingBaseInterval int `json:"adaptivePollingBaseInterval,omitempty"`
|
||||||
AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"`
|
AdaptivePollingMinInterval int `json:"adaptivePollingMinInterval,omitempty"`
|
||||||
AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"`
|
AdaptivePollingMaxInterval int `json:"adaptivePollingMaxInterval,omitempty"`
|
||||||
BackendPort int `json:"backendPort,omitempty"`
|
BackendPort int `json:"backendPort,omitempty"`
|
||||||
FrontendPort int `json:"frontendPort,omitempty"`
|
FrontendPort int `json:"frontendPort,omitempty"`
|
||||||
AllowedOrigins string `json:"allowedOrigins,omitempty"`
|
AllowedOrigins string `json:"allowedOrigins,omitempty"`
|
||||||
ConnectionTimeout int `json:"connectionTimeout,omitempty"`
|
ConnectionTimeout int `json:"connectionTimeout,omitempty"`
|
||||||
UpdateChannel string `json:"updateChannel,omitempty"`
|
UpdateChannel string `json:"updateChannel,omitempty"`
|
||||||
AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved
|
AutoUpdateEnabled bool `json:"autoUpdateEnabled"` // Removed omitempty so false is saved
|
||||||
AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"`
|
AutoUpdateCheckInterval int `json:"autoUpdateCheckInterval,omitempty"`
|
||||||
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
|
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
|
||||||
LogLevel string `json:"logLevel,omitempty"`
|
LogLevel string `json:"logLevel,omitempty"`
|
||||||
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
DiscoveryEnabled bool `json:"discoveryEnabled"`
|
||||||
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
|
||||||
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
DiscoveryConfig DiscoveryConfig `json:"discoveryConfig"`
|
||||||
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
|
||||||
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
AllowEmbedding bool `json:"allowEmbedding"` // Allow iframe embedding
|
||||||
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
AllowedEmbedOrigins string `json:"allowedEmbedOrigins,omitempty"` // Comma-separated list of allowed origins for embedding
|
||||||
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
|
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
|
||||||
DNSCacheTimeout int `json:"dnsCacheTimeout,omitempty"` // DNS cache timeout in seconds (0 = default 5 minutes)
|
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)
|
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")
|
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
|
// APIToken removed - now handled via .env file only
|
||||||
}
|
}
|
||||||
|
|
@ -838,6 +838,7 @@ type SystemSettings struct {
|
||||||
func DefaultSystemSettings() *SystemSettings {
|
func DefaultSystemSettings() *SystemSettings {
|
||||||
defaultDiscovery := DefaultDiscoveryConfig()
|
defaultDiscovery := DefaultDiscoveryConfig()
|
||||||
return &SystemSettings{
|
return &SystemSettings{
|
||||||
|
PVEPollingInterval: 10,
|
||||||
PBSPollingInterval: 60,
|
PBSPollingInterval: 60,
|
||||||
PMGPollingInterval: 60,
|
PMGPollingInterval: 60,
|
||||||
AutoUpdateEnabled: false,
|
AutoUpdateEnabled: false,
|
||||||
|
|
@ -1361,9 +1362,8 @@ func (c *ConfigPersistence) updateEnvFile(envFile string, settings SystemSetting
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
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=") {
|
if strings.HasPrefix(line, "POLLING_INTERVAL=") {
|
||||||
// Skip this line, polling interval is now hardcoded
|
|
||||||
continue
|
continue
|
||||||
} else if strings.HasPrefix(line, "UPDATE_CHANNEL=") && settings.UpdateChannel != "" {
|
} else if strings.HasPrefix(line, "UPDATE_CHANNEL=") && settings.UpdateChannel != "" {
|
||||||
lines = append(lines, fmt.Sprintf("UPDATE_CHANNEL=%s", 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
|
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
|
// Build the new content
|
||||||
content := strings.Join(lines, "\n")
|
content := strings.Join(lines, "\n")
|
||||||
|
|
|
||||||
|
|
@ -4019,10 +4019,28 @@ func (m *Monitor) getExecutor() PollExecutor {
|
||||||
return exec
|
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
|
// Start begins the monitoring loop
|
||||||
func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||||
|
pollingInterval := m.effectivePVEPollingInterval()
|
||||||
log.Info().
|
log.Info().
|
||||||
Dur("pollingInterval", 10*time.Second).
|
Dur("pollingInterval", pollingInterval).
|
||||||
Msg("Starting monitoring loop")
|
Msg("Starting monitoring loop")
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|
@ -4113,9 +4131,7 @@ func (m *Monitor) Start(ctx context.Context, wsHub *websocket.Hub) {
|
||||||
wsHub.BroadcastAlert(alert)
|
wsHub.BroadcastAlert(alert)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create separate tickers for polling and broadcasting
|
// Create separate tickers for polling and broadcasting using the configured cadence
|
||||||
// Hardcoded to 10 seconds since Proxmox updates cluster/resources every 10 seconds
|
|
||||||
const pollingInterval = 10 * time.Second
|
|
||||||
|
|
||||||
workerCount := len(m.pveClients) + len(m.pbsClients) + len(m.pmgClients)
|
workerCount := len(m.pveClients) + len(m.pbsClients) + len(m.pmgClients)
|
||||||
m.startTaskWorkers(ctx, workerCount)
|
m.startTaskWorkers(ctx, workerCount)
|
||||||
|
|
|
||||||
|
|
@ -90,9 +90,7 @@ func (rm *ReloadableMonitor) doReload() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Polling interval is now hardcoded to 10s, no special handling needed
|
// Polling interval changes and other settings require a full reload
|
||||||
|
|
||||||
// For other changes, do a full reload
|
|
||||||
log.Info().Msg("Performing full monitor reload")
|
log.Info().Msg("Performing full monitor reload")
|
||||||
|
|
||||||
// Cancel current monitor
|
// Cancel current monitor
|
||||||
|
|
@ -123,8 +121,6 @@ func (rm *ReloadableMonitor) doReload() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Polling interval change detection removed - now hardcoded to 10s
|
|
||||||
|
|
||||||
// GetMonitor returns the current monitor instance
|
// GetMonitor returns the current monitor instance
|
||||||
func (rm *ReloadableMonitor) GetMonitor() *Monitor {
|
func (rm *ReloadableMonitor) GetMonitor() *Monitor {
|
||||||
rm.mu.RLock()
|
rm.mu.RLock()
|
||||||
|
|
@ -160,5 +156,3 @@ func (rm *ReloadableMonitor) Stop() {
|
||||||
rm.monitor.Stop()
|
rm.monitor.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: UpdatePollingInterval removed - polling interval is now hardcoded to 10s
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue