refactor(proxysettings): Enhance proxy settings UI by categorizing advanced options and improving documentation. The channel_client_wait_period and channel_init_grace_period are now marked as advanced settings, and the UI has been updated to allow users to expand and view these options. Additionally, the changelog has been updated to reflect these changes and clarify the behavior of grace periods.

This commit is contained in:
SergeantPanda 2026-06-30 10:00:22 -05:00
parent 391c3412d2
commit ac6d43af8c
4 changed files with 167 additions and 69 deletions

View file

@ -9,15 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **New proxy setting: Client Connect Grace Period (`channel_client_wait_period`, default 5s).** Restores the grace-period behaviour that was unintentionally dropped in 0.27.1. When a channel's buffer becomes ready but no client has connected yet (e.g. after an API warmup), this controls how long to keep the channel alive before tearing down.
- **New proxy setting: Client Connect Grace Period (`channel_client_wait_period`, default 5s).** Adds a dedicated timeout for channels that have filled their buffer but still have no viewers (`waiting_for_clients`). Previously that window reused `channel_shutdown_delay` (default 0s), so those channels were torn down almost immediately.
### Changed
- **Proxy grace-period settings are now split into three distinct timeouts.** In 0.27.1, `channel_init_grace_period` was repurposed as the channel startup/failover timeout (replacing a hardcoded 10s). That left API-warmup "wait for the first client" behaviour tied to `channel_shutdown_delay` (default 0s), so warmed-up channels stopped almost immediately. Behaviour is now:
- **Proxy grace-period settings are now split into three distinct timeouts.** The cleanup watchdog already applied `channel_init_grace_period` while a channel was still connecting (buffer not ready) and reused `channel_shutdown_delay` once `connection_ready_time` was set, including for `waiting_for_clients` with zero viewers. With the default `channel_shutdown_delay` of 0s, a buffered channel waiting for its first viewer was stopped almost immediately; raising shutdown delay was the only workaround, but that also delayed teardown after real disconnects. Behaviour is now:
- **`channel_init_grace_period` (default 60s, max 300s):** how long the proxy may spend connecting and cycling failover streams before giving up on startup.
- **`channel_client_wait_period` (default 5s):** how long a ready channel with no viewers stays up waiting for the first client (the original grace-period use case).
- **`channel_shutdown_delay` (default 0s):** delay after the last client disconnects only; no longer applies to warmup.
- **Migration 0026 bumps `channel_init_grace_period` to 60s when the stored value is below 60.** Existing installs on the old 5s default (or any custom value under 60) are raised automatically. Values already at 60s or higher are unchanged. If you previously raised `channel_shutdown_delay` to keep warmed-up channels alive, set `channel_client_wait_period` instead.
- **`channel_shutdown_delay` (default 0s):** delay after the last client disconnects only; no longer applies when the buffer is ready but no viewer has connected yet.
- **Migration 0026 bumps `channel_init_grace_period` to 60s when the stored value is below 60.** Existing installs on the old 5s default (or any custom value under 60) are raised automatically. Values already at 60s or higher are unchanged. If you previously raised `channel_shutdown_delay` to keep buffered channels alive with no viewers, set `channel_client_wait_period` instead (Settings → Proxy → Advanced).
- **Proxy settings UI: less-used options moved under Advanced.** Settings → Proxy now shows the day-to-day tuning fields by default (`buffering_timeout`, `buffering_speed`, `channel_shutdown_delay`, `new_client_behind_seconds`). **Buffer Chunk TTL**, **Channel Initialization Timeout**, and **Client Connect Grace Period** are tucked under **Show Advanced Settings**.
## [0.27.1] - 2026-06-25

View file

@ -5,20 +5,20 @@ import { updateSetting } from '../../../utils/pages/SettingsUtils.js';
import {
Alert,
Button,
Collapse,
Flex,
NumberInput,
Stack,
TextInput,
} from '@mantine/core';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { PROXY_SETTINGS_OPTIONS } from '../../../constants.js';
import {
getProxySettingDefaults,
getProxySettingsFormInitialValues,
} from '../../../utils/forms/settings/ProxySettingsFormUtils.js';
const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
const isNumericField = (key) => {
// Determine if this field should be a NumberInput
return [
'buffering_timeout',
'redis_chunk_ttl',
@ -28,25 +28,19 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
'new_client_behind_seconds',
].includes(key);
};
const isFloatField = (key) => {
return key === 'buffering_speed';
};
const isFloatField = (key) => key === 'buffering_speed';
const getNumericFieldMax = (key) => {
return key === 'buffering_timeout'
? 300
: key === 'redis_chunk_ttl'
? 3600
: key === 'channel_shutdown_delay'
? 300
: key === 'channel_client_wait_period'
? 300
: key === 'new_client_behind_seconds'
? 120
: 300;
if (key === 'buffering_timeout') return 300;
if (key === 'redis_chunk_ttl') return 3600;
if (key === 'channel_shutdown_delay') return 300;
if (key === 'channel_client_wait_period') return 300;
if (key === 'new_client_behind_seconds') return 120;
return 300;
};
return (
<>
{Object.entries(PROXY_SETTINGS_OPTIONS).map(([key, config]) => {
const renderProxySettingField = (key, config, proxySettingsForm) => {
if (isNumericField(key)) {
return (
<NumberInput
@ -58,7 +52,9 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
max={getNumericFieldMax(key)}
/>
);
} else if (isFloatField(key)) {
}
if (isFloatField(key)) {
return (
<NumberInput
key={key}
@ -71,7 +67,8 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
precision={1}
/>
);
} else {
}
return (
<TextInput
key={key}
@ -80,8 +77,47 @@ const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
description={config.description || null}
/>
);
};
const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => {
const [advancedOpen, setAdvancedOpen] = useState(false);
const entries = Object.entries(PROXY_SETTINGS_OPTIONS);
const mainEntries = entries.filter(([, config]) => !config.advanced);
const advancedEntries = entries.filter(([, config]) => config.advanced);
return (
<>
{mainEntries.map(([key, config]) =>
renderProxySettingField(key, config, proxySettingsForm)
)}
{advancedEntries.length > 0 && (
<>
<Button
variant="subtle"
size="xs"
leftSection={
advancedOpen ? (
<ChevronDown size={12} />
) : (
<ChevronRight size={12} />
)
}
})}
onClick={() => setAdvancedOpen((open) => !open)}
c="dimmed"
styles={{ root: { alignSelf: 'flex-start' } }}
>
{advancedOpen ? 'Hide' : 'Show'} Advanced Settings
</Button>
<Collapse in={advancedOpen}>
<Stack gap="sm">
{advancedEntries.map(([key, config]) =>
renderProxySettingField(key, config, proxySettingsForm)
)}
</Stack>
</Collapse>
</>
)}
</>
);
});

View file

@ -14,6 +14,21 @@ vi.mock('../../../../constants.js', () => ({
description: 'Speed multiplier',
},
redis_url: { label: 'Redis URL', description: 'Redis connection URL' },
redis_chunk_ttl: {
label: 'Buffer Chunk TTL',
advanced: true,
description: 'Chunk TTL',
},
channel_init_grace_period: {
label: 'Channel Initialization Timeout',
advanced: true,
description: 'Init timeout',
},
channel_client_wait_period: {
label: 'Client Connect Grace Period',
advanced: true,
description: 'Advanced grace period',
},
},
}));
@ -71,6 +86,8 @@ vi.mock('@mantine/core', () => ({
</div>
),
Stack: ({ children }) => <div>{children}</div>,
Collapse: ({ in: isOpen, children }) =>
isOpen ? <div data-testid="collapse-open">{children}</div> : null,
TextInput: ({ label, description, ...rest }) => (
<div>
<label>{label}</label>
@ -353,11 +370,52 @@ describe('ProxySettingsForm', () => {
// ProxySettingsOptions field routing
describe('ProxySettingsOptions field routing', () => {
it('calls getInputProps for each PROXY_SETTINGS_OPTIONS key', () => {
it('calls getInputProps for main settings on initial render', () => {
render(<ProxySettingsForm active={true} />);
expect(formMock.getInputProps).toHaveBeenCalledWith('buffering_timeout');
expect(formMock.getInputProps).toHaveBeenCalledWith('buffering_speed');
expect(formMock.getInputProps).toHaveBeenCalledWith('redis_url');
expect(formMock.getInputProps).not.toHaveBeenCalledWith(
'channel_client_wait_period'
);
expect(formMock.getInputProps).not.toHaveBeenCalledWith(
'channel_init_grace_period'
);
expect(formMock.getInputProps).not.toHaveBeenCalledWith('redis_chunk_ttl');
});
it('hides advanced settings until expanded', () => {
render(<ProxySettingsForm active={true} />);
expect(
screen.queryByTestId('number-input-Client Connect Grace Period')
).not.toBeInTheDocument();
expect(
screen.queryByTestId('number-input-Channel Initialization Timeout')
).not.toBeInTheDocument();
expect(
screen.queryByTestId('number-input-Buffer Chunk TTL')
).not.toBeInTheDocument();
expect(screen.getByText('Show Advanced Settings')).toBeInTheDocument();
});
it('shows advanced settings when expanded', () => {
render(<ProxySettingsForm active={true} />);
fireEvent.click(screen.getByText('Show Advanced Settings'));
expect(screen.getByTestId('collapse-open')).toBeInTheDocument();
expect(
screen.getByTestId('number-input-Client Connect Grace Period')
).toBeInTheDocument();
expect(
screen.getByTestId('number-input-Channel Initialization Timeout')
).toBeInTheDocument();
expect(screen.getByTestId('number-input-Buffer Chunk TTL')).toBeInTheDocument();
expect(formMock.getInputProps).toHaveBeenCalledWith(
'channel_client_wait_period'
);
expect(formMock.getInputProps).toHaveBeenCalledWith(
'channel_init_grace_period'
);
expect(formMock.getInputProps).toHaveBeenCalledWith('redis_chunk_ttl');
});
});
});

View file

@ -43,6 +43,7 @@ export const PROXY_SETTINGS_OPTIONS = {
},
redis_chunk_ttl: {
label: 'Buffer Chunk TTL',
advanced: true,
description:
'Time-to-live for buffer chunks in seconds (how long stream data is cached)',
},
@ -53,13 +54,15 @@ export const PROXY_SETTINGS_OPTIONS = {
},
channel_init_grace_period: {
label: 'Channel Initialization Timeout',
advanced: true,
description:
'Maximum seconds to wait for the initial buffer to fill while a channel is connecting. Channels that never receive enough buffered data are stopped after this limit.',
},
channel_client_wait_period: {
label: 'Client Connect Grace Period',
advanced: true,
description:
'Seconds to keep a ready channel alive when no client has connected yet (e.g. after an API warmup). Once a client connects the channel becomes active; if none connect within this window the channel stops.',
'Seconds to keep a buffered channel alive when no viewer is connected yet. Rarely needed unless you start channels programmatically.',
},
new_client_behind_seconds: {
label: 'New Client Buffer (seconds)',