175 lines
8.2 KiB
TypeScript
175 lines
8.2 KiB
TypeScript
'use client'
|
||
import { useEffect, useState } from 'react'
|
||
import { fetchConfig, saveConfig } from '@/lib/api/config'
|
||
import { testTelegram, downloadBackup, restoreBackup } from '@/lib/api/system'
|
||
import { useSettingsStore } from '@/store/settingsStore'
|
||
|
||
const Field = ({
|
||
label, k, cfg, onChange, type = 'text', mono = false,
|
||
}: {
|
||
label: string; k: string; cfg: Record<string, string>;
|
||
onChange: (k: string, v: string) => void; type?: string; mono?: boolean
|
||
}) => (
|
||
<div className="flex items-center gap-4">
|
||
<label className="text-xs text-g-faint w-52 shrink-0 leading-snug">{label}</label>
|
||
<input
|
||
type={type}
|
||
value={cfg[k] ?? ''}
|
||
onChange={(e) => onChange(k, e.target.value)}
|
||
className={`g-input h-8 text-sm flex-1 ${mono ? 'font-mono' : ''}`}
|
||
/>
|
||
</div>
|
||
)
|
||
|
||
const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (
|
||
<div className="g-card overflow-hidden">
|
||
<div className="px-5 py-3 border-b border-g-border/50">
|
||
<h2 className="text-xs font-semibold uppercase tracking-widest text-g-faint">{title}</h2>
|
||
</div>
|
||
<div className="p-5 space-y-3.5">
|
||
{children}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
export default function SettingsPage() {
|
||
const { config, loaded, setConfig, updateKey } = useSettingsStore()
|
||
const [saving, setSaving] = useState(false)
|
||
const [msg, setMsg] = useState('')
|
||
|
||
useEffect(() => {
|
||
if (!loaded) fetchConfig().then(setConfig).catch(() => setConfig({}))
|
||
}, [loaded, setConfig])
|
||
|
||
const save = async () => {
|
||
setSaving(true)
|
||
try { await saveConfig(config); setMsg('Saved.') }
|
||
catch { setMsg('Save failed.') }
|
||
setSaving(false)
|
||
setTimeout(() => setMsg(''), 3000)
|
||
}
|
||
|
||
const handleRestore = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (!file) return
|
||
if (!confirm('Restore this backup? Current data will be replaced.')) return
|
||
try { await restoreBackup(file); setMsg('Restored. Restart engine.') }
|
||
catch { setMsg('Restore failed.') }
|
||
}
|
||
|
||
if (!loaded) return (
|
||
<div className="flex items-center gap-2 text-xs text-g-faint">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-g-faint animate-pulse" />
|
||
Loading settings…
|
||
</div>
|
||
)
|
||
|
||
return (
|
||
<div className="space-y-6 max-w-2xl">
|
||
<div>
|
||
<h1 className="g-page-title">Settings</h1>
|
||
<p className="g-page-sub">Engine and alert configuration</p>
|
||
</div>
|
||
|
||
<Section title="Telegram">
|
||
<Field label="Bot token" k="telegram_token" cfg={config} onChange={updateKey} mono />
|
||
<Field label="Chat ID" k="telegram_chat_id" cfg={config} onChange={updateKey} mono />
|
||
<button onClick={async () => {
|
||
try { await testTelegram(); setMsg('Telegram OK!') }
|
||
catch { setMsg('Telegram failed.') }
|
||
}} className="g-btn text-xs">
|
||
Send test message
|
||
</button>
|
||
</Section>
|
||
|
||
<Section title="Engine">
|
||
<Field label="Scrape interval (s)" k="timer" cfg={config} onChange={updateKey} type="number" />
|
||
<Field label="Browser" k="browser_choice" cfg={config} onChange={updateKey} />
|
||
<Field label="Humanize level" k="humanize_level" cfg={config} onChange={updateKey} />
|
||
<Field label="Show browser (true/false)" k="show_browser" cfg={config} onChange={updateKey} />
|
||
<Field label="Incognito mode" k="incognito_mode" cfg={config} onChange={updateKey} />
|
||
<Field label="Delay launch (s)" k="delay_launch" cfg={config} onChange={updateKey} type="number" />
|
||
<Field label="Delay post-search (s)" k="delay_post_search" cfg={config} onChange={updateKey} type="number" />
|
||
<Field label="Scrape window enabled" k="scrape_window_enabled" cfg={config} onChange={updateKey} />
|
||
<Field label="Window start hour (0–23)" k="scrape_start_hour" cfg={config} onChange={updateKey} type="number" />
|
||
<Field label="Window end hour (0–23)" k="scrape_end_hour" cfg={config} onChange={updateKey} type="number" />
|
||
<Field label="Boost interval (min)" k="boost_interval_mins" cfg={config} onChange={updateKey} type="number" />
|
||
</Section>
|
||
|
||
<Section title="Alerts">
|
||
<Field label="Alert channels (telegram,discord,email)" k="alert_channels" cfg={config} onChange={updateKey} />
|
||
<Field label="Discord webhook" k="discord_webhook" cfg={config} onChange={updateKey} />
|
||
<Field label="Gmail address" k="gmail_address" cfg={config} onChange={updateKey} />
|
||
<Field label="Gmail app password" k="gmail_app_password" cfg={config} onChange={updateKey} type="password" mono />
|
||
<Field label="Email to" k="email_to" cfg={config} onChange={updateKey} />
|
||
<Field label="Closing alerts (true/false)" k="closing_alert_enabled" cfg={config} onChange={updateKey} />
|
||
<Field label="Alert schedule (min, e.g. 60,30,10)" k="closing_alert_schedule" cfg={config} onChange={updateKey} />
|
||
</Section>
|
||
|
||
<Section title="Currency">
|
||
<Field label="Display currency (blank = raw)" k="display_currency" cfg={config} onChange={updateKey} />
|
||
</Section>
|
||
|
||
<Section title="Proxy">
|
||
<Field label="Proxy enabled (true/false)" k="proxy_enabled" cfg={config} onChange={updateKey} />
|
||
<div className="flex items-start gap-4">
|
||
<label className="text-xs text-g-faint w-52 shrink-0 pt-1.5">Proxy list</label>
|
||
<textarea
|
||
value={config['proxy_list'] ?? ''}
|
||
onChange={(e) => updateKey('proxy_list', e.target.value)}
|
||
rows={3}
|
||
placeholder={"http://proxy1:8080\nsocks5://proxy2:1080"}
|
||
className="g-input text-sm font-mono resize-none flex-1"
|
||
/>
|
||
</div>
|
||
</Section>
|
||
|
||
<Section title="CAPTCHA">
|
||
<Field label="Solver (2captcha / capsolver)" k="captcha_solver" cfg={config} onChange={updateKey} />
|
||
<Field label="API key" k="captcha_api_key" cfg={config} onChange={updateKey} type="password" mono />
|
||
</Section>
|
||
|
||
<Section title="AI Filter">
|
||
<Field label="AI enabled (true/false)" k="ai_filter_enabled" cfg={config} onChange={updateKey} />
|
||
<Field label="AI provider (groq/ollama)" k="ai_provider" cfg={config} onChange={updateKey} />
|
||
<Field label="AI model" k="ai_model" cfg={config} onChange={updateKey} mono />
|
||
<Field label="Groq API key" k="ai_api_key" cfg={config} onChange={updateKey} type="password" mono />
|
||
<Field label="Ollama URL" k="ai_base_url" cfg={config} onChange={updateKey} />
|
||
<Field label="AI debug (true/false)" k="ai_debug" cfg={config} onChange={updateKey} />
|
||
<Field label="Auto-adapt (true/false)" k="auto_adapt_enabled" cfg={config} onChange={updateKey} />
|
||
</Section>
|
||
|
||
<Section title="Database">
|
||
<Field label="DB URL (blank = SQLite)" k="db_url" cfg={config} onChange={updateKey} mono />
|
||
<Field label="Auto-disable after N failures (0 = never)" k="site_auto_disable_after" cfg={config} onChange={updateKey} type="number" />
|
||
</Section>
|
||
|
||
<Section title="Backup & Restore">
|
||
<div className="flex gap-2">
|
||
<button onClick={downloadBackup} className="g-btn text-xs">Download backup</button>
|
||
<label className="g-btn text-xs cursor-pointer">
|
||
Restore backup
|
||
<input type="file" accept=".db" onChange={handleRestore} className="hidden" />
|
||
</label>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* Save bar */}
|
||
<div className="flex gap-3 items-center pt-2">
|
||
<button
|
||
onClick={save}
|
||
disabled={saving}
|
||
className="g-btn-primary text-sm px-5 disabled:opacity-50"
|
||
>
|
||
{saving ? 'Saving…' : 'Save settings'}
|
||
</button>
|
||
{msg && (
|
||
<span className={`text-xs ${msg.includes('fail') || msg.includes('fail') ? 'text-g-red' : 'text-g-green'}`}>
|
||
{msg}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|