Files
NeoTachyon/fe-app-tachyon/src/tachyon/tabs/dashboard/partials/renderAdvancedSettings.ts
T

1214 lines
39 KiB
TypeScript

import { TachyonShellMethods } from '../../../methods';
import { showToast } from '../../../../helpers/showToast';
import { getConfigSections } from '../../../methods/custom/getConfigSections';
import { TACHYON_UCI_PACKAGE } from '../../../../constants';
// ─── Types ───────────────────────────────────────────────────────────────────
interface AdvancedSettingsState {
watchdogRunning: boolean;
watchdogLoading: boolean;
smartDetectEnabled: boolean;
smartDetectSections: string[];
allSectionNames: string[];
deviceIpsPerSection: Record<string, string[]>;
dnsTurboCache: boolean;
agentApiToken: string;
enableAiDoctor: boolean;
aiDoctorProvider: string;
aiDoctorModel: string;
aiDoctorLang: string;
aiDoctorApiKey: string;
aiDoctorCustomUrl: string;
aiWatchdog: {
proxyHealthEnabled: boolean;
proxyHealthInterval: string;
proxyHealthFailThreshold: string;
proxyHealthUrl: string;
dnsContinuousEnabled: boolean;
dnsInterval: string;
reloadDedupEnabled: boolean;
metricsEnabled: boolean;
metricsRetentionHours: string;
smartCooldownsEnabled: boolean;
configValidationEnabled: boolean;
gracefulDegradationEnabled: boolean;
persistentSmartDetect: boolean;
adaptiveIntervalsEnabled: boolean;
anomalyDetectionEnabled: boolean;
anomalyReconnectThreshold: string;
};
saving: boolean;
loaded: boolean;
}
let _state: AdvancedSettingsState = {
watchdogRunning: false,
watchdogLoading: false,
smartDetectEnabled: false,
smartDetectSections: [],
allSectionNames: [],
deviceIpsPerSection: {},
dnsTurboCache: false,
agentApiToken: '',
enableAiDoctor: false,
aiDoctorProvider: 'openai',
aiDoctorModel: '',
aiDoctorLang: 'ru',
aiDoctorApiKey: '',
aiDoctorCustomUrl: '',
aiWatchdog: {
proxyHealthEnabled: true,
proxyHealthInterval: '30',
proxyHealthFailThreshold: '3',
proxyHealthUrl: 'https://cp.cloudflare.com/generate_204',
dnsContinuousEnabled: true,
dnsInterval: '60',
reloadDedupEnabled: true,
metricsEnabled: true,
metricsRetentionHours: '24',
smartCooldownsEnabled: true,
configValidationEnabled: true,
gracefulDegradationEnabled: true,
persistentSmartDetect: true,
adaptiveIntervalsEnabled: true,
anomalyDetectionEnabled: true,
anomalyReconnectThreshold: '10',
},
saving: false,
loaded: false,
};
// ─── Rerender ────────────────────────────────────────────────────────────────
function rerender() {
const el = document.getElementById('tachyon-advanced-settings');
if (!el) return;
const inner = document.getElementById('tachyon-advanced-settings-inner');
if (!inner) return;
const next = renderAdvancedSettingsBody(_state);
inner.replaceChildren(...(Array.isArray(next) ? next : [next]));
}
// ─── Data loading ─────────────────────────────────────────────────────────────
export async function loadAdvancedSettingsState() {
const sections = await getConfigSections();
const settingsSec = sections.find((s) => s['.type'] === 'settings');
const smartDetect = settingsSec?.smart_detect === '1';
const raw = settingsSec?.smart_detect_sections;
const smartDetectSections: string[] = Array.isArray(raw)
? raw
: raw && typeof raw === 'string' && raw.trim()
? [raw.trim()]
: [];
const ruleSections = sections.filter(
(s) =>
s['.type'] === 'section' &&
s.enabled !== '0' &&
(s.action as string) !== 'bypass' &&
(s.action as string) !== 'block' &&
(s.action as string) !== 'dns',
);
const allSectionNames = ruleSections.map((s) => s['.name'] as string);
const deviceIpsPerSection: Record<string, string[]> = {};
for (const s of ruleSections) {
const name = s['.name'] as string;
const ips = s.fully_routed_ips;
deviceIpsPerSection[name] = Array.isArray(ips)
? ips
: ips && typeof ips === 'string' && ips.trim()
? [ips.trim()]
: [];
}
const wdRes = await TachyonShellMethods.getWatchdogStatus();
const watchdogRunning: boolean = wdRes.success
? Boolean((wdRes as { data: { running: boolean } }).data.running)
: false;
_state = {
..._state,
watchdogRunning,
smartDetectEnabled: smartDetect,
smartDetectSections:
smartDetectSections.length > 0
? smartDetectSections
: allSectionNames.slice(0, 1),
allSectionNames,
deviceIpsPerSection,
dnsTurboCache: settingsSec?.dns_turbo_cache === '1',
agentApiToken:
((settingsSec as unknown as Record<string, unknown>)
?.agent_api_token as string) || '',
enableAiDoctor:
(settingsSec as unknown as Record<string, unknown>)?.enable_ai_doctor ===
'1',
aiDoctorProvider:
((settingsSec as unknown as Record<string, unknown>)
?.ai_doctor_provider as string) || 'openai',
aiDoctorModel:
((settingsSec as unknown as Record<string, unknown>)
?.ai_doctor_model as string) || '',
aiDoctorLang:
((settingsSec as unknown as Record<string, unknown>)
?.ai_doctor_lang as string) || 'ru',
aiDoctorApiKey:
((settingsSec as unknown as Record<string, unknown>)
?.ai_doctor_api_key as string) || '',
aiDoctorCustomUrl:
((settingsSec as unknown as Record<string, unknown>)
?.ai_doctor_custom_url as string) || '',
aiWatchdog: {
proxyHealthEnabled: settingsSec?.ai_proxy_health_enabled !== '0',
proxyHealthInterval:
(settingsSec?.ai_proxy_health_interval as string) || '30',
proxyHealthFailThreshold:
(settingsSec?.ai_proxy_health_fail_threshold as string) || '3',
proxyHealthUrl:
(settingsSec?.ai_proxy_health_url as string) ||
'https://cp.cloudflare.com/generate_204',
dnsContinuousEnabled: settingsSec?.ai_dns_continuous_enabled !== '0',
dnsInterval: (settingsSec?.ai_dns_interval as string) || '60',
reloadDedupEnabled: settingsSec?.ai_reload_dedup_enabled !== '0',
metricsEnabled: settingsSec?.ai_metrics_enabled !== '0',
metricsRetentionHours:
(settingsSec?.ai_metrics_retention_hours as string) || '24',
smartCooldownsEnabled: settingsSec?.ai_smart_cooldowns_enabled !== '0',
configValidationEnabled:
settingsSec?.ai_config_validation_enabled !== '0',
gracefulDegradationEnabled:
settingsSec?.ai_graceful_degradation_enabled !== '0',
persistentSmartDetect: settingsSec?.ai_persistent_smart_detect !== '0',
adaptiveIntervalsEnabled:
settingsSec?.ai_adaptive_intervals_enabled !== '0',
anomalyDetectionEnabled:
settingsSec?.ai_anomaly_detection_enabled !== '0',
anomalyReconnectThreshold:
(settingsSec?.ai_anomaly_reconnect_threshold as string) || '10',
},
loaded: true,
};
rerender();
}
// ─── Actions ──────────────────────────────────────────────────────────────────
async function toggleWatchdog() {
_state = { ..._state, watchdogLoading: true };
rerender();
if (_state.watchdogRunning) {
await TachyonShellMethods.watchdogStop();
} else {
await TachyonShellMethods.watchdogStart();
}
const wdRes = await TachyonShellMethods.getWatchdogStatus();
const running = wdRes.success
? (wdRes as { data: { running: boolean } }).data.running
: _state.watchdogRunning;
_state = { ..._state, watchdogRunning: running, watchdogLoading: false };
rerender();
}
async function saveSmartDetect() {
_state = { ..._state, saving: true };
rerender();
try {
await TachyonShellMethods.uciRunCommand([
'set',
`${TACHYON_UCI_PACKAGE}.settings.smart_detect=${_state.smartDetectEnabled ? '1' : '0'}`,
]);
await TachyonShellMethods.uciRunCommand([
'delete',
`${TACHYON_UCI_PACKAGE}.settings.smart_detect_sections`,
]);
for (const sec of _state.smartDetectSections) {
await TachyonShellMethods.uciRunCommand([
'add_list',
`${TACHYON_UCI_PACKAGE}.settings.smart_detect_sections=${sec}`,
]);
}
await TachyonShellMethods.uciRunCommand(['commit', TACHYON_UCI_PACKAGE]);
showToast(_('Smart Detect settings saved'), 'success');
} catch {
showToast(_('Failed to save Smart Detect settings'), 'error');
}
_state = { ..._state, saving: false };
rerender();
}
async function saveDeviceIps(sectionName: string, ipsText: string) {
_state = { ..._state, saving: true };
rerender();
try {
const ips = ipsText
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
await TachyonShellMethods.uciRunCommand([
'delete',
`${TACHYON_UCI_PACKAGE}.${sectionName}.fully_routed_ips`,
]);
for (const ip of ips) {
await TachyonShellMethods.uciRunCommand([
'add_list',
`${TACHYON_UCI_PACKAGE}.${sectionName}.fully_routed_ips=${ip}`,
]);
}
await TachyonShellMethods.uciRunCommand(['commit', TACHYON_UCI_PACKAGE]);
_state.deviceIpsPerSection[sectionName] = ips;
showToast(_('Device IPs saved'), 'success');
// Reload tachyon async so UI stays responsive
void TachyonShellMethods.uciRunCommand([
'-q',
'commit',
TACHYON_UCI_PACKAGE,
]);
} catch {
showToast(_('Failed to save device IPs'), 'error');
}
_state = { ..._state, saving: false };
rerender();
}
async function saveDnsTurboCache(enabled: boolean) {
_state = { ..._state, saving: true };
rerender();
try {
await TachyonShellMethods.uciRunCommand([
'set',
`${TACHYON_UCI_PACKAGE}.settings.dns_turbo_cache=${enabled ? '1' : '0'}`,
]);
await TachyonShellMethods.uciRunCommand(['commit', TACHYON_UCI_PACKAGE]);
_state = { ..._state, dnsTurboCache: enabled };
showToast(_('DNS Turbo Cache saved'), 'success');
} catch {
showToast(_('Failed to save DNS Turbo Cache'), 'error');
}
_state = { ..._state, saving: false };
rerender();
}
async function saveAiWatchdogSettings() {
_state = { ..._state, saving: true };
rerender();
try {
const ai = _state.aiWatchdog;
const cmds: string[][] = [
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.agent_api_token=${_state.agentApiToken}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.enable_ai_doctor=${_state.enableAiDoctor ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_doctor_provider=${_state.aiDoctorProvider}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_doctor_model=${_state.aiDoctorModel}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_doctor_lang=${_state.aiDoctorLang}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_doctor_api_key=${_state.aiDoctorApiKey}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_doctor_custom_url=${_state.aiDoctorCustomUrl}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_proxy_health_enabled=${ai.proxyHealthEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_proxy_health_interval=${ai.proxyHealthInterval}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_proxy_health_fail_threshold=${ai.proxyHealthFailThreshold}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_proxy_health_url=${ai.proxyHealthUrl}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_dns_continuous_enabled=${ai.dnsContinuousEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_dns_interval=${ai.dnsInterval}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_reload_dedup_enabled=${ai.reloadDedupEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_metrics_enabled=${ai.metricsEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_metrics_retention_hours=${ai.metricsRetentionHours}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_smart_cooldowns_enabled=${ai.smartCooldownsEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_config_validation_enabled=${ai.configValidationEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_graceful_degradation_enabled=${ai.gracefulDegradationEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_persistent_smart_detect=${ai.persistentSmartDetect ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_adaptive_intervals_enabled=${ai.adaptiveIntervalsEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_anomaly_detection_enabled=${ai.anomalyDetectionEnabled ? '1' : '0'}`,
],
[
'set',
`${TACHYON_UCI_PACKAGE}.settings.ai_anomaly_reconnect_threshold=${ai.anomalyReconnectThreshold}`,
],
];
for (const cmd of cmds) {
await TachyonShellMethods.uciRunCommand(cmd);
}
await TachyonShellMethods.uciRunCommand(['commit', TACHYON_UCI_PACKAGE]);
showToast(
_('AI Watchdog settings saved. Restart watchdog to apply.'),
'success',
);
} catch {
showToast(_('Failed to save AI Watchdog settings'), 'error');
}
_state = { ..._state, saving: false };
rerender();
}
function moveSectionUp(idx: number) {
if (idx <= 0) return;
const arr = [..._state.smartDetectSections];
[arr[idx - 1], arr[idx]] = [arr[idx], arr[idx - 1]];
_state = { ..._state, smartDetectSections: arr };
rerender();
}
function moveSectionDown(idx: number) {
const arr = [..._state.smartDetectSections];
if (idx >= arr.length - 1) return;
[arr[idx], arr[idx + 1]] = [arr[idx + 1], arr[idx]];
_state = { ..._state, smartDetectSections: arr };
rerender();
}
function toggleSectionInList(sectionName: string, checked: boolean) {
let arr = [..._state.smartDetectSections];
if (checked && !arr.includes(sectionName)) {
arr.push(sectionName);
} else if (!checked) {
arr = arr.filter((s) => s !== sectionName);
}
_state = { ..._state, smartDetectSections: arr };
rerender();
}
// ─── Render ───────────────────────────────────────────────────────────────────
function renderWatchdogSection(state: AdvancedSettingsState) {
const { watchdogRunning, watchdogLoading } = state;
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🐕'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Watchdog')),
]),
E('div', { class: 'tachyon_adv__row' }, [
E('span', { class: 'tachyon_adv__label' }, _('Status')),
E(
'span',
{
class: watchdogRunning
? 'tachyon_adv__badge tachyon_adv__badge--ok'
: 'tachyon_adv__badge tachyon_adv__badge--err',
},
watchdogRunning ? _('✔ Running') : _('✘ Stopped'),
),
E(
'button',
{
class: `btn cbi-button ${watchdogRunning ? 'cbi-button-negative' : 'cbi-button-action'} tachyon_adv__ctrl-btn`,
type: 'button',
disabled: watchdogLoading,
onclick: () => void toggleWatchdog(),
},
watchdogLoading ? '…' : watchdogRunning ? _('⏹ Stop') : _('▶ Start'),
),
]),
]);
}
function renderDnsTurboCacheSection(state: AdvancedSettingsState) {
const { dnsTurboCache, saving } = state;
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '⚡'),
E('h3', { class: 'tachyon_adv__section-title' }, _('DNS Turbo Cache')),
]),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Keeps FakeIP cache persistent across reboots and pre-resolves popular blocked domains on startup so first-visit latency is 0\u00a0ms.',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
type: 'checkbox',
checked: dnsTurboCache,
onchange: (e: Event) => {
const enabled = (e.target as HTMLInputElement).checked;
void saveDnsTurboCache(enabled);
},
}),
E('span', {}, _('Enable DNS Turbo Cache')),
]),
]),
saving
? E('span', { class: 'tachyon_adv__hint' }, _('Saving…'))
: E('span', {}),
]);
}
function renderAiWatchdogSection(state: AdvancedSettingsState) {
const { aiWatchdog, saving, watchdogRunning } = state;
const toggle = (key: keyof typeof aiWatchdog, label: string) =>
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
type: 'checkbox',
checked: Boolean(aiWatchdog[key]),
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).checked;
_state = {
..._state,
aiWatchdog: { ..._state.aiWatchdog, [key]: val },
};
rerender();
},
}),
E('span', {}, _(label)),
]),
]);
const numInput = (
key: keyof typeof aiWatchdog,
label: string,
hint: string,
min: number,
) =>
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _(label)),
E('input', {
type: 'number',
class: 'cbi-input-text',
value: aiWatchdog[key] as string,
min: String(min),
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = {
..._state,
aiWatchdog: { ..._state.aiWatchdog, [key]: val },
};
},
}),
E('span', { class: 'tachyon_adv__hint' }, _(hint)),
]);
const textInput = (key: keyof typeof aiWatchdog, label: string) =>
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _(label)),
E('input', {
type: 'text',
class: 'cbi-input-text',
value: aiWatchdog[key] as string,
style: 'width: 100%;',
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = {
..._state,
aiWatchdog: { ..._state.aiWatchdog, [key]: val },
};
},
}),
]);
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🤖'),
E('h3', { class: 'tachyon_adv__section-title' }, _('AI Watchdog')),
]),
E(
'p',
{ class: 'tachyon_adv__hint' },
_('Advanced self-healing features. Requires Watchdog to be enabled.'),
),
// Health Checks group
E('div', { class: 'tachyon_adv__group' }, [
E('h4', { class: 'tachyon_adv__group-title' }, _('Health Checks')),
toggle('proxyHealthEnabled', 'Enable Proxy Health Monitor'),
aiWatchdog.proxyHealthEnabled
? E('div', { class: 'tachyon_adv__subrows' }, [
numInput(
'proxyHealthInterval',
'Check interval (s)',
'Fast tier, 15-120s',
15,
),
numInput(
'proxyHealthFailThreshold',
'Fail threshold',
'Consecutive fails before restart',
1,
),
textInput('proxyHealthUrl', 'Health Check URL'),
])
: E('span', {}),
toggle('dnsContinuousEnabled', 'Enable DNS Continuous Check'),
aiWatchdog.dnsContinuousEnabled
? E('div', { class: 'tachyon_adv__subrows' }, [
numInput('dnsInterval', 'DNS Check Interval (s)', '30-300s', 30),
])
: E('span', {}),
]),
// Protection group
E('div', { class: 'tachyon_adv__group' }, [
E('h4', { class: 'tachyon_adv__group-title' }, _('Protection')),
toggle(
'reloadDedupEnabled',
'Firewall Reload Dedup (prevents connection drops)',
),
toggle(
'configValidationEnabled',
'Config Validation (validates before restart)',
),
toggle(
'gracefulDegradationEnabled',
'Graceful Degradation (skip failed checks, continue)',
),
]),
// Monitoring group
E('div', { class: 'tachyon_adv__group' }, [
E(
'h4',
{ class: 'tachyon_adv__group-title' },
_('Monitoring & Analytics'),
),
toggle('metricsEnabled', 'Health Metrics'),
aiWatchdog.metricsEnabled
? E('div', { class: 'tachyon_adv__subrows' }, [
numInput(
'metricsRetentionHours',
'Metrics Retention (hours)',
'1-168h',
1,
),
])
: E('span', {}),
toggle(
'anomalyDetectionEnabled',
'Anomaly Detection (reconnect frequency)',
),
aiWatchdog.anomalyDetectionEnabled
? E('div', { class: 'tachyon_adv__subrows' }, [
numInput(
'anomalyReconnectThreshold',
'Reconnect Threshold',
'Max reconnects/hour',
1,
),
])
: E('span', {}),
]),
// Optimization group
E('div', { class: 'tachyon_adv__group' }, [
E('h4', { class: 'tachyon_adv__group-title' }, _('Optimization')),
toggle(
'smartCooldownsEnabled',
'Smart Cooldowns (15s/120s/300s tier intervals)',
),
toggle(
'adaptiveIntervalsEnabled',
'Adaptive Intervals (longer when healthy)',
),
toggle(
'persistentSmartDetect',
'Persistent Smart Detect (survive reboots)',
),
]),
// AI Agent API group
E('div', { class: 'tachyon_adv__group' }, [
E(
'h4',
{ class: 'tachyon_adv__group-title' },
_('AI Agent REST API (12 Endpoints)'),
),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'HTTP REST API for external AI agents (Claude, GPT, Open-WebUI) served at /cgi-bin/tachyon-agent/',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _('Bearer Auth Token')),
E('div', { style: 'display: flex; gap: 8px; flex: 1;' }, [
E('input', {
type: 'text',
class: 'cbi-input-text',
value: state.agentApiToken,
placeholder: _('Secret token for WRITE requests'),
style: 'flex: 1;',
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = { ..._state, agentApiToken: val.trim() };
},
}),
E(
'button',
{
class: 'btn cbi-button cbi-button-action',
type: 'button',
onclick: () => {
const randomBytes = new Uint8Array(16);
crypto.getRandomValues(randomBytes);
const token = Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
_state = { ..._state, agentApiToken: token };
rerender();
},
},
_('🔑 Generate'),
),
]),
]),
E(
'span',
{ class: 'tachyon_adv__hint' },
_(
'GET endpoints are open on LAN. POST endpoints require Authorization: Bearer <token>.',
),
),
]),
// AI Doctor (ChatGPT / DeepSeek API) group
E('div', { class: 'tachyon_adv__group' }, [
E(
'h4',
{ class: 'tachyon_adv__group-title' },
_('AI Doctor (ChatGPT / DeepSeek API)'),
),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Connect ChatGPT, DeepSeek or local LLM to get automated diagnostic reports and intelligent root-cause analysis.',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
type: 'checkbox',
checked: state.enableAiDoctor,
onchange: (e: Event) => {
const enabled = (e.target as HTMLInputElement).checked;
_state = { ..._state, enableAiDoctor: enabled };
rerender();
},
}),
E('span', {}, _('Enable AI Doctor (LLM Integration)')),
]),
]),
state.enableAiDoctor
? E('div', { class: 'tachyon_adv__subrows' }, [
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _('AI Provider')),
E(
'select',
{
class: 'cbi-input-select',
onchange: (e: Event) => {
const val = (e.target as HTMLSelectElement).value;
// Reset model when provider changes — defaults differ per provider
_state = {
..._state,
aiDoctorProvider: val,
aiDoctorModel: '',
};
rerender();
},
},
[
E(
'option',
{
value: 'openai',
selected: state.aiDoctorProvider === 'openai',
},
'OpenAI (ChatGPT)',
),
E(
'option',
{
value: 'anthropic',
selected: state.aiDoctorProvider === 'anthropic',
},
'Anthropic (Claude API)',
),
E(
'option',
{
value: 'deepseek',
selected: state.aiDoctorProvider === 'deepseek',
},
'DeepSeek API',
),
E(
'option',
{
value: 'openrouter',
selected: state.aiDoctorProvider === 'openrouter',
},
'OpenRouter (openrouter.ai)',
),
E(
'option',
{
value: 'ollama',
selected: state.aiDoctorProvider === 'ollama',
},
_('Ollama (Local PC / Server)'),
),
E(
'option',
{
value: 'lmstudio',
selected: state.aiDoctorProvider === 'lmstudio',
},
_('LM Studio (Local PC / Server)'),
),
E(
'option',
{
value: 'custom',
selected: state.aiDoctorProvider === 'custom',
},
'Custom OpenAI-Compatible API',
),
],
),
]),
// ── Model selector ────────────────────────────────────────────────
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _('AI Model')),
E('input', {
type: 'text',
class: 'cbi-input-text',
value: state.aiDoctorModel,
placeholder:
{
openai: 'gpt-4o-mini',
anthropic: 'claude-3-5-haiku-20241022',
deepseek: 'deepseek-chat',
openrouter: 'openai/gpt-4o-mini',
ollama: 'llama3:latest',
lmstudio: 'local-model',
}[state.aiDoctorProvider] ?? 'gpt-4o-mini',
style: 'width: 100%;',
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = { ..._state, aiDoctorModel: val.trim() };
},
}),
]),
E('div', { class: 'tachyon_adv__row' }, [
E(
'label',
{ class: 'tachyon_adv__label' },
_('AI Doctor Language'),
),
E(
'select',
{
class: 'cbi-input-select',
onchange: (e: Event) => {
const val = (e.target as HTMLSelectElement).value;
_state = { ..._state, aiDoctorLang: val };
rerender();
},
},
[
E(
'option',
{
value: 'ru',
selected: state.aiDoctorLang === 'ru',
},
'Русский (Russian)',
),
E(
'option',
{
value: 'en',
selected: state.aiDoctorLang === 'en',
},
'English',
),
],
),
]),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__label' }, _('AI API Key')),
E('input', {
type: 'password',
class: 'cbi-input-text',
value: state.aiDoctorApiKey,
placeholder: ['ollama', 'lmstudio'].includes(
state.aiDoctorProvider,
)
? _('Optional for local LLM')
: 'sk-...',
style: 'width: 100%;',
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = { ..._state, aiDoctorApiKey: val.trim() };
},
}),
]),
['custom', 'ollama', 'lmstudio'].includes(state.aiDoctorProvider)
? E('div', { class: 'tachyon_adv__row' }, [
E(
'label',
{ class: 'tachyon_adv__label' },
_('Custom API Endpoint URL'),
),
E('input', {
type: 'text',
class: 'cbi-input-text',
value: state.aiDoctorCustomUrl,
placeholder:
{
ollama:
'http://192.168.1.100:11434/v1/chat/completions',
lmstudio:
'http://192.168.1.100:1234/v1/chat/completions',
custom: 'https://openrouter.ai/api/v1/chat/completions',
}[state.aiDoctorProvider] ??
'http://192.168.1.100:11434/v1/chat/completions',
style: 'width: 100%;',
onchange: (e: Event) => {
const val = (e.target as HTMLInputElement).value;
_state = { ..._state, aiDoctorCustomUrl: val.trim() };
},
}),
])
: E('span', {}),
])
: E('span', {}),
]),
E(
'button',
{
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving || !watchdogRunning,
onclick: () => void saveAiWatchdogSettings(),
},
saving
? _('Saving…')
: watchdogRunning
? _('Save AI Watchdog Settings')
: _('Start Watchdog first'),
),
]);
}
function renderSmartDetectSection(state: AdvancedSettingsState) {
const { smartDetectEnabled, smartDetectSections, allSectionNames, saving } =
state;
const unselected = allSectionNames.filter(
(s) => !smartDetectSections.includes(s),
);
const rows: Element[] = [];
smartDetectSections.forEach((secName, idx) => {
rows.push(
E('div', { class: 'tachyon_adv__priority-row' }, [
E('label', { class: 'tachyon_adv__priority-label' }, [
E('input', {
type: 'checkbox',
checked: true,
onchange: (e: Event) =>
toggleSectionInList(
secName,
(e.target as HTMLInputElement).checked,
),
}),
E(
'span',
{ class: 'tachyon_adv__priority-name' },
`${idx + 1}. ${secName}`,
),
]),
E('div', { class: 'tachyon_adv__arrows' }, [
E(
'button',
{
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move up'),
disabled: idx === 0,
onclick: () => moveSectionUp(idx),
},
'△',
),
E(
'button',
{
class: 'btn tachyon_adv__arrow',
type: 'button',
title: _('Move down'),
disabled: idx === smartDetectSections.length - 1,
onclick: () => moveSectionDown(idx),
},
'▽',
),
]),
]),
);
});
unselected.forEach((secName) => {
rows.push(
E(
'div',
{ class: 'tachyon_adv__priority-row tachyon_adv__priority-row--off' },
[
E('label', { class: 'tachyon_adv__priority-label' }, [
E('input', {
type: 'checkbox',
checked: false,
onchange: (e: Event) =>
toggleSectionInList(
secName,
(e.target as HTMLInputElement).checked,
),
}),
E(
'span',
{
class:
'tachyon_adv__priority-name tachyon_adv__priority-name--off',
},
secName,
),
]),
],
),
);
});
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🔍'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Smart Detect')),
]),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Auto-detects blocked domains from logs and adds them to the first section where they work via proxy.',
),
),
E('div', { class: 'tachyon_adv__row' }, [
E('label', { class: 'tachyon_adv__toggle' }, [
E('input', {
type: 'checkbox',
checked: smartDetectEnabled,
onchange: (e: Event) => {
_state = {
..._state,
smartDetectEnabled: (e.target as HTMLInputElement).checked,
};
rerender();
},
}),
E('span', {}, _('Enable Smart Detect')),
]),
]),
smartDetectEnabled && rows.length > 0
? E('div', { class: 'tachyon_adv__priority-list' }, [
E(
'p',
{ class: 'tachyon_adv__sub-hint' },
_('Section test order (checked = active, drag rows with △▽):'),
),
...rows,
])
: E('span', {}),
E(
'button',
{
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => void saveSmartDetect(),
},
saving ? _('Saving…') : _('Save Smart Detect Settings'),
),
]);
}
function renderDeviceRoutingSection(state: AdvancedSettingsState) {
const { allSectionNames, deviceIpsPerSection, saving } = state;
if (allSectionNames.length === 0) {
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🖥'),
E(
'h3',
{ class: 'tachyon_adv__section-title' },
_('Per-Device Routing'),
),
]),
E(
'p',
{ class: 'tachyon_adv__hint' },
_('No active routing sections found.'),
),
]);
}
const editors = allSectionNames.map((secName) => {
const currentIps = (deviceIpsPerSection[secName] || []).join('\n');
const taId = `tachyon-device-ips-${secName}`;
return E('div', { class: 'tachyon_adv__device-block' }, [
E('div', { class: 'tachyon_adv__device-name' }, secName),
E(
'label',
{ class: 'tachyon_adv__device-label' },
_('Device IPs (one per line):'),
),
E(
'textarea',
{
id: taId,
class: 'cbi-input-textarea tachyon_adv__device-ta',
rows: 3,
placeholder: '192.168.1.100\n192.168.1.105',
},
currentIps,
),
E(
'button',
{
class: 'btn cbi-button cbi-button-save tachyon_adv__save-btn',
type: 'button',
disabled: saving,
onclick: () => {
const ta = document.getElementById(
taId,
) as HTMLTextAreaElement | null;
void saveDeviceIps(secName, ta ? ta.value : '');
},
},
saving ? _('Saving…') : _('Save'),
),
]);
});
return E('div', { class: 'tachyon_adv__section' }, [
E('div', { class: 'tachyon_adv__section-header' }, [
E('span', { class: 'tachyon_adv__section-icon' }, '🖥'),
E('h3', { class: 'tachyon_adv__section-title' }, _('Per-Device Routing')),
]),
E(
'p',
{ class: 'tachyon_adv__hint' },
_(
'Devices listed here are always routed through the assigned section, regardless of global rules.',
),
),
...editors,
]);
}
function renderAdvancedSettingsBody(state: AdvancedSettingsState) {
if (!state.loaded) {
return E('div', { class: 'tachyon_adv__loading' }, _('Loading…'));
}
return E('div', { class: 'tachyon_adv__body' }, [
renderWatchdogSection(state),
E('hr', { class: 'tachyon_adv__divider' }),
renderAiWatchdogSection(state),
E('hr', { class: 'tachyon_adv__divider' }),
renderDnsTurboCacheSection(state),
E('hr', { class: 'tachyon_adv__divider' }),
renderSmartDetectSection(state),
E('hr', { class: 'tachyon_adv__divider' }),
renderDeviceRoutingSection(state),
]);
}
export function renderAdvancedSettingsPanel() {
return E('div', { id: 'tachyon-advanced-settings', class: 'tachyon_adv' }, [
E('details', { class: 'tachyon_adv__details' }, [
E(
'summary',
{ class: 'tachyon_adv__summary' },
_('⚙ Advanced Settings'),
),
E(
'div',
{ id: 'tachyon-advanced-settings-inner', class: 'tachyon_adv__inner' },
[renderAdvancedSettingsBody(_state)],
),
]),
]);
}