// ════════════════════════════════════════════════════════════════
// /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')} ₽
update('budget_action', e.target.value)} style={window.selectStyle}>
остановить cron автоматически
только предупредить, не останавливать
блокировать все новые LLM-операции
При выбранном «остановить cron» при превышении бюджета автозапуск pipeline отключится. В шапке появится bronze badge «Бюджет исчерпан». Email уведомление отключено до подключения провайдера (deferred).
update('cron_enabled', v)} />
{[1, 3, 6, 12, 24].map(h => (
update('cron_frequency_hours', h)}
style={{ padding: '4px 10px', fontSize: 12 }}>{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 />
всегда включено
Действия без возврата
Сначала экспортируйте, потом нажимайте. Отменить нельзя.
Экспорт всех данных (JSON)
{showResetInvites ? (
Отозвать все непринятые приглашения?
setShowResetInvites(false)}>Отмена
setShowResetInvites(false)}>Отозвать
) : (
setShowResetInvites(true)}>
Сбросить все непринятые приглашения
)}
);
}
function SettingsBlock({ title, help, children }) {
return (
{help ? {title} : title}
{children}
);
}
function SettingRow({ label, children }) {
return (
);
}
function Toggle({ value, onChange, disabled }) {
return (
!disabled && onChange(!value)} disabled={disabled}
style={{
width: 38, height: 22,
background: value ? 'var(--brand-bronze)' : 'var(--border-default)',
opacity: disabled ? 0.6 : 1,
borderRadius: 11, border: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
position: 'relative',
padding: 0,
transition: 'background .15s',
}}>
);
}
window.SettingsPage = SettingsPage;
window.Toggle = Toggle;