feat: add keepalive controls to System Settings

- Introduced optional keepalive settings: `keepalive_idle` and `keepalive_count` in the Server struct.
- Implemented UI controls for keepalive settings in System Settings, including validation and persistence.
- Added localization support for new keepalive fields in multiple languages.
- Created a manual test tracking plan for verifying keepalive controls and their behavior.
- Updated existing tests to cover new functionality and ensure proper validation of keepalive inputs.
- Ensured safe defaults and fallback behavior for missing or invalid keepalive values.
This commit is contained in:
GitHub Actions
2026-02-23 19:33:14 +00:00
parent 9424aca5e2
commit ee5350d675
19 changed files with 938 additions and 105 deletions
@@ -58,6 +58,8 @@ describe('SystemSettings', () => {
vi.mocked(settingsApi.getSettings).mockResolvedValue({
'caddy.admin_api': 'http://localhost:2019',
'caddy.ssl_provider': 'auto',
'caddy.keepalive_idle': '',
'caddy.keepalive_count': '',
'ui.domain_link_behavior': 'new_tab',
'security.cerberus.enabled': 'false',
})
@@ -162,6 +164,34 @@ describe('SystemSettings', () => {
})
})
it('loads keepalive settings when present', async () => {
vi.mocked(settingsApi.getSettings).mockResolvedValue({
'caddy.admin_api': 'http://localhost:2019',
'caddy.ssl_provider': 'auto',
'caddy.keepalive_idle': '2m',
'caddy.keepalive_count': '5',
'ui.domain_link_behavior': 'new_tab',
})
renderWithProviders(<SystemSettings />)
await waitFor(() => {
const keepaliveIdleInput = screen.getByLabelText('Keepalive Idle (Optional)') as HTMLInputElement
const keepaliveCountInput = screen.getByLabelText('Keepalive Count (Optional)') as HTMLInputElement
expect(keepaliveIdleInput.value).toBe('2m')
expect(keepaliveCountInput.value).toBe('5')
})
})
it('renders keepalive controls in General settings', async () => {
renderWithProviders(<SystemSettings />)
await waitFor(() => {
expect(screen.getByLabelText('Keepalive Idle (Optional)')).toBeInTheDocument()
expect(screen.getByLabelText('Keepalive Count (Optional)')).toBeInTheDocument()
})
})
it('saves all settings when save button is clicked', async () => {
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
@@ -176,7 +206,7 @@ describe('SystemSettings', () => {
await user.click(saveButtons[0])
await waitFor(() => {
expect(settingsApi.updateSetting).toHaveBeenCalledTimes(4)
expect(settingsApi.updateSetting).toHaveBeenCalledTimes(6)
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'caddy.admin_api',
expect.any(String),
@@ -189,6 +219,18 @@ describe('SystemSettings', () => {
'caddy',
'string'
)
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'caddy.keepalive_idle',
'',
'caddy',
'string'
)
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'caddy.keepalive_count',
'',
'caddy',
'string'
)
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'ui.domain_link_behavior',
expect.any(String),
@@ -197,6 +239,62 @@ describe('SystemSettings', () => {
)
})
})
it('saves keepalive settings when valid values are provided', async () => {
vi.mocked(settingsApi.updateSetting).mockResolvedValue(undefined)
renderWithProviders(<SystemSettings />)
await waitFor(() => {
expect(screen.getByLabelText('Keepalive Idle (Optional)')).toBeInTheDocument()
})
const user = userEvent.setup()
const keepaliveIdleInput = screen.getByLabelText('Keepalive Idle (Optional)')
const keepaliveCountInput = screen.getByLabelText('Keepalive Count (Optional)')
await user.clear(keepaliveIdleInput)
await user.type(keepaliveIdleInput, '30s')
await user.clear(keepaliveCountInput)
await user.type(keepaliveCountInput, '3')
const saveButtons = screen.getAllByRole('button', { name: /Save Settings/i })
await user.click(saveButtons[0])
await waitFor(() => {
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'caddy.keepalive_idle',
'30s',
'caddy',
'string'
)
expect(settingsApi.updateSetting).toHaveBeenCalledWith(
'caddy.keepalive_count',
'3',
'caddy',
'string'
)
})
})
it('disables save when keepalive values are invalid', async () => {
renderWithProviders(<SystemSettings />)
await waitFor(() => {
expect(screen.getByLabelText('Keepalive Idle (Optional)')).toBeInTheDocument()
})
const user = userEvent.setup()
const keepaliveIdleInput = screen.getByLabelText('Keepalive Idle (Optional)')
await user.clear(keepaliveIdleInput)
await user.type(keepaliveIdleInput, 'invalid-duration')
await waitFor(() => {
expect(screen.getByText('Enter a valid duration (for example: 30s, 2m, 1h).')).toBeInTheDocument()
})
const saveButtons = screen.getAllByRole('button', { name: /Save Settings/i })
expect(saveButtons[0]).toBeDisabled()
})
})
describe('System Status', () => {