diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a9bcaf..309ed5c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/frontend/src/components/forms/settings/ProxySettingsForm.jsx b/frontend/src/components/forms/settings/ProxySettingsForm.jsx index a3a9600a..91b36bb7 100644 --- a/frontend/src/components/forms/settings/ProxySettingsForm.jsx +++ b/frontend/src/components/forms/settings/ProxySettingsForm.jsx @@ -5,83 +5,119 @@ 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 isNumericField = (key) => { + return [ + 'buffering_timeout', + 'redis_chunk_ttl', + 'channel_shutdown_delay', + 'channel_init_grace_period', + 'channel_client_wait_period', + 'new_client_behind_seconds', + ].includes(key); +}; + +const isFloatField = (key) => key === 'buffering_speed'; + +const getNumericFieldMax = (key) => { + 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; +}; + +const renderProxySettingField = (key, config, proxySettingsForm) => { + if (isNumericField(key)) { + return ( + + ); + } + + if (isFloatField(key)) { + return ( + + ); + } + + return ( + + ); +}; + const ProxySettingsOptions = React.memo(({ proxySettingsForm }) => { - const isNumericField = (key) => { - // Determine if this field should be a NumberInput - return [ - 'buffering_timeout', - 'redis_chunk_ttl', - 'channel_shutdown_delay', - 'channel_init_grace_period', - 'channel_client_wait_period', - 'new_client_behind_seconds', - ].includes(key); - }; - const isFloatField = (key) => { - return 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; - }; + 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 ( <> - {Object.entries(PROXY_SETTINGS_OPTIONS).map(([key, config]) => { - if (isNumericField(key)) { - return ( - - ); - } else if (isFloatField(key)) { - return ( - - ); - } else { - return ( - - ); - } - })} + {mainEntries.map(([key, config]) => + renderProxySettingField(key, config, proxySettingsForm) + )} + + {advancedEntries.length > 0 && ( + <> + + + + {advancedEntries.map(([key, config]) => + renderProxySettingField(key, config, proxySettingsForm) + )} + + + + )} ); }); diff --git a/frontend/src/components/forms/settings/__tests__/ProxySettingsForm.test.jsx b/frontend/src/components/forms/settings/__tests__/ProxySettingsForm.test.jsx index c874b3e6..b0fa3a56 100644 --- a/frontend/src/components/forms/settings/__tests__/ProxySettingsForm.test.jsx +++ b/frontend/src/components/forms/settings/__tests__/ProxySettingsForm.test.jsx @@ -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', () => ({ ), Stack: ({ children }) =>
{children}
, + Collapse: ({ in: isOpen, children }) => + isOpen ?
{children}
: null, TextInput: ({ label, description, ...rest }) => (
@@ -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(); 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(); + 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(); + 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'); }); }); }); diff --git a/frontend/src/constants.js b/frontend/src/constants.js index fba0d7c6..ea6dec13 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -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)',