// ════════════════════════════════════════════════════════════════ // /admin/settings — системные настройки (super_admin only) // ════════════════════════════════════════════════════════════════ function SettingsPage({ currentUser }) { const A = window.SDH_ADMIN; const [s, setS] = React.useState(A.settings); const [dirty, setDirty] = React.useState(false); const [showResetInvites, setShowResetInvites] = React.useState(false); const [saveMsg, setSaveMsg] = React.useState(null); function update(key, value) { setS(prev => ({ ...prev, [key]: value })); setDirty(true); setSaveMsg(null); } async function save() { setSaveMsg(null); if (window.SDH_USE_API && window.SDH_API) { try { const resp = await window.SDH_API.patch('/api/admin/settings', s); if (resp && resp.settings) { Object.assign(A.settings, resp.settings); setS(resp.settings); } setDirty(false); setSaveMsg({ type: 'ok', text: 'Сохранено' }); } catch (e) { setSaveMsg({ type: 'error', text: (e && e.message) || 'Ошибка сохранения' }); } return; } // Mock Object.assign(A.settings, s); setDirty(false); setSaveMsg({ type: 'ok', text: 'Сохранено локально (mock)' }); } if (currentUser.role !== 'super_admin') { return ; } return (
Сохранить } />
update('budget_monthly_usd', Number(e.target.value))} style={{ ...authFieldStyle, width: 120 }} /> ≈ {Math.round(s.budget_monthly_usd * 92).toLocaleString('ru-RU')} ₽

При выбранном «остановить cron» при превышении бюджета автозапуск pipeline отключится. В шапке появится bronze badge «Бюджет исчерпан». Email уведомление отключено до подключения провайдера (deferred).

update('cron_enabled', v)} />
{[1, 3, 6, 12, 24].map(h => ( ))}
update('email_alerts_cron_failure', v)} /> update('email_alerts_expired_tokens', v)} /> update('email_alerts_daily_digest', v)} />
⚠ Email отправка не работает (deferred · v1.1). Все триггеры сохраняются в очередь logs/email_queue.log, реально не уходят. Подключение провайдера планируется отдельной фазой.
update('brand_no_ai_providers', v)} disabled /> всегда включено update('brand_no_em_dash', v)} disabled /> всегда включено update('brand_russian_only', v)} disabled /> всегда включено

Действия без возврата

Сначала экспортируйте, потом нажимайте. Отменить нельзя.

{showResetInvites ? (
Отозвать все непринятые приглашения?
) : ( )}
); } function SettingsBlock({ title, help, children }) { return (

{help ? {title} : title}

{children}
); } function SettingRow({ label, children }) { return (
{label}
{children}
); } function Toggle({ value, onChange, disabled }) { return ( ); } window.SettingsPage = SettingsPage; window.Toggle = Toggle;