Implement UI toggle for Hide Local Login (related to issue #750)
This commit is contained in:
parent
8b7ff2ad48
commit
3acd29c3f3
7 changed files with 382 additions and 332 deletions
|
|
@ -82,6 +82,7 @@ Environment variables take precedence over `system.json`.
|
||||||
| `LOG_LEVEL` | Log verbosity | `info` |
|
| `LOG_LEVEL` | Log verbosity | `info` |
|
||||||
| `DISCOVERY_ENABLED` | Auto-discover nodes | `false` |
|
| `DISCOVERY_ENABLED` | Auto-discover nodes | `false` |
|
||||||
| `ALLOWED_ORIGINS` | CORS allowed domains | `""` (Same origin) |
|
| `ALLOWED_ORIGINS` | CORS allowed domains | `""` (Same origin) |
|
||||||
|
| `PULSE_AUTH_HIDE_LOCAL_LOGIN` | Hide username/password form (useful for SSO) | `false` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ Enable Single Sign-On (SSO) with providers like Authentik, Keycloak, Okta, and A
|
||||||
* **Client ID & Secret**: From your IdP.
|
* **Client ID & Secret**: From your IdP.
|
||||||
4. **Save**: The login page will now show a "Continue with Single Sign-On" button.
|
4. **Save**: The login page will now show a "Continue with Single Sign-On" button.
|
||||||
|
|
||||||
|
> **Tip**: To hide the username/password form and only show the SSO button, set `PULSE_AUTH_HIDE_LOCAL_LOGIN=true` in your environment. You can still access the local login by appending `?show_local=true` to the login URL.
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
| Setting | Description |
|
| Setting | Description |
|
||||||
|
|
|
||||||
|
|
@ -638,10 +638,16 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
const [temperatureMonitoringEnabled, setTemperatureMonitoringEnabled] = createSignal(true);
|
const [temperatureMonitoringEnabled, setTemperatureMonitoringEnabled] = createSignal(true);
|
||||||
const [savingTemperatureSetting, setSavingTemperatureSetting] = createSignal(false);
|
const [savingTemperatureSetting, setSavingTemperatureSetting] = createSignal(false);
|
||||||
const [hostProxyStatus, setHostProxyStatus] = createSignal<HostProxyStatusResponse | null>(null);
|
const [hostProxyStatus, setHostProxyStatus] = createSignal<HostProxyStatusResponse | null>(null);
|
||||||
|
const [hideLocalLogin, setHideLocalLogin] = createSignal(false);
|
||||||
|
const [savingHideLocalLogin, setSavingHideLocalLogin] = createSignal(false);
|
||||||
|
|
||||||
const temperatureMonitoringLocked = () =>
|
const temperatureMonitoringLocked = () =>
|
||||||
Boolean(
|
Boolean(
|
||||||
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
|
envOverrides().temperatureMonitoringEnabled || envOverrides()['ENABLE_TEMPERATURE_MONITORING'],
|
||||||
);
|
);
|
||||||
|
const hideLocalLoginLocked = () =>
|
||||||
|
Boolean(envOverrides().hideLocalLogin || envOverrides()['PULSE_AUTH_HIDE_LOCAL_LOGIN']);
|
||||||
|
|
||||||
const pvePollingEnvLocked = () =>
|
const pvePollingEnvLocked = () =>
|
||||||
Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL);
|
Boolean(envOverrides().pvePollingInterval || envOverrides().PVE_POLLING_INTERVAL);
|
||||||
let discoverySubnetInputRef: HTMLInputElement | undefined;
|
let discoverySubnetInputRef: HTMLInputElement | undefined;
|
||||||
|
|
@ -705,6 +711,35 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHideLocalLoginChange = async (enabled: boolean): Promise<void> => {
|
||||||
|
if (hideLocalLoginLocked() || savingHideLocalLogin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = hideLocalLogin();
|
||||||
|
setHideLocalLogin(enabled);
|
||||||
|
setSavingHideLocalLogin(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SettingsAPI.updateSystemSettings({ hideLocalLogin: enabled });
|
||||||
|
if (enabled) {
|
||||||
|
notificationStore.success('Local login hidden', 2000);
|
||||||
|
} else {
|
||||||
|
notificationStore.info('Local login visible', 2000);
|
||||||
|
}
|
||||||
|
// Reload security status to reflect changes
|
||||||
|
await loadSecurityStatus();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to update hide local login setting', error);
|
||||||
|
notificationStore.error(
|
||||||
|
error instanceof Error ? error.message : 'Failed to update hide local login setting',
|
||||||
|
);
|
||||||
|
setHideLocalLogin(previous);
|
||||||
|
} finally {
|
||||||
|
setSavingHideLocalLogin(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const applySavedDiscoverySubnet = (subnet?: string | null) => {
|
const applySavedDiscoverySubnet = (subnet?: string | null) => {
|
||||||
const raw = typeof subnet === 'string' ? subnet.trim() : '';
|
const raw = typeof subnet === 'string' ? subnet.trim() : '';
|
||||||
if (raw === '' || raw.toLowerCase() === 'auto') {
|
if (raw === '' || raw.toLowerCase() === 'auto') {
|
||||||
|
|
@ -980,8 +1015,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
diag.temperatureProxy.socketFound && diag.temperatureProxy.proxyReachable
|
diag.temperatureProxy.socketFound && diag.temperatureProxy.proxyReachable
|
||||||
? 'healthy'
|
? 'healthy'
|
||||||
: diag.temperatureProxy.socketFound
|
: diag.temperatureProxy.socketFound
|
||||||
? 'error'
|
? 'error'
|
||||||
: 'missing';
|
: 'missing';
|
||||||
const cooldowns: Record<string, TemperatureSocketCooldownInfo> = {};
|
const cooldowns: Record<string, TemperatureSocketCooldownInfo> = {};
|
||||||
const socketHosts = diag.temperatureProxy.socketHostCooldowns || [];
|
const socketHosts = diag.temperatureProxy.socketHostCooldowns || [];
|
||||||
(socketHosts as TemperatureProxySocketHost[]).forEach((entry) => {
|
(socketHosts as TemperatureProxySocketHost[]).forEach((entry) => {
|
||||||
|
|
@ -1018,11 +1053,11 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
setHostProxyStatus(
|
setHostProxyStatus(
|
||||||
diag?.temperatureProxy?.hostProxySummary
|
diag?.temperatureProxy?.hostProxySummary
|
||||||
? {
|
? {
|
||||||
hostSocketPresent: Boolean(diag.temperatureProxy?.socketFound),
|
hostSocketPresent: Boolean(diag.temperatureProxy?.socketFound),
|
||||||
containerSocketPresent:
|
containerSocketPresent:
|
||||||
diag.temperatureProxy?.hostProxySummary?.containerSocketPresent ?? undefined,
|
diag.temperatureProxy?.hostProxySummary?.containerSocketPresent ?? undefined,
|
||||||
summary: diag.temperatureProxy?.hostProxySummary ?? undefined,
|
summary: diag.temperatureProxy?.hostProxySummary ?? undefined,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -1090,10 +1125,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
const nodes = Array.isArray(data.nodes)
|
const nodes = Array.isArray(data.nodes)
|
||||||
? (data.nodes as Array<Record<string, unknown>>).map((node) => ({
|
? (data.nodes as Array<Record<string, unknown>>).map((node) => ({
|
||||||
name: typeof node.name === 'string' ? node.name : 'unknown',
|
name: typeof node.name === 'string' ? node.name : 'unknown',
|
||||||
sshReady: Boolean(node.ssh_ready),
|
sshReady: Boolean(node.ssh_ready),
|
||||||
error: typeof node.error === 'string' ? node.error : undefined,
|
error: typeof node.error === 'string' ? node.error : undefined,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
setProxyRegisterSummary(nodes);
|
setProxyRegisterSummary(nodes);
|
||||||
|
|
@ -1191,83 +1226,83 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}[];
|
}[];
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
id: 'platforms',
|
id: 'platforms',
|
||||||
label: 'Platforms',
|
label: 'Platforms',
|
||||||
items: [
|
items: [
|
||||||
{ id: 'proxmox', label: 'Proxmox', icon: ProxmoxIcon },
|
{ id: 'proxmox', label: 'Proxmox', icon: ProxmoxIcon },
|
||||||
{ id: 'docker', label: 'Docker', icon: Boxes },
|
{ id: 'docker', label: 'Docker', icon: Boxes },
|
||||||
{ id: 'hosts', label: 'Hosts', icon: Monitor, iconProps: { strokeWidth: 2 } },
|
{ id: 'hosts', label: 'Hosts', icon: Monitor, iconProps: { strokeWidth: 2 } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'operations',
|
id: 'operations',
|
||||||
label: 'Operations',
|
label: 'Operations',
|
||||||
items: [
|
items: [
|
||||||
{ id: 'api', label: 'API Tokens', icon: BadgeCheck },
|
{ id: 'api', label: 'API Tokens', icon: BadgeCheck },
|
||||||
{
|
{
|
||||||
id: 'diagnostics',
|
id: 'diagnostics',
|
||||||
label: 'Diagnostics',
|
label: 'Diagnostics',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'system',
|
id: 'system',
|
||||||
label: 'System',
|
label: 'System',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'system-general',
|
id: 'system-general',
|
||||||
label: 'General',
|
label: 'General',
|
||||||
icon: Sliders,
|
icon: Sliders,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'system-network',
|
id: 'system-network',
|
||||||
label: 'Network',
|
label: 'Network',
|
||||||
icon: Network,
|
icon: Network,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'system-updates',
|
id: 'system-updates',
|
||||||
label: 'Updates',
|
label: 'Updates',
|
||||||
icon: RefreshCw,
|
icon: RefreshCw,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'system-backups',
|
id: 'system-backups',
|
||||||
label: 'Backups',
|
label: 'Backups',
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'security',
|
id: 'security',
|
||||||
label: 'Security',
|
label: 'Security',
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'security-overview',
|
id: 'security-overview',
|
||||||
label: 'Overview',
|
label: 'Overview',
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'security-auth',
|
id: 'security-auth',
|
||||||
label: 'Authentication',
|
label: 'Authentication',
|
||||||
icon: Lock,
|
icon: Lock,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'security-sso',
|
id: 'security-sso',
|
||||||
label: 'Single Sign-On',
|
label: 'Single Sign-On',
|
||||||
icon: Key,
|
icon: Key,
|
||||||
iconProps: { strokeWidth: 2 },
|
iconProps: { strokeWidth: 2 },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const flatTabs = tabGroups.flatMap((group) => group.items);
|
const flatTabs = tabGroups.flatMap((group) => group.items);
|
||||||
|
|
||||||
|
|
@ -1960,6 +1995,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
? systemSettings.temperatureMonitoringEnabled
|
? systemSettings.temperatureMonitoringEnabled
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
|
// Load hideLocalLogin setting
|
||||||
|
setHideLocalLogin(systemSettings.hideLocalLogin ?? false);
|
||||||
|
|
||||||
// Backup polling controls
|
// Backup polling controls
|
||||||
if (typeof systemSettings.backupPollingEnabled === 'boolean') {
|
if (typeof systemSettings.backupPollingEnabled === 'boolean') {
|
||||||
setBackupPollingEnabled(systemSettings.backupPollingEnabled);
|
setBackupPollingEnabled(systemSettings.backupPollingEnabled);
|
||||||
|
|
@ -2558,15 +2596,13 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
type="button"
|
type="button"
|
||||||
aria-current={isActive() ? 'page' : undefined}
|
aria-current={isActive() ? 'page' : undefined}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
class={`flex w-full items-center ${sidebarCollapsed() ? 'justify-center' : 'gap-2.5'} rounded-md ${
|
class={`flex w-full items-center ${sidebarCollapsed() ? 'justify-center' : 'gap-2.5'} rounded-md ${sidebarCollapsed() ? 'px-2 py-2.5' : 'px-3 py-2'
|
||||||
sidebarCollapsed() ? 'px-2 py-2.5' : 'px-3 py-2'
|
} text-sm font-medium transition-colors ${item.disabled
|
||||||
} text-sm font-medium transition-colors ${
|
|
||||||
item.disabled
|
|
||||||
? 'opacity-60 cursor-not-allowed text-gray-400 dark:text-gray-600'
|
? 'opacity-60 cursor-not-allowed text-gray-400 dark:text-gray-600'
|
||||||
: isActive()
|
: isActive()
|
||||||
? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-200'
|
? 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-200'
|
||||||
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700/60 dark:hover:text-gray-100'
|
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-700/60 dark:hover:text-gray-100'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.disabled) return;
|
if (item.disabled) return;
|
||||||
setActiveTab(item.id);
|
setActiveTab(item.id);
|
||||||
|
|
@ -2604,13 +2640,12 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
class={`px-3 py-2 text-xs font-medium border-b-2 transition-colors whitespace-nowrap ${
|
class={`px-3 py-2 text-xs font-medium border-b-2 transition-colors whitespace-nowrap ${disabled
|
||||||
disabled
|
? 'opacity-60 cursor-not-allowed text-gray-400 dark:text-gray-600 border-transparent'
|
||||||
? 'opacity-60 cursor-not-allowed text-gray-400 dark:text-gray-600 border-transparent'
|
: isActive
|
||||||
: isActive
|
? 'text-blue-600 dark:text-blue-300 border-blue-500 dark:border-blue-400'
|
||||||
? 'text-blue-600 dark:text-blue-300 border-blue-500 dark:border-blue-400'
|
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-blue-500 dark:hover:text-blue-300 hover:border-blue-300/70 dark:hover:border-blue-500/50'
|
||||||
: 'text-gray-600 dark:text-gray-400 border-transparent hover:text-blue-500 dark:hover:text-blue-300 hover:border-blue-300/70 dark:hover:border-blue-500/50'
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
setActiveTab(tab.id);
|
setActiveTab(tab.id);
|
||||||
|
|
@ -2803,7 +2838,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Last scan{' '}
|
Last scan{' '}
|
||||||
{formatRelativeTime(
|
{formatRelativeTime(
|
||||||
discoveryScanStatus().lastResultAt ??
|
discoveryScanStatus().lastResultAt ??
|
||||||
discoveryScanStatus().lastScanStartedAt,
|
discoveryScanStatus().lastScanStartedAt,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -3090,7 +3125,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Last scan{' '}
|
Last scan{' '}
|
||||||
{formatRelativeTime(
|
{formatRelativeTime(
|
||||||
discoveryScanStatus().lastResultAt ??
|
discoveryScanStatus().lastResultAt ??
|
||||||
discoveryScanStatus().lastScanStartedAt,
|
discoveryScanStatus().lastScanStartedAt,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -3374,7 +3409,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Last scan{' '}
|
Last scan{' '}
|
||||||
{formatRelativeTime(
|
{formatRelativeTime(
|
||||||
discoveryScanStatus().lastResultAt ??
|
discoveryScanStatus().lastResultAt ??
|
||||||
discoveryScanStatus().lastScanStartedAt,
|
discoveryScanStatus().lastScanStartedAt,
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -3584,8 +3619,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Current cadence: {pvePollingInterval()} seconds (
|
Current cadence: {pvePollingInterval()} seconds (
|
||||||
{pvePollingInterval() >= 60
|
{pvePollingInterval() >= 60
|
||||||
? `${(pvePollingInterval() / 60).toFixed(
|
? `${(pvePollingInterval() / 60).toFixed(
|
||||||
pvePollingInterval() % 60 === 0 ? 0 : 1,
|
pvePollingInterval() % 60 === 0 ? 0 : 1,
|
||||||
)} minute${pvePollingInterval() / 60 === 1 ? '' : 's'}`
|
)} minute${pvePollingInterval() / 60 === 1 ? '' : 's'}`
|
||||||
: 'under a minute'}
|
: 'under a minute'}
|
||||||
).
|
).
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -3596,11 +3631,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
|
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${pvePollingSelection() === option.value
|
||||||
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-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'
|
||||||
: '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' : ''}`}
|
||||||
} ${pvePollingEnvLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
|
||||||
disabled={pvePollingEnvLocked()}
|
disabled={pvePollingEnvLocked()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pvePollingEnvLocked()) return;
|
if (pvePollingEnvLocked()) return;
|
||||||
|
|
@ -3615,11 +3649,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</For>
|
</For>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${
|
class={`rounded-lg border px-3 py-2 text-left text-sm transition-colors ${pvePollingSelection() === 'custom'
|
||||||
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-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'
|
||||||
: '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' : ''}`}
|
||||||
} ${pvePollingEnvLocked() ? 'opacity-60 cursor-not-allowed' : ''}`}
|
|
||||||
disabled={pvePollingEnvLocked()}
|
disabled={pvePollingEnvLocked()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (pvePollingEnvLocked()) return;
|
if (pvePollingEnvLocked()) return;
|
||||||
|
|
@ -3792,11 +3825,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</legend>
|
</legend>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label
|
<label
|
||||||
class={`flex items-start gap-3 rounded-lg border p-2 transition-colors ${
|
class={`flex items-start gap-3 rounded-lg border p-2 transition-colors ${discoveryMode() === 'auto'
|
||||||
discoveryMode() === 'auto'
|
? 'border-blue-200 bg-blue-50/80 dark:border-blue-700 dark:bg-blue-900/20'
|
||||||
? 'border-blue-200 bg-blue-50/80 dark:border-blue-700 dark:bg-blue-900/20'
|
: 'border-transparent hover:border-gray-200 dark:hover:border-gray-600'
|
||||||
: 'border-transparent hover:border-gray-200 dark:hover:border-gray-600'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|
@ -3825,11 +3857,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
class={`flex items-start gap-3 rounded-lg border p-2 transition-colors ${
|
class={`flex items-start gap-3 rounded-lg border p-2 transition-colors ${discoveryMode() === 'custom'
|
||||||
discoveryMode() === 'custom'
|
? 'border-blue-200 bg-blue-50/80 dark:border-blue-700 dark:bg-blue-900/20'
|
||||||
? 'border-blue-200 bg-blue-50/80 dark:border-blue-700 dark:bg-blue-900/20'
|
: 'border-transparent hover:border-gray-200 dark:hover:border-gray-600'
|
||||||
: 'border-transparent hover:border-gray-200 dark:hover:border-gray-600'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
|
|
@ -3869,11 +3900,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`rounded border px-2.5 py-1 text-[0.7rem] transition-colors ${
|
class={`rounded border px-2.5 py-1 text-[0.7rem] transition-colors ${isActive
|
||||||
isActive
|
? 'border-blue-500 bg-blue-600 text-white dark:border-blue-400 dark:bg-blue-500'
|
||||||
? 'border-blue-500 bg-blue-600 text-white dark:border-blue-400 dark:bg-blue-500'
|
: 'border-gray-300 text-gray-700 hover:border-blue-400 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-500 dark:hover:bg-blue-900/30'
|
||||||
: 'border-gray-300 text-gray-700 hover:border-blue-400 hover:bg-blue-50 dark:border-gray-600 dark:text-gray-300 dark:hover:border-blue-500 dark:hover:bg-blue-900/30'
|
}`}
|
||||||
}`}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (envOverrides().discoverySubnet) {
|
if (envOverrides().discoverySubnet) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -3958,11 +3988,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
? 'auto (scan every network phase)'
|
? 'auto (scan every network phase)'
|
||||||
: '192.168.1.0/24, 10.0.0.0/24'
|
: '192.168.1.0/24, 10.0.0.0/24'
|
||||||
}
|
}
|
||||||
class={`w-full rounded-lg border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
class={`w-full rounded-lg border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 ${envOverrides().discoverySubnet
|
||||||
envOverrides().discoverySubnet
|
? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-200 cursor-not-allowed opacity-60'
|
||||||
? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-200 cursor-not-allowed opacity-60'
|
: 'border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-900/70'
|
||||||
: 'border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-900/70'
|
}`}
|
||||||
}`}
|
|
||||||
disabled={envOverrides().discoverySubnet}
|
disabled={envOverrides().discoverySubnet}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
if (envOverrides().discoverySubnet) {
|
if (envOverrides().discoverySubnet) {
|
||||||
|
|
@ -4084,11 +4113,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
}}
|
}}
|
||||||
disabled={envOverrides().allowedOrigins}
|
disabled={envOverrides().allowedOrigins}
|
||||||
placeholder="* or https://example.com"
|
placeholder="* or https://example.com"
|
||||||
class={`w-full px-3 py-1.5 text-sm border rounded-lg ${
|
class={`w-full px-3 py-1.5 text-sm border rounded-lg ${envOverrides().allowedOrigins
|
||||||
envOverrides().allowedOrigins
|
? 'border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 cursor-not-allowed opacity-75'
|
||||||
? 'border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 cursor-not-allowed opacity-75'
|
: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800'
|
||||||
: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
{envOverrides().allowedOrigins && (
|
{envOverrides().allowedOrigins && (
|
||||||
<div class="mt-2 p-2 bg-amber-100 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-800 dark:text-amber-200">
|
<div class="mt-2 p-2 bg-amber-100 dark:bg-amber-900/30 border border-amber-300 dark:border-amber-700 rounded text-xs text-amber-800 dark:text-amber-200">
|
||||||
|
|
@ -4305,11 +4333,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
versionInfo()?.isDocker ||
|
versionInfo()?.isDocker ||
|
||||||
versionInfo()?.isSourceBuild
|
versionInfo()?.isSourceBuild
|
||||||
}
|
}
|
||||||
class={`px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-2 ${
|
class={`px-4 py-2 text-sm rounded-lg transition-colors flex items-center gap-2 ${versionInfo()?.isDocker || versionInfo()?.isSourceBuild
|
||||||
versionInfo()?.isDocker || versionInfo()?.isSourceBuild
|
? 'bg-gray-100 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
||||||
? 'bg-gray-100 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed'
|
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{checkingForUpdates() ? (
|
{checkingForUpdates() ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -5125,6 +5152,18 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<Toggle
|
||||||
|
label="Hide local login form"
|
||||||
|
description="Hide the username/password form on the login page. Users will only see SSO options unless ?show_local=true is used."
|
||||||
|
checked={hideLocalLogin()}
|
||||||
|
onChange={(e: ToggleChangeEvent) => handleHideLocalLoginChange(e.target.checked)}
|
||||||
|
disabled={hideLocalLoginLocked() || savingHideLocalLogin()}
|
||||||
|
locked={hideLocalLoginLocked()}
|
||||||
|
lockedMessage="This setting is managed by the PULSE_AUTH_HIDE_LOCAL_LOGIN environment variable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Show when={!authDisabledByEnv() && showQuickSecurityWizard()}>
|
<Show when={!authDisabledByEnv() && showQuickSecurityWizard()}>
|
||||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<QuickSecuritySetup
|
<QuickSecuritySetup
|
||||||
|
|
@ -5503,9 +5542,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>Proxy socket</span>
|
<span>Proxy socket</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
class={`px-2 py-0.5 rounded text-white text-xs ${temp().socketFound ? 'bg-green-500' : 'bg-red-500'
|
||||||
temp().socketFound ? 'bg-green-500' : 'bg-red-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{temp().socketFound ? 'Detected' : 'Missing'}
|
{temp().socketFound ? 'Detected' : 'Missing'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -5513,9 +5551,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>Daemon</span>
|
<span>Daemon</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
class={`px-2 py-0.5 rounded text-white text-xs ${temp().proxyReachable ? 'bg-green-500' : 'bg-yellow-500'
|
||||||
temp().proxyReachable ? 'bg-green-500' : 'bg-yellow-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{temp().proxyReachable ? 'Responding' : 'No response'}
|
{temp().proxyReachable ? 'Responding' : 'No response'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -5546,71 +5583,70 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<Show when={typeof temp().legacySshKeyCount === 'number'}>
|
<Show when={typeof temp().legacySshKeyCount === 'number'}>
|
||||||
<div>Legacy SSH keys: {temp().legacySshKeyCount ?? 0}</div>
|
<div>Legacy SSH keys: {temp().legacySshKeyCount ?? 0}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={temp().legacySSHDetected}>
|
<Show when={temp().legacySSHDetected}>
|
||||||
<div class="text-red-500">
|
<div class="text-red-500">
|
||||||
Legacy SSH temperature collection detected
|
Legacy SSH temperature collection detected
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
temp().controlPlaneStates &&
|
||||||
|
(temp().controlPlaneStates as TemperatureProxyControlPlaneState[]).length > 0
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
||||||
|
Control plane sync
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class={`px-2 py-0.5 rounded text-white text-xs ${temp().controlPlaneEnabled ? 'bg-green-500' : 'bg-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{temp().controlPlaneEnabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<For each={temp().controlPlaneStates || []}>
|
||||||
|
{(state) => (
|
||||||
|
<div class="rounded border border-gray-200 dark:border-gray-700 px-2 py-1.5 space-y-1">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
|
{state.instance || 'Proxy'}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class={`px-2 py-0.5 rounded text-white text-xs ${controlPlaneStatusClass(
|
||||||
|
state.status,
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{controlPlaneStatusLabel(state.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Show when={state.lastSync}>
|
||||||
|
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
||||||
|
Last sync: {formatIsoRelativeTime(state.lastSync)}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={typeof state.secondsBehind === 'number' && (state.secondsBehind || 0) > 0}>
|
||||||
|
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
||||||
|
Behind by ~{formatUptime(state.secondsBehind || 0)}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={state.refreshIntervalSeconds}>
|
||||||
|
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
||||||
|
Target interval: {formatUptime(state.refreshIntervalSeconds || 0)}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
<Show
|
||||||
<Show
|
when={
|
||||||
when={
|
temp().httpProxies && (temp().httpProxies as TemperatureProxyHTTPStatus[]).length > 0
|
||||||
temp().controlPlaneStates &&
|
}
|
||||||
(temp().controlPlaneStates as TemperatureProxyControlPlaneState[]).length > 0
|
>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
|
||||||
Control plane sync
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
|
||||||
temp().controlPlaneEnabled ? 'bg-green-500' : 'bg-gray-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{temp().controlPlaneEnabled ? 'Enabled' : 'Disabled'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<For each={temp().controlPlaneStates || []}>
|
|
||||||
{(state) => (
|
|
||||||
<div class="rounded border border-gray-200 dark:border-gray-700 px-2 py-1.5 space-y-1">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="font-medium text-gray-700 dark:text-gray-200">
|
|
||||||
{state.instance || 'Proxy'}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${controlPlaneStatusClass(
|
|
||||||
state.status,
|
|
||||||
)}`}
|
|
||||||
>
|
|
||||||
{controlPlaneStatusLabel(state.status)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Show when={state.lastSync}>
|
|
||||||
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
|
||||||
Last sync: {formatIsoRelativeTime(state.lastSync)}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={typeof state.secondsBehind === 'number' && (state.secondsBehind || 0) > 0}>
|
|
||||||
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
|
||||||
Behind by ~{formatUptime(state.secondsBehind || 0)}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={state.refreshIntervalSeconds}>
|
|
||||||
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
|
||||||
Target interval: {formatUptime(state.refreshIntervalSeconds || 0)}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
temp().httpProxies && (temp().httpProxies as TemperatureProxyHTTPStatus[]).length > 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
||||||
HTTPS proxies
|
HTTPS proxies
|
||||||
|
|
@ -5630,9 +5666,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
class={`px-2 py-0.5 rounded text-white text-xs ${proxy.reachable ? 'bg-green-500' : 'bg-red-500'
|
||||||
proxy.reachable ? 'bg-green-500' : 'bg-red-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{proxy.reachable ? 'Healthy' : 'Error'}
|
{proxy.reachable ? 'Healthy' : 'Error'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -5658,49 +5693,49 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
temp().socketHostCooldowns &&
|
temp().socketHostCooldowns &&
|
||||||
(temp().socketHostCooldowns as TemperatureProxySocketHost[]).length > 0
|
(temp().socketHostCooldowns as TemperatureProxySocketHost[]).length > 0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
<div class="mt-3 text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
<div class="font-semibold text-gray-700 dark:text-gray-200">
|
||||||
Socket cooldowns
|
Socket cooldowns
|
||||||
</div>
|
</div>
|
||||||
<For each={temp().socketHostCooldowns || []}>
|
<For each={temp().socketHostCooldowns || []}>
|
||||||
{(entry) => (
|
{(entry) => (
|
||||||
<div class="rounded border border-amber-200 dark:border-amber-700 px-2 py-1.5 space-y-1">
|
<div class="rounded border border-amber-200 dark:border-amber-700 px-2 py-1.5 space-y-1">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-700 dark:text-gray-200">
|
<div class="font-medium text-gray-700 dark:text-gray-200">
|
||||||
{entry.node || entry.host || 'Host'}
|
{entry.node || entry.host || 'Host'}
|
||||||
</div>
|
|
||||||
<Show when={entry.cooldownUntil}>
|
|
||||||
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
|
||||||
Until {entry.cooldownUntil}
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show when={entry.cooldownUntil}>
|
||||||
|
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
||||||
|
Until {entry.cooldownUntil}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<span class="px-2 py-0.5 rounded text-white text-xs bg-amber-500">
|
||||||
|
Cooling
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="px-2 py-0.5 rounded text-white text-xs bg-amber-500">
|
<Show when={typeof entry.secondsRemaining === 'number'}>
|
||||||
Cooling
|
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
||||||
</span>
|
Retrying in ~{formatUptime(entry.secondsRemaining || 0)}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={entry.lastError}>
|
||||||
|
<div class="text-[0.65rem] text-red-500">
|
||||||
|
{entry.lastError}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={typeof entry.secondsRemaining === 'number'}>
|
)}
|
||||||
<div class="text-[0.65rem] text-gray-500 dark:text-gray-400">
|
</For>
|
||||||
Retrying in ~{formatUptime(entry.secondsRemaining || 0)}
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
</Show>
|
|
||||||
<Show when={entry.lastError}>
|
|
||||||
<div class="text-[0.65rem] text-red-500">
|
|
||||||
{entry.lastError}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show
|
<Show
|
||||||
when={proxyNodeChecksSupported()}
|
when={proxyNodeChecksSupported()}
|
||||||
fallback={
|
fallback={
|
||||||
|
|
@ -5848,11 +5883,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<div class="text-xs space-y-2 text-gray-600 dark:text-gray-400">
|
<div class="text-xs space-y-2 text-gray-600 dark:text-gray-400">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-xs ${
|
class={`px-2 py-0.5 rounded text-xs ${apiDiag().enabled
|
||||||
apiDiag().enabled
|
? 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
||||||
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{apiDiag().enabled
|
{apiDiag().enabled
|
||||||
? 'Token auth enabled'
|
? 'Token auth enabled'
|
||||||
|
|
@ -5911,8 +5945,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
Last used:{' '}
|
Last used:{' '}
|
||||||
{token.lastUsedAt
|
{token.lastUsedAt
|
||||||
? formatRelativeTime(
|
? formatRelativeTime(
|
||||||
new Date(token.lastUsedAt).getTime(),
|
new Date(token.lastUsedAt).getTime(),
|
||||||
)
|
)
|
||||||
: 'Never'}
|
: 'Never'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -6005,11 +6039,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
{entry.name}
|
{entry.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-xs ${
|
class={`px-2 py-0.5 rounded text-xs ${entry.status === 'online'
|
||||||
entry.status === 'online'
|
? 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
||||||
? 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
: 'bg-red-100 text-red-700 dark:bg-red-700/40 dark:text-red-100'
|
||||||
: 'bg-red-100 text-red-700 dark:bg-red-700/40 dark:text-red-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{entry.status || 'unknown'}
|
{entry.status || 'unknown'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -6128,11 +6161,10 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
<div class="text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
<div class="text-xs text-gray-600 dark:text-gray-400 space-y-2">
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-xs ${
|
class={`px-2 py-0.5 rounded text-xs ${alerts().legacyThresholdsDetected
|
||||||
alerts().legacyThresholdsDetected
|
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
||||||
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
||||||
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Legacy thresholds{' '}
|
Legacy thresholds{' '}
|
||||||
{alerts().legacyThresholdsDetected
|
{alerts().legacyThresholdsDetected
|
||||||
|
|
@ -6140,21 +6172,19 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
: 'migrated'}
|
: 'migrated'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-xs ${
|
class={`px-2 py-0.5 rounded text-xs ${alerts().missingCooldown
|
||||||
alerts().missingCooldown
|
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
||||||
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
||||||
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Cooldown{' '}
|
Cooldown{' '}
|
||||||
{alerts().missingCooldown ? 'missing' : 'configured'}
|
{alerts().missingCooldown ? 'missing' : 'configured'}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-xs ${
|
class={`px-2 py-0.5 rounded text-xs ${alerts().missingGroupingWindow
|
||||||
alerts().missingGroupingWindow
|
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
||||||
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-700/40 dark:text-yellow-100'
|
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
||||||
: 'bg-green-100 text-green-700 dark:bg-green-700/40 dark:text-green-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Grouping window{' '}
|
Grouping window{' '}
|
||||||
{alerts().missingGroupingWindow ? 'disabled' : 'enabled'}
|
{alerts().missingGroupingWindow ? 'disabled' : 'enabled'}
|
||||||
|
|
@ -6210,9 +6240,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
{node.name}
|
{node.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
class={`px-2 py-0.5 rounded text-white text-xs ${node.connected ? 'bg-green-500' : 'bg-red-500'
|
||||||
node.connected ? 'bg-green-500' : 'bg-red-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{node.connected ? 'Connected' : 'Failed'}
|
{node.connected ? 'Connected' : 'Failed'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -6248,9 +6277,8 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
{pbs.name}
|
{pbs.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`px-2 py-0.5 rounded text-white text-xs ${
|
class={`px-2 py-0.5 rounded text-white text-xs ${pbs.connected ? 'bg-green-500' : 'bg-red-500'
|
||||||
pbs.connected ? 'bg-green-500' : 'bg-red-500'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{pbs.connected ? 'Connected' : 'Failed'}
|
{pbs.connected ? 'Connected' : 'Failed'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -6494,13 +6522,13 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
: undefined;
|
: undefined;
|
||||||
const clusterEndpoints = Array.isArray(node.clusterEndpoints)
|
const clusterEndpoints = Array.isArray(node.clusterEndpoints)
|
||||||
? (node.clusterEndpoints as Array<Record<string, unknown>>).map(
|
? (node.clusterEndpoints as Array<Record<string, unknown>>).map(
|
||||||
(ep, epIndex: number) => ({
|
(ep, epIndex: number) => ({
|
||||||
...ep,
|
...ep,
|
||||||
NodeName: `node-${epIndex + 1}`,
|
NodeName: `node-${epIndex + 1}`,
|
||||||
Host: `node-${epIndex + 1}`,
|
Host: `node-${epIndex + 1}`,
|
||||||
IP: sanitizeIP(typeof ep.IP === 'string' ? ep.IP : ''),
|
IP: sanitizeIP(typeof ep.IP === 'string' ? ep.IP : ''),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: node.clusterEndpoints;
|
: node.clusterEndpoints;
|
||||||
|
|
||||||
const sanitizedId = `${nodeType}-${index}`;
|
const sanitizedId = `${nodeType}-${index}`;
|
||||||
|
|
@ -6868,9 +6896,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
host:
|
host:
|
||||||
typeof node.host === 'string'
|
typeof node.host === 'string'
|
||||||
? (node.host as string).replace(
|
? (node.host as string).replace(
|
||||||
/https?:\/\/[^:\/]+/,
|
/https?:\/\/[^:\/]+/,
|
||||||
'https://REDACTED',
|
'https://REDACTED',
|
||||||
)
|
)
|
||||||
: node.host,
|
: node.host,
|
||||||
error:
|
error:
|
||||||
sanitizeText(
|
sanitizeText(
|
||||||
|
|
@ -6897,9 +6925,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
host:
|
host:
|
||||||
typeof pbsNode.host === 'string'
|
typeof pbsNode.host === 'string'
|
||||||
? (pbsNode.host as string).replace(
|
? (pbsNode.host as string).replace(
|
||||||
/https?:\/\/[^:\/]+/,
|
/https?:\/\/[^:\/]+/,
|
||||||
'https://REDACTED',
|
'https://REDACTED',
|
||||||
)
|
)
|
||||||
: pbsNode.host,
|
: pbsNode.host,
|
||||||
error:
|
error:
|
||||||
sanitizeText(
|
sanitizeText(
|
||||||
|
|
@ -7003,9 +7031,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
node: sanitizeHostname(alertNode),
|
node: sanitizeHostname(alertNode),
|
||||||
details: details
|
details: details
|
||||||
? details.replace(
|
? details.replace(
|
||||||
/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g,
|
/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g,
|
||||||
'xxx.xxx.xxx.xxx',
|
'xxx.xxx.xxx.xxx',
|
||||||
)
|
)
|
||||||
: details,
|
: details,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -7118,12 +7146,12 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
total: s.total,
|
total: s.total,
|
||||||
zfsPool: s.zfsPool
|
zfsPool: s.zfsPool
|
||||||
? {
|
? {
|
||||||
state: s.zfsPool.state,
|
state: s.zfsPool.state,
|
||||||
readErrors: s.zfsPool.readErrors,
|
readErrors: s.zfsPool.readErrors,
|
||||||
writeErrors: s.zfsPool.writeErrors,
|
writeErrors: s.zfsPool.writeErrors,
|
||||||
checksumErrors: s.zfsPool.checksumErrors,
|
checksumErrors: s.zfsPool.checksumErrors,
|
||||||
deviceCount: s.zfsPool.devices?.length || 0,
|
deviceCount: s.zfsPool.devices?.length || 0,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
hasBackups:
|
hasBackups:
|
||||||
(pveBackupsState()?.storageBackups ?? []).filter(
|
(pveBackupsState()?.storageBackups ?? []).filter(
|
||||||
|
|
@ -7460,13 +7488,13 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
nodes().map((n) =>
|
nodes().map((n) =>
|
||||||
n.id === editingNode()!.id
|
n.id === editingNode()!.id
|
||||||
? {
|
? {
|
||||||
...n,
|
...n,
|
||||||
...nodeData,
|
...nodeData,
|
||||||
// Update hasPassword/hasToken based on whether credentials were provided
|
// Update hasPassword/hasToken based on whether credentials were provided
|
||||||
hasPassword: nodeData.password ? true : n.hasPassword,
|
hasPassword: nodeData.password ? true : n.hasPassword,
|
||||||
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
}
|
}
|
||||||
: n,
|
: n,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -7532,12 +7560,12 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
nodes().map((n) =>
|
nodes().map((n) =>
|
||||||
n.id === editingNode()!.id
|
n.id === editingNode()!.id
|
||||||
? {
|
? {
|
||||||
...n,
|
...n,
|
||||||
...nodeData,
|
...nodeData,
|
||||||
hasPassword: nodeData.password ? true : n.hasPassword,
|
hasPassword: nodeData.password ? true : n.hasPassword,
|
||||||
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
}
|
}
|
||||||
: n,
|
: n,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -7599,12 +7627,12 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
nodes().map((n) =>
|
nodes().map((n) =>
|
||||||
n.id === editingNode()!.id
|
n.id === editingNode()!.id
|
||||||
? {
|
? {
|
||||||
...n,
|
...n,
|
||||||
...nodeData,
|
...nodeData,
|
||||||
hasPassword: nodeData.password ? true : n.hasPassword,
|
hasPassword: nodeData.password ? true : n.hasPassword,
|
||||||
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
hasToken: nodeData.tokenValue ? true : n.hasToken,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
}
|
}
|
||||||
: n,
|
: n,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ export interface SystemConfig {
|
||||||
allowEmbedding?: boolean; // Allow iframe embedding
|
allowEmbedding?: boolean; // Allow iframe embedding
|
||||||
allowedEmbedOrigins?: string; // Comma-separated list of allowed origins for embedding
|
allowedEmbedOrigins?: string; // Comma-separated list of allowed origins for embedding
|
||||||
webhookAllowedPrivateCIDRs?: string; // Comma-separated list of private CIDR ranges allowed for webhooks (e.g., "192.168.1.0/24,10.0.0.0/8")
|
webhookAllowedPrivateCIDRs?: string; // Comma-separated list of private CIDR ranges allowed for webhooks (e.g., "192.168.1.0/24,10.0.0.0/8")
|
||||||
|
hideLocalLogin?: boolean; // Hide local login form (username/password)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -121,6 +122,7 @@ export interface SecurityStatus {
|
||||||
disabled?: boolean; // legacy field (removed backend support)
|
disabled?: boolean; // legacy field (removed backend support)
|
||||||
deprecatedDisableAuth?: boolean;
|
deprecatedDisableAuth?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
hideLocalLogin?: boolean; // Hide local login form
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2852,8 +2852,22 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
|
||||||
backupEnabled := h.config.EnableBackupPolling
|
backupEnabled := h.config.EnableBackupPolling
|
||||||
settings.BackupPollingEnabled = &backupEnabled
|
settings.BackupPollingEnabled = &backupEnabled
|
||||||
|
|
||||||
|
// Create response structure that includes environment overrides
|
||||||
|
response := struct {
|
||||||
|
config.SystemSettings
|
||||||
|
EnvOverrides map[string]bool `json:"envOverrides,omitempty"`
|
||||||
|
}{
|
||||||
|
SystemSettings: settings,
|
||||||
|
EnvOverrides: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for environment variable overrides
|
||||||
|
if os.Getenv("PULSE_AUTH_HIDE_LOCAL_LOGIN") != "" {
|
||||||
|
response.EnvOverrides["hideLocalLogin"] = true
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(settings)
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleVerifyTemperatureSSH tests SSH connectivity to nodes for temperature monitoring
|
// HandleVerifyTemperatureSSH tests SSH connectivity to nodes for temperature monitoring
|
||||||
|
|
|
||||||
|
|
@ -654,6 +654,8 @@ func Load() (*Config, error) {
|
||||||
} else {
|
} else {
|
||||||
cfg.SSHPort = 22 // Default SSH port
|
cfg.SSHPort = 22 // Default SSH port
|
||||||
}
|
}
|
||||||
|
// Load HideLocalLogin
|
||||||
|
cfg.HideLocalLogin = systemSettings.HideLocalLogin
|
||||||
// APIToken no longer loaded from system.json - only from .env
|
// APIToken no longer loaded from system.json - only from .env
|
||||||
log.Info().
|
log.Info().
|
||||||
Str("updateChannel", cfg.UpdateChannel).
|
Str("updateChannel", cfg.UpdateChannel).
|
||||||
|
|
|
||||||
|
|
@ -831,6 +831,7 @@ type SystemSettings struct {
|
||||||
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")
|
||||||
|
HideLocalLogin bool `json:"hideLocalLogin"` // Hide local login form (username/password)
|
||||||
// APIToken removed - now handled via .env file only
|
// APIToken removed - now handled via .env file only
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue