/* * Copyright (c) 2026 by Christian Kellner. * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause */ import React from 'react'; import { useActions, useSelector } from '../../services/state/store'; import { Divider, TimePicker, Button, Checkbox, Input, Modal } from '@douyinfe/semi-ui-19'; import { InputNumber } from '@douyinfe/semi-ui-19'; import { xhrPost } from '../../services/xhr'; import { SegmentPart } from '../../components/segment/SegmentPart'; import { Banner, Toast } from '@douyinfe/semi-ui-19'; import { downloadBackup as downloadBackupZip, precheckRestore as clientPrecheckRestore, restore as clientRestore, } from '../../services/backupRestoreClient'; import { IconSave, IconCalendar, IconRefresh, IconSignal, IconLineChartStroked, IconSearch, IconFolder, } from '@douyinfe/semi-icons'; import './GeneralSettings.less'; function formatFromTimestamp(ts) { const date = new Date(ts); return `${date.getHours()}:${date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes()}`; } function formatFromTBackend(time) { if (time == null || time.length === 0) { return null; } const date = new Date(); const split = time.split(':'); date.setHours(split[0]); date.setMinutes(split[1]); return date.getTime(); } const GeneralSettings = function GeneralSettings() { const actions = useActions(); const [loading, setLoading] = React.useState(true); const settings = useSelector((state) => state.generalSettings.settings); const [interval, setInterval] = React.useState(''); const [port, setPort] = React.useState(''); const [workingHourFrom, setWorkingHourFrom] = React.useState(null); const [workingHourTo, setWorkingHourTo] = React.useState(null); const [demoMode, setDemoMode] = React.useState(null); const [analyticsEnabled, setAnalyticsEnabled] = React.useState(null); const [sqlitePath, setSqlitePath] = React.useState(null); const fileInputRef = React.useRef(null); const [restoreModalVisible, setRestoreModalVisible] = React.useState(false); const [precheckInfo, setPrecheckInfo] = React.useState(null); const [restoreBusy, setRestoreBusy] = React.useState(false); const [selectedRestoreFile, setSelectedRestoreFile] = React.useState(null); React.useEffect(() => { async function init() { await actions.generalSettings.getGeneralSettings(); setLoading(false); } init(); }, []); React.useEffect(() => { async function init() { setInterval(settings?.interval); setPort(settings?.port); setWorkingHourFrom(settings?.workingHours?.from); setWorkingHourTo(settings?.workingHours?.to); setAnalyticsEnabled(settings?.analyticsEnabled || false); setDemoMode(settings?.demoMode || false); setSqlitePath(settings?.sqlitepath); } init(); }, [settings]); const nullOrEmpty = (val) => val == null || val.length === 0; const handleStore = async () => { if (nullOrEmpty(interval)) { Toast.error('Interval may not be empty.'); return; } if (nullOrEmpty(port)) { Toast.error('Port may not be empty.'); return; } if ( (!nullOrEmpty(workingHourFrom) && nullOrEmpty(workingHourTo)) || (nullOrEmpty(workingHourFrom) && !nullOrEmpty(workingHourTo)) ) { Toast.error('Working hours to and from must be set if either to or from has been set before.'); return; } if (nullOrEmpty(sqlitePath)) { Toast.error('SQLite db path cannot be empty.'); return; } try { await xhrPost('/api/admin/generalSettings', { interval, port, workingHours: { from: workingHourFrom, to: workingHourTo, }, demoMode, analyticsEnabled, sqlitepath: sqlitePath, }); } catch (exception) { console.error(exception); if (exception?.json?.message != null) { Toast.error(exception.json.message); } else { Toast.error('Error while trying to store settings.'); } return; } Toast.success('Settings stored successfully. We will reload your browser in 3 seconds.'); setTimeout(() => { location.reload(); }, 3000); }; const handleDownloadBackup = React.useCallback(async () => { try { await downloadBackupZip(); } catch (e) { console.error(e); Toast.error('Unexpected error while downloading backup.'); } }, []); const precheckRestore = React.useCallback(async (file) => { try { const data = await clientPrecheckRestore(file); setPrecheckInfo(data); setRestoreModalVisible(true); } catch (e) { console.error(e); Toast.error('Failed to analyze backup.'); } }, []); const performRestore = React.useCallback( async (force) => { try { setRestoreBusy(true); await clientRestore(selectedRestoreFile, force); Toast.success('Restore completed. Please restart the Fredy backend now!'); } catch (e) { console.error(e); Toast.error(e?.message || 'Unexpected error while restoring backup.'); } finally { setRestoreBusy(false); } }, [selectedRestoreFile], ); const handleSelectRestoreFile = React.useCallback( async (ev) => { const file = ev?.target?.files?.[0]; if (!file) return; setSelectedRestoreFile(file); await precheckRestore(file); // reset the input to allow same file re-select ev.target.value = ''; }, [precheckRestore], ); const handleOpenFilePicker = React.useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); return (
{!loading && (
`${value}`.replace(/\D/g, '')} onChange={(value) => setInterval(value)} suffix={'minutes'} />
`${value}`.replace(/\D/g, '')} onChange={(value) => setPort(value)} /> Warning
} style={{ marginBottom: '1rem' }} description={
Changing the path later may result in data loss.
You must restart Fredy immediately after changing this setting!
} /> { setSqlitePath(value); }} />
{ setWorkingHourFrom(val == null ? null : formatFromTimestamp(val)); }} /> { setWorkingHourTo(val == null ? null : formatFromTimestamp(val)); }} />
Explanation
} style={{ marginBottom: '1rem' }} description={
Analytics are disabled by default. If you choose to enable them, we will begin tracking the following:
The data is sent anonymously and helps me understand which providers or adapters are being used the most. In the end it helps me to improve fredy.
} /> setAnalyticsEnabled(e.target.checked)}> {' '} Enabled Explanation} style={{ marginBottom: '1rem' }} description={
In demo mode, Fredy will not (really) search for any real estates. Fredy is in a lockdown mode. Also all database files will be set back to the default values at midnight.
} /> setDemoMode(e.target.checked)}> {' '} Enabled
)} {restoreModalVisible && ( setRestoreModalVisible(false)} onOk={() => performRestore(!precheckInfo?.compatible)} okText={precheckInfo?.compatible ? 'Restore now' : 'Restore anyway'} okType={precheckInfo?.compatible ? 'primary' : 'danger'} confirmLoading={restoreBusy} > {precheckInfo?.severity === 'danger' && ( Problem detected} description={
{precheckInfo?.message}
} /> )} {precheckInfo?.severity === 'warning' && ( Automatic migrations will be applied} description={
{precheckInfo?.message}
} /> )} {precheckInfo?.severity === 'info' && ( Backup is compatible} description={
{precheckInfo?.message}
} /> )}
Backup migration: {precheckInfo?.backupMigration ?? 'unknown'} | Required migration:{' '} {precheckInfo?.requiredMigration ?? 'unknown'}
)} ); }; export default GeneralSettings;