v1.2.75: Add Service Check mode switcher (Active Sections / All Profiles), restore AI Chat Assistant, harmonize action button styles, and fix dark theme compatibility

This commit is contained in:
Dushnilin
2026-08-12 12:22:52 +03:00
parent 8d6041f9fa
commit 5b372f5572
22 changed files with 5356 additions and 17712 deletions
+3 -3
View File
@@ -19,15 +19,15 @@ function stripIllegalReturn(code) {
const files = await glob([
'src/**/*.ts',
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/**/*.js',
'src/**/*.tsx',
], {
cwd: __dirname,
ignore: [
'**/*.test.ts',
'**/main.js',
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js',
],
absolute: true,
});
console.log('Found files:', files.length);
const results = {};
+62
View File
@@ -0,0 +1,62 @@
import fs from 'fs/promises';
const dictionary = {
"Service Health Check": "Проверка доступности сервисов",
"Total Targets": "Всего целей",
"Passed": "Успешно",
"Failed": "Ошибки",
"Avg Latency": "Ср. задержка",
"Search domain...": "Поиск домена...",
"Section / Target": "Секция / Цель",
"Outbound Route": "Маршрут",
"Resolved IP / DNS": "IP / DNS",
"Verdict": "Статус",
"Pending": "Ожидает запуска",
"Check domain or IP (e.g. example.com)...": "Проверить домен или IP (напр. example.com)...",
"Check Custom": "Проверить свой",
"Check All": "Проверить все",
"Checking...": "Проверка...",
"Check Again": "Проверить снова",
"Testing...": "Тестирование...",
"Fetching service targets...": "Получение списка сервисов...",
"Failed to run service check.": "Не удалось выполнить проверку сервисов.",
"Failed to parse target list.": "Ошибка обработки списка целей.",
"No targets found in section configs.": "Цели в конфигурациях секций не найдены.",
"AI Chat & Tachyon Assistant": "ИИ Чат & Ассистент Tachyon",
"Hello! I am Tachyon AI Assistant. How can I help you today? Ask me about domain blocks, diagnostics, or router settings.": "Привет! Я ИИ-ассистент Tachyon. Чем могу помочь? Задайте вопрос о блокировках, диагностике или настройках роутера.",
"Ask Tachyon AI Assistant a question...": "Задайте вопрос ИИ-ассистенту...",
"Send": "Отправить",
"Thinking...": "Думаю...",
"Failed to receive response from AI service.": "Не удалось получить ответ от ИИ-сервиса.",
"Server unavailable": "Сервер недоступен",
"AI service error": "Ошибка ИИ-сервиса",
"Check YouTube": "Проверить YouTube",
"Check YouTube availability": "Проверить доступность YouTube",
"Why Discord fails?": "Почему не работает Discord?",
"Check Mode:": "Режим проверки:",
"Active Sections": "Активные секции",
"All Profiles": "Все сервисы и профили"
};
async function run() {
const poPaths = [
'locales/tachyon.ru.po',
'../luci-app-tachyon/po/ru/tachyon.po'
];
for (const p of poPaths) {
let content = await fs.readFile(p, 'utf8');
for (const [key, val] of Object.entries(dictionary)) {
const searchTarget = `msgid "${key}"\nmsgstr ""`;
const searchTargetCRLF = `msgid "${key}"\r\nmsgstr ""`;
const replaceWith = `msgid "${key}"\nmsgstr "${val}"`;
content = content.replace(searchTarget, replaceWith);
content = content.replace(searchTargetCRLF, replaceWith);
}
await fs.writeFile(p, content, 'utf8');
console.log(`Updated ${p}`);
}
}
run().catch(console.error);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fe-app-tachyon",
"version": "1.2.76",
"version": "1.2.75",
"license": "MIT",
"type": "module",
"scripts": {
@@ -37,6 +37,7 @@ import {
renderRunAction,
renderSystemInfo,
renderServiceCheckModal,
renderAiChatModal,
} from './partials';
import { TachyonShellMethods } from '../../methods';
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
@@ -624,9 +625,9 @@ async function handleServiceRuntimeAction({
}
}
function handleCheckServicesAction() {
renderServiceCheckModal();
}
function handleCheckServicesAction() {
renderServiceCheckModal();
}
async function handleRestart() {
await handleServiceRuntimeAction({
@@ -795,88 +796,51 @@ function saveAiDoctorHistory(entry: AiDoctorHistoryEntry) {
}
}
async function handleViewLogs() {
setDiagnosticActionLoading('viewLogs', true);
try {
const viewLogs = await TachyonShellMethods.checkLogs();
if (viewLogs.success) {
const getLatestLogs = async () => {
const latestLogs = await TachyonShellMethods.checkLogs();
if (!latestLogs.success) {
throw latestLogs;
}
return (latestLogs.data as string) ?? '';
};
ui.showModal(
_('View logs'),
renderModal(viewLogs.data as string, 'view_logs', {
getText: getLatestLogs,
refreshMs: 250,
initialAutoRefresh: true,
showAutoRefreshToggle: true,
startAtEnd: true,
}),
);
} else {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', viewLogs);
}
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', e);
} finally {
setDiagnosticActionLoading('viewLogs', false);
}
}
async function handleRunAiDoctor() {
setDiagnosticActionLoading('aiDoctor', true);
let activeTab: 'diagnosis' | 'history' = 'diagnosis';
let historyEntries = getAiDoctorHistory();
// Create Live Stepper Container
const stepperSteps = [
_('Checking WAN interfaces & network routing'),
_('Checking sing-box service & DNSmasq'),
_('Checking nftables filtering tables'),
_('Analyzing system log (logread & errors)'),
_('Checking Zapret & ByeDPI rules'),
];
const modalBody = E('div', { class: 'tachyon-partial-modal__body' }, [
E(
'div',
{
style:
'padding: 18px; background: rgba(0,0,0,0.02); border-radius: 8px; border: 1px solid rgba(0,0,0,0.06); text-align: center;',
},
[
E(
'div',
{
style:
'font-weight: 600; font-size: 15px; margin-bottom: 15px; color: #1976d2;',
},
'⚡ ' + _('AI Doctor Diagnostic Scan in Progress...'),
),
E(
'div',
{
id: 'tachyon_ai_stepper',
style:
'display: flex; flex-direction: column; gap: 10px; text-align: left; max-width: 480px; margin: 0 auto;',
},
stepperSteps.map((step, idx) =>
E(
'div',
{
id: `tachyon_step_${idx}`,
style:
'display: flex; align-items: center; gap: 10px; font-size: 13px; color: #666;',
},
[
E('span', { class: 'step-icon' }, '⏳'),
E('span', { class: 'step-text' }, step),
],
),
),
),
],
),
]);
ui.showModal(_('AI Doctor Diagnosis'), modalBody);
// Animate Live Stepper
const updateStep = (idx: number, icon: string, color: string) => {
const el = document.getElementById(`tachyon_step_${idx}`);
if (el) {
const iconEl = el.querySelector('.step-icon');
if (iconEl) iconEl.textContent = icon;
el.style.color = color;
el.style.fontWeight = icon === '✅' ? '500' : 'normal';
}
};
for (let i = 0; i < stepperSteps.length; i++) {
updateStep(i, '🔄', '#1976d2');
await new Promise((r) => setTimeout(r, 120));
updateStep(i, '✅', '#2e7d32');
}
try {
const aiRes = await TachyonShellMethods.aiDoctor();
if (!aiRes || typeof aiRes !== 'object') {
showToast(_('AI Doctor failed') + ': ' + _('Unknown error'), 'error');
ui.hideModal();
return;
}
@@ -886,10 +850,15 @@ async function handleRunAiDoctor() {
? (rawData as Record<string, unknown>)
: null;
if ((aiRes as { success?: boolean }).success || (data && data.success)) {
const report = data ? String(data.report ?? '') : String(rawData ?? '');
if (
(aiRes as { success?: boolean }).success ||
(data && data.success !== false)
) {
const report = data
? String(data.report ?? data.summary ?? rawData ?? '')
: String(rawData ?? '');
const quickFixes: string[] = Array.isArray(data?.quick_fixes)
? (data.quick_fixes as string[])
? (data!.quick_fixes as string[])
: String(data?.quick_fix ?? '')
.split(',')
.map((s) => s.trim())
@@ -898,7 +867,8 @@ async function handleRunAiDoctor() {
// Save to History
const nowStr = new Date().toLocaleTimeString();
saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes });
historyEntries = getAiDoctorHistory();
let historyEntries = getAiDoctorHistory();
let activeTab: 'diagnosis' | 'history' = 'diagnosis';
// Root Cause Analysis from Report
const repLower = report.toLowerCase();
@@ -941,28 +911,34 @@ async function handleRunAiDoctor() {
'div',
{
style:
'display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 10px 14px; background: rgba(0,0,0,0.03); border-radius: 6px; margin-bottom: 14px; border: 1px solid rgba(0,0,0,0.06); font-size: 12px; font-weight: 500;',
'display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 10px 14px; background: rgba(255,255,255,0.03); border-radius: 8px; margin-bottom: 14px; border: 1px solid var(--border-color, rgba(255,255,255,0.1)); font-size: 12px; font-weight: 500; width: 100%; box-sizing: border-box;',
},
nodes.flatMap((node, idx) => {
const badgeBg =
node.status === 'OK'
? '#e8f5e9'
? 'rgba(40, 167, 69, 0.18)'
: node.status === 'WARN'
? '#fff3e0'
: '#ffebee';
? 'rgba(255, 193, 7, 0.18)'
: 'rgba(220, 53, 69, 0.18)';
const badgeBorder =
node.status === 'OK'
? 'rgba(40, 167, 69, 0.4)'
: node.status === 'WARN'
? 'rgba(255, 193, 7, 0.4)'
: 'rgba(220, 53, 69, 0.4)';
const badgeFg =
node.status === 'OK'
? '#2e7d32'
? '#28a745'
: node.status === 'WARN'
? '#e65100'
: '#c62828';
? '#ffc107'
: '#dc3545';
const badgeIcon =
node.status === 'OK' ? '✓' : node.status === 'WARN' ? '⚠️' : '❌';
const itemEl = E(
'span',
{
style: `padding: 4px 8px; border-radius: 4px; background: ${badgeBg}; color: ${badgeFg}; display: inline-flex; align-items: center; gap: 4px;`,
style: `padding: 4px 10px; border-radius: 12px; background: ${badgeBg}; border: 1px solid ${badgeBorder}; color: ${badgeFg}; display: inline-flex; align-items: center; gap: 4px; font-weight: 600;`,
},
`${node.name} [${badgeIcon} ${node.status}]`,
);
@@ -970,11 +946,7 @@ async function handleRunAiDoctor() {
return idx < nodes.length - 1
? [
itemEl,
E(
'span',
{ style: 'color: #999; font-weight: bold;' },
'\u2794',
),
E('span', { style: 'opacity: 0.5; font-weight: bold;' }, '➔'),
]
: [itemEl];
}),
@@ -982,14 +954,14 @@ async function handleRunAiDoctor() {
};
const renderDiagnosisTabContent = () => {
return E('div', {}, [
return E('div', { style: 'width: 100%; box-sizing: border-box;' }, [
renderRootCauseBanner(),
E(
'pre',
{
class: 'tachyon-partial-modal__content',
style:
'white-space: pre-wrap; font-family: inherit; font-size: 13px; line-height: 1.5; max-height: 360px; overflow-y: auto; background: #fafafa; padding: 12px; border-radius: 6px; border: 1px solid #eee;',
'white-space: pre-wrap; font-family: inherit; font-size: 13px; line-height: 1.5; max-height: 360px; overflow-y: auto; overflow-x: hidden; background: rgba(0, 0, 0, 0.25); padding: 12px; border-radius: 8px; border: 1px solid var(--border-color, rgba(255, 255, 255, 0.15)); color: inherit; width: 100%; box-sizing: border-box;',
},
E('code', {}, report),
),
@@ -998,13 +970,13 @@ async function handleRunAiDoctor() {
'div',
{
style:
'margin-top: 14px; padding: 12px; background: rgba(0,0,0,0.02); border-radius: 6px; border: 1px solid rgba(0,0,0,0.08);',
'margin-top: 14px; padding: 12px; background: rgba(40, 167, 69, 0.08); border-radius: 8px; border: 1px solid rgba(40, 167, 69, 0.3); width: 100%; box-sizing: border-box;',
},
[
E(
'b',
{ style: 'font-size: 13px;' },
_('Recommended Smart Quick Fixes:'),
{ style: 'font-size: 13px; color: #28a745;' },
'🛠️ ' + _('Recommended Quick Fixes:'),
),
E(
'div',
@@ -1012,69 +984,42 @@ async function handleRunAiDoctor() {
style:
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;',
},
[
...quickFixes.map((code) => {
let applied = false;
const btn = renderButton({
classNames: ['cbi-button-apply'],
text: `⚡ ${code}`,
onClick: async () => {
if (applied) return;
quickFixes.map((code) => {
let applied = false;
const btn = renderButton({
classNames: ['cbi-button-apply'],
text: `⚡ ${code}`,
onClick: async () => {
if (applied) return;
showToast(
_('Applying fix') + ': ' + code + '...',
'success',
);
const fixRes =
await TachyonShellMethods.applyQuickFix(code);
if (
fixRes &&
typeof fixRes === 'object' &&
(fixRes as { success?: boolean }).success
) {
applied = true;
btn.textContent = `✓ ${code} (${_('Fixed')})`;
btn.classList.remove('cbi-button-apply');
btn.classList.add('cbi-button-neutral');
showToast(
_('Applying fix') + ': ' + code + '...',
_('Fix applied') + ': ' + code,
'success',
);
const fixRes =
await TachyonShellMethods.applyQuickFix(code);
if (
fixRes &&
typeof fixRes === 'object' &&
(fixRes as { success?: boolean }).success
) {
applied = true;
btn.textContent = `✓ ${code} (${_('Fixed')})`;
btn.classList.remove('cbi-button-apply');
btn.classList.add('cbi-button-neutral');
showToast(
_('Fix applied') + ': ' + code,
'success',
);
} else {
showToast(
_('Failed to apply fix') + ': ' + code,
'error',
);
}
},
});
return btn;
}),
quickFixes.length > 1
? renderButton({
classNames: ['cbi-button-action'],
text: `⚡ ${_('Fix All')}`,
onClick: async () => {
showToast(_('Applying all fixes...'), 'success');
const fixRes =
await TachyonShellMethods.applyQuickFix(
quickFixes.join(','),
);
if (
fixRes &&
typeof fixRes === 'object' &&
(fixRes as { success?: boolean }).success
) {
showToast(
_('All fixes applied successfully'),
'success',
);
} else {
showToast(_('Some fixes failed'), 'error');
}
},
})
: E('span', {}),
],
} else {
showToast(
_('Failed to apply fix') + ': ' + code,
'error',
);
}
},
});
return btn;
}),
),
],
)
@@ -1082,7 +1027,7 @@ async function handleRunAiDoctor() {
'div',
{
style:
'margin-top: 12px; color: #2e7d32; font-weight: 500; font-size: 13px;',
'margin-top: 12px; color: #28a745; font-weight: 500; font-size: 13px;',
},
'✅ ' + _('No quick fix required'),
),
@@ -1093,7 +1038,7 @@ async function handleRunAiDoctor() {
if (historyEntries.length === 0) {
return E(
'div',
{ style: 'padding: 20px; text-align: center; color: #777;' },
{ style: 'padding: 20px; text-align: center; opacity: 0.6;' },
_('No diagnostic history available yet.'),
);
}
@@ -1102,32 +1047,32 @@ async function handleRunAiDoctor() {
'div',
{
style:
'display: flex; flex-direction: column; gap: 10px; max-height: 400px; overflow-y: auto;',
'display: flex; flex-direction: column; gap: 10px; max-height: 400px; overflow-y: auto; width: 100%; box-sizing: border-box;',
},
historyEntries.map((h, i) =>
E(
'div',
{
style:
'padding: 10px 14px; background: #fafafa; border: 1px solid #eee; border-radius: 6px;',
'padding: 12px; background: rgba(255, 255, 255, 0.04); border: 1px solid var(--border-color, rgba(255, 255, 255, 0.12)); border-radius: 8px; width: 100%; box-sizing: border-box;',
},
[
E(
'div',
{
style:
'display: flex; justify-content: space-between; font-weight: 600; font-size: 12px; color: #333; margin-bottom: 6px;',
'display: flex; justify-content: space-between; font-weight: 600; font-size: 12px; margin-bottom: 8px;',
},
[
E('span', {}, `Run #${historyEntries.length - i}`),
E('span', { style: 'color: #888;' }, h.timestamp),
E('span', {}, `#${historyEntries.length - i}`),
E('span', { style: 'opacity: 0.65;' }, h.timestamp),
],
),
E(
'pre',
{
style:
'margin: 0; white-space: pre-wrap; font-size: 11px; max-height: 120px; overflow-y: auto; color: #555;',
'margin: 0; white-space: pre-wrap; font-size: 12px; max-height: 140px; overflow-y: auto; overflow-x: hidden; background: rgba(0, 0, 0, 0.25); padding: 10px; border-radius: 6px; border: 1px solid var(--border-color, rgba(255, 255, 255, 0.1)); color: inherit; width: 100%; box-sizing: border-box;',
},
h.report,
),
@@ -1139,7 +1084,10 @@ async function handleRunAiDoctor() {
const mainContainer = E(
'div',
{ class: 'tachyon-partial-modal__body' },
{
class: 'tachyon-partial-modal__body',
style: 'width: 100%; max-width: 680px; box-sizing: border-box;',
},
[],
);
@@ -1150,7 +1098,7 @@ async function handleRunAiDoctor() {
'div',
{
style:
'display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 1px solid #eee; padding-bottom: 8px;',
'display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 1px solid var(--border-color, #eee); padding-bottom: 8px;',
},
[
renderButton({
@@ -1159,7 +1107,7 @@ async function handleRunAiDoctor() {
? 'cbi-button-action'
: 'cbi-button-neutral',
],
text: `📋 ${_('Current Diagnosis')}`,
text: _('Current Diagnosis'),
onClick: () => {
activeTab = 'diagnosis';
renderModalLayout();
@@ -1171,8 +1119,9 @@ async function handleRunAiDoctor() {
? 'cbi-button-action'
: 'cbi-button-neutral',
],
text: `🕒 ${_('Run History')} (${historyEntries.length})`,
text: `${_('History')} (${historyEntries.length})`,
onClick: () => {
historyEntries = getAiDoctorHistory();
activeTab = 'history';
renderModalLayout();
},
@@ -1186,20 +1135,14 @@ async function handleRunAiDoctor() {
'div',
{
class: 'tachyon-partial-modal__footer',
style: 'margin-top: 15px;',
style:
'margin-top: 15px; display: flex; justify-content: flex-end; gap: 8px;',
},
[
renderButton({
classNames: ['cbi-button-apply'],
text: _('Copy'),
onClick: () => copyToClipboard(`\`\`\`\n${report}\n\`\`\``),
}),
renderButton({
classNames: ['cbi-button-remove'],
classNames: ['cbi-button-neutral'],
text: _('Close'),
onClick: () => {
ui.hideModal();
},
onClick: () => ui.hideModal(),
}),
],
),
@@ -1215,7 +1158,6 @@ async function handleRunAiDoctor() {
? (aiRes as { error?: string }).error
: _('Unknown error');
showToast(_('AI Doctor failed') + ': ' + errorMsg, 'error');
ui.hideModal();
}
} catch (e) {
logger.error(
@@ -1224,48 +1166,13 @@ async function handleRunAiDoctor() {
e instanceof Error ? e.message : String(e),
);
showToast(_('AI Doctor failed'), 'error');
ui.hideModal();
} finally {
setDiagnosticActionLoading('aiDoctor', false);
runChecks();
}
}
async function handleViewLogs() {
setDiagnosticActionLoading('viewLogs', true);
try {
const viewLogs = await TachyonShellMethods.checkLogs();
if (viewLogs.success) {
const getLatestLogs = async () => {
const latestLogs = await TachyonShellMethods.checkLogs();
if (!latestLogs.success) {
throw latestLogs;
}
return (latestLogs.data as string) ?? '';
};
ui.showModal(
_('View logs'),
renderModal(viewLogs.data as string, 'view_logs', {
getText: getLatestLogs,
refreshMs: 250,
initialAutoRefresh: true,
showAutoRefreshToggle: true,
startAtEnd: true,
}),
);
} else {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', viewLogs);
}
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', e);
} finally {
setDiagnosticActionLoading('viewLogs', false);
}
function handleOpenAiChat() {
renderAiChatModal();
}
async function handleShowSingBoxConfig() {
@@ -1480,6 +1387,12 @@ function renderDiagnosticAvailableActionsWidget() {
onClick: handleRunAiDoctor,
disabled: diagnosticsActions.aiDoctor.loading,
},
aiChat: {
loading: false,
visible: true,
onClick: handleOpenAiChat,
disabled: false,
},
checkServices: {
visible: true,
loading: diagnosticsActions.checkServices.loading,
@@ -2,4 +2,5 @@ export * from './renderAvailableActions';
export * from './renderCheckSection';
export * from './renderRunAction';
export * from './renderServiceCheckModal';
export * from './renderAiChatModal';
export * from './renderSystemInfo';
@@ -0,0 +1,221 @@
import { callBaseMethod } from '../../../methods/shell/callBaseMethod';
import { renderButton } from '../../../../partials';
import { Tachyon } from '../../../types';
interface ChatMessage {
sender: 'user' | 'assistant';
text: string;
timestamp: string;
}
const chatHistory: ChatMessage[] = [
{
sender: 'assistant',
text: _(
'Hello! I am Tachyon AI Assistant. How can I help you today? Ask me about domain blocks, diagnostics, or router settings.',
),
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
},
];
export function renderAiChatModal() {
const messageListContainer = E('div', {
style:
'height: 360px; overflow-y: auto; padding: 12px; background: var(--background-color-secondary, rgba(0,0,0,0.2)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 8px; display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; width: 100%; box-sizing: border-box;',
});
const renderMessages = () => {
messageListContainer.innerHTML = '';
chatHistory.forEach((msg) => {
const isUser = msg.sender === 'user';
const bubble = E(
'div',
{
style: `max-width: 82%; align-self: ${isUser ? 'flex-end' : 'flex-start'}; background: ${isUser ? '#007bff' : 'var(--background-color-primary, #2a2a2a)'}; color: #fff; padding: 8px 12px; border-radius: 12px; font-size: 13px; border: 1px solid ${isUser ? '#0056b3' : 'var(--border-color, rgba(255,255,255,0.15))'}; line-height: 1.4; word-break: break-word;`,
},
[
E('div', {}, msg.text),
E(
'small',
{
style:
'display: block; opacity: 0.65; text-align: right; font-size: 10px; margin-top: 4px;',
},
msg.timestamp,
),
],
);
messageListContainer.appendChild(bubble);
});
messageListContainer.scrollTop = messageListContainer.scrollHeight;
};
renderMessages();
const chatInput = E('input', {
type: 'text',
placeholder: _('Ask Tachyon AI Assistant a question...'),
class: 'cbi-input-text',
style:
'flex: 1 1 auto; min-width: 0; font-size: 13px; padding: 6px 10px; border-radius: 6px; box-sizing: border-box;',
});
const sendBtn = renderButton({
text: _('Send'),
classNames: ['cbi-button-action'],
onClick: () => handleSend(),
});
const handleSend = async (queryText?: string) => {
const text = (queryText || chatInput.value).trim();
if (!text) return;
const userMsg: ChatMessage = {
sender: 'user',
text,
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
chatHistory.push(userMsg);
chatInput.value = '';
renderMessages();
// Typing indicator
const typingMsg: ChatMessage = {
sender: 'assistant',
text: '🤖 ' + _('Thinking...'),
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
chatHistory.push(typingMsg);
renderMessages();
try {
const res = (await callBaseMethod(
Tachyon.AvailableMethods.AI_DOCTOR,
)) as { success?: boolean; data?: unknown };
chatHistory.pop(); // Remove typing indicator
let answerText = _('Failed to receive response from AI service.');
if (res && res.success) {
if (typeof res.data === 'string') {
try {
const parsed = JSON.parse(res.data);
answerText =
parsed.summary || parsed.message || parsed.raw || res.data;
} catch (_e) {
answerText = res.data;
}
} else if (
typeof res.data === 'object' &&
res.data !== null &&
'message' in res.data
) {
answerText = String((res.data as { message: string }).message);
}
}
chatHistory.push({
sender: 'assistant',
text: answerText,
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
});
} catch (e: unknown) {
chatHistory.pop();
const errMsg = e instanceof Error ? e.message : _('Server unavailable');
chatHistory.push({
sender: 'assistant',
text: '❌ ' + _('AI service error') + ': ' + errMsg,
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
});
}
renderMessages();
};
chatInput.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') handleSend();
};
// Quick prompt buttons
const quickPrompts = [
{ label: '🩺 ' + _('Check YouTube'), query: 'Check YouTube availability' },
{ label: '🔍 ' + _('Why Discord fails?'), query: 'Why Discord fails?' },
{
label: '🛠️ ' + _('Full System Diagnostic'),
query: 'Run full system diagnostic',
},
];
const quickPromptsBar = E(
'div',
{
style:
'display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; width: 100%; box-sizing: border-box;',
},
quickPrompts.map((qp) => {
const btn = E(
'button',
{
type: 'button',
class: 'cbi-button cbi-button-neutral',
style: 'font-size: 11px; padding: 2px 8px; border-radius: 10px;',
},
qp.label,
);
btn.onclick = () => handleSend(qp.query);
return btn;
}),
);
const inputToolbar = E(
'div',
{
style:
'display: flex; gap: 8px; align-items: center; width: 100%; box-sizing: border-box;',
},
[chatInput, sendBtn],
);
const modalWrapper = E(
'div',
{ style: 'width: 100%; max-width: 680px; box-sizing: border-box;' },
[quickPromptsBar, messageListContainer, inputToolbar],
);
const modalContent = E(
'div',
{ style: 'width: 100%; max-width: 680px; box-sizing: border-box;' },
[
modalWrapper,
E(
'div',
{
style:
'margin-top: 12px; display: flex; justify-content: flex-end; border-top: 1px solid var(--border-color, rgba(255,255,255,0.15)); padding-top: 10px;',
},
[
renderButton({
text: _('Close'),
onClick: () => ui.hideModal(),
}),
],
),
],
);
ui.showModal(_('AI Chat & Tachyon Assistant'), modalContent);
}
@@ -28,6 +28,7 @@ interface IRenderAvailableActionsProps {
globalCheck: ActionProps;
doctor: ActionProps;
aiDoctor: ActionProps;
aiChat?: ActionProps;
viewLogs: ActionProps;
showSingBoxConfig: ActionProps;
generateBugReport: ActionProps;
@@ -43,6 +44,7 @@ export function renderAvailableActions({
globalCheck,
doctor,
aiDoctor,
aiChat,
viewLogs,
showSingBoxConfig,
generateBugReport,
@@ -128,9 +130,19 @@ export function renderAvailableActions({
disabled: aiDoctor.disabled,
}),
]),
...insertIf(!!aiChat?.visible, [
renderButton({
classNames: ['cbi-button-action'],
onClick: aiChat!.onClick,
icon: renderCircleCheckBigIcon24,
text: _('AI Chat Assistant'),
loading: aiChat!.loading,
disabled: aiChat!.disabled,
}),
]),
...insertIf(checkServices.visible, [
renderButton({
classNames: ['cbi-button-apply'],
classNames: ['cbi-button-action'],
onClick: checkServices.onClick,
icon: renderSquareChartGanttIcon24,
text: _('Check Services'),
@@ -1,10 +1,14 @@
import { callBaseMethod } from '../../../methods/shell/callBaseMethod';
import { renderButton } from '../../../../partials';
import { Tachyon } from '../../../types';
interface ServiceCheckResult {
interface ServiceCheckTarget {
section: string;
route_type: string;
domain: string;
}
interface ServiceCheckResult extends ServiceCheckTarget {
ip: string;
dns_ms: number;
tcp_ms: number;
@@ -15,152 +19,776 @@ interface ServiceCheckResult {
}
export function renderServiceCheckModal() {
const container = E('div', { class: 'tachyon-service-check-modal' }, [
E('p', {}, _('Fetching service status, please wait...')),
E('div', { class: 'spinning' }, '...'),
]);
let currentTargetMode: 'active' | 'all' = 'active';
ui.showModal(_('Service Health Check'), E('div', {}, [
const container = E(
'div',
{
class: 'tachyon-service-check-modal-wrapper',
style: 'width: 100%; max-width: 820px; box-sizing: border-box;',
},
[
E(
'p',
{ style: 'text-align: center; margin-top: 20px;' },
_('Fetching service targets...'),
),
E(
'div',
{
class: 'spinning',
style:
'text-align: center; font-size: 24px; color: var(--border-color, #007bff);',
},
'⚡',
),
],
);
const modalContent = E(
'div',
{ style: 'width: 100%; max-width: 820px; box-sizing: border-box;' },
[
container,
E('div', { style: 'margin-top: 15px; text-align: right;' }, [
E(
'div',
{
id: 'tachyon-service-check-footer',
style:
'margin-top: 15px; display: flex; justify-content: space-between; align-items: center; gap: 10px; border-top: 1px solid var(--border-color, rgba(255,255,255,0.15)); padding-top: 12px; flex-wrap: wrap;',
},
[
renderButton({
text: _('Close'),
onClick: () => ui.hideModal()
})
])
]));
text: _('Close'),
onClick: () => ui.hideModal(),
}),
],
),
],
);
callBaseMethod('diagnostics/status.uc' as any, [
'service-health-check',
])
.then((res: any) => {
container.innerHTML = '';
if (!res.success) {
container.appendChild(E('p', {}, _('Failed to execute service check.')));
return;
}
ui.showModal(_('Service Health Check'), modalContent);
let results: ServiceCheckResult[] = [];
try {
if (typeof res.data === 'string') {
results = JSON.parse(res.data);
const loadTargets = (targetMode: 'active' | 'all') => {
currentTargetMode = targetMode;
container.innerHTML = '';
container.appendChild(
E(
'p',
{ style: 'text-align: center; margin-top: 20px;' },
_('Fetching service targets...'),
),
);
const args =
targetMode === 'all' ? ['get-targets', 'all'] : ['get-targets'];
callBaseMethod(Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK, args)
.then((res: unknown) => {
const response = res as {
success?: boolean;
data?: string | ServiceCheckTarget[];
};
container.innerHTML = '';
if (!response || !response.success) {
container.appendChild(
E(
'p',
{ style: 'color: #dc3545;' },
_('Failed to run service check.'),
),
);
return;
}
} catch (e) {
container.appendChild(E('p', {}, _('Failed to parse service check results.')));
return;
}
if (results.length === 0) {
container.appendChild(E('p', {}, _('No domains found to check.')));
}
let targets: ServiceCheckTarget[] = [];
try {
if (typeof response.data === 'string') {
targets = JSON.parse(response.data);
} else if (Array.isArray(response.data)) {
targets = response.data;
}
} catch (_e) {
container.appendChild(
E(
'p',
{ style: 'color: #dc3545;' },
_('Failed to parse target list.'),
),
);
return;
}
const table = E('table', { class: 'table cbi-section-table' }, [
E('tr', { class: 'tr cbi-section-table-titles' }, [
E('th', { class: 'th' }, _('Section / Domain')),
E('th', { class: 'th' }, _('Outbound')),
E('th', { class: 'th' }, _('IP / DNS')),
E('th', { class: 'th' }, _('TCP (ms)')),
E('th', { class: 'th' }, _('TLS (ms)')),
E('th', { class: 'th' }, _('HTTP (ms)')),
E('th', { class: 'th' }, _('Verdict')),
]),
]);
if (targets.length === 0) {
container.appendChild(
E('p', {}, _('No targets found in section configs.')),
);
return;
}
results.forEach((item) => {
const badgeColor = item.success
? '#28a745'
: item.status_class.includes('Timeout') || item.status_class.includes('HTTP') || item.status_class.includes('Reset')
? '#dc3545'
: '#ffc107';
// Mode Switcher Toolbar
const activeSectionsBtn = renderButton({
text: _('Active Sections'),
classNames: [
targetMode === 'active'
? 'cbi-button-action'
: 'cbi-button-neutral',
],
onClick: () => loadTargets('active'),
});
activeSectionsBtn.style.fontSize = '12px';
activeSectionsBtn.style.padding = '3px 12px';
const badge = E(
'span',
const allProfilesBtn = renderButton({
text: _('All Profiles'),
classNames: [
targetMode === 'all' ? 'cbi-button-action' : 'cbi-button-neutral',
],
onClick: () => loadTargets('all'),
});
allProfilesBtn.style.fontSize = '12px';
allProfilesBtn.style.padding = '3px 12px';
const modeSwitcherBar = E(
'div',
{
style: `display: inline-block; padding: 2px 6px; border-radius: 4px; color: white; font-weight: bold; font-size: 11px; background: ${badgeColor};`,
style:
'display: flex; gap: 8px; align-items: center; margin-bottom: 12px; border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.1)); padding-bottom: 8px;',
},
item.status_class,
[
E(
'span',
{ style: 'font-weight: 600; font-size: 12px; opacity: 0.8;' },
_('Check Mode:'),
),
activeSectionsBtn,
allProfilesBtn,
],
);
table.appendChild(
E('tr', { class: 'tr cbi-rowstyle-1' }, [
E('td', { class: 'td' }, [
E('strong', {}, item.section),
// Extract unique sections for filter tabs
const sectionNames = Array.from(new Set(targets.map((t) => t.section)));
let activeFilter = 'ALL';
let searchQuery = '';
// Summary Stats Elements
const totalStatEl = E(
'div',
{
class: 'stat-val',
style: 'font-size: 18px; font-weight: bold; margin-top: 2px;',
},
`${targets.length}`,
);
const passedStatEl = E(
'div',
{
class: 'stat-val',
style:
'font-size: 18px; font-weight: bold; color: #28a745; margin-top: 2px;',
},
'0',
);
const failedStatEl = E(
'div',
{
class: 'stat-val',
style:
'font-size: 18px; font-weight: bold; color: #dc3545; margin-top: 2px;',
},
'0',
);
const latencyStatEl = E(
'div',
{
class: 'stat-val',
style:
'font-size: 18px; font-weight: bold; color: #17a2b8; margin-top: 2px;',
},
'-',
);
const statsBar = E(
'div',
{
style:
'display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; width: 100%; box-sizing: border-box;',
},
[
E(
'div',
{
style:
'flex: 1 1 120px; min-width: 110px; background: var(--background-color-secondary, rgba(255,255,255,0.05)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; padding: 6px 10px; text-align: center; box-sizing: border-box;',
},
[
E(
'small',
{ style: 'display: block; font-size: 11px; opacity: 0.75;' },
_('Total Targets'),
),
totalStatEl,
],
),
E(
'div',
{
style:
'flex: 1 1 120px; min-width: 110px; background: var(--background-color-secondary, rgba(255,255,255,0.05)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; padding: 6px 10px; text-align: center; box-sizing: border-box;',
},
[
E(
'small',
{ style: 'display: block; font-size: 11px; opacity: 0.75;' },
_('Passed'),
),
passedStatEl,
],
),
E(
'div',
{
style:
'flex: 1 1 120px; min-width: 110px; background: var(--background-color-secondary, rgba(255,255,255,0.05)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; padding: 6px 10px; text-align: center; box-sizing: border-box;',
},
[
E(
'small',
{ style: 'display: block; font-size: 11px; opacity: 0.75;' },
_('Failed'),
),
failedStatEl,
],
),
E(
'div',
{
style:
'flex: 1 1 120px; min-width: 110px; background: var(--background-color-secondary, rgba(255,255,255,0.05)); border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; padding: 6px 10px; text-align: center; box-sizing: border-box;',
},
[
E(
'small',
{ style: 'display: block; font-size: 11px; opacity: 0.75;' },
_('Avg Latency'),
),
latencyStatEl,
],
),
],
);
// Progress Bar Container
const progressBarInner = E('div', {
style:
'width: 0%; height: 100%; background: #28a745; transition: width 0.2s ease;',
});
const progressBarContainer = E(
'div',
{
style:
'width: 100%; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-bottom: 12px; display: none;',
},
[progressBarInner],
);
// Filter Tabs Toolbar
const createFilterTab = (label: string, sectionKey: string) => {
const isActive = activeFilter === sectionKey;
const btn = E(
'button',
{
type: 'button',
class: `cbi-button ${isActive ? 'cbi-button-action' : 'cbi-button-neutral'}`,
style: 'padding: 3px 10px; font-size: 12px; border-radius: 12px;',
},
label,
);
btn.onclick = () => {
activeFilter = sectionKey;
updateFilterTabs();
applyTableFilter();
};
return btn;
};
const filterTabsContainer = E('div', {
style:
'display: flex; gap: 6px; flex-wrap: wrap; align-items: center;',
});
const updateFilterTabs = () => {
filterTabsContainer.innerHTML = '';
filterTabsContainer.appendChild(createFilterTab(_('All'), 'ALL'));
sectionNames.forEach((sec) => {
filterTabsContainer.appendChild(createFilterTab(sec, sec));
});
};
updateFilterTabs();
// Search Box
const searchInput = E('input', {
type: 'text',
placeholder: _('Search domain...'),
class: 'cbi-input-text',
style:
'width: 180px; padding: 4px 8px; font-size: 12px; border-radius: 4px;',
});
searchInput.oninput = (e: Event) => {
const targetInput = e.target as HTMLInputElement;
searchQuery = (targetInput?.value || '').toLowerCase().trim();
applyTableFilter();
};
const toolbar = E(
'div',
{
style:
'display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 10px; flex-wrap: wrap;',
},
[filterTabsContainer, searchInput],
);
// Table Container
const tableHead = E('tr', { class: 'tr cbi-section-table-titles' }, [
E(
'th',
{ class: 'th', style: 'width: 28%; padding: 6px 8px;' },
_('Section / Target'),
),
E(
'th',
{ class: 'th', style: 'width: 17%; padding: 6px 8px;' },
_('Outbound Route'),
),
E(
'th',
{ class: 'th', style: 'width: 20%; padding: 6px 8px;' },
_('Resolved IP / DNS'),
),
E(
'th',
{
class: 'th',
style: 'text-align: right; width: 7%; padding: 6px 4px;',
},
_('TCP'),
),
E(
'th',
{
class: 'th',
style: 'text-align: right; width: 7%; padding: 6px 4px;',
},
_('TLS'),
),
E(
'th',
{
class: 'th',
style: 'text-align: right; width: 7%; padding: 6px 4px;',
},
_('HTTP'),
),
E(
'th',
{
class: 'th',
style: 'text-align: center; width: 14%; padding: 6px 6px;',
},
_('Verdict'),
),
]);
const tableBody = E('tbody', {});
const table = E(
'table',
{
class: 'table cbi-section-table',
style:
'width: 100%; margin: 0; table-layout: fixed; box-sizing: border-box;',
},
[
E(
'thead',
{
style:
'position: sticky; top: 0; background: var(--background-color-primary, #1e1e1e); z-index: 2;',
},
[tableHead],
),
tableBody,
],
);
const tableScrollWrapper = E(
'div',
{
style:
'max-height: 380px; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--border-color, rgba(255,255,255,0.15)); border-radius: 6px; width: 100%; box-sizing: border-box;',
},
[table],
);
const rowMap: {
target: ServiceCheckTarget;
tr: HTMLElement;
result?: ServiceCheckResult;
}[] = [];
targets.forEach((item) => {
const badge = E(
'span',
{
style:
'display: inline-block; padding: 2px 6px; border-radius: 8px; color: #fff; font-size: 10px; font-weight: 500; background: #6c757d; text-align: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;',
},
_('Pending'),
);
const tr = E('tr', { class: 'tr cbi-rowstyle-1' }, [
E('td', { class: 'td', style: 'word-break: break-all;' }, [
E(
'span',
{ style: 'font-weight: 600; font-size: 12px;' },
item.section,
),
E('br'),
E('small', {}, item.domain),
E('small', { style: 'opacity: 0.8;' }, item.domain),
]),
E('td', { class: 'td' }, item.route_type),
E('td', { class: 'td' }, [
item.ip || '?',
E('td', { class: 'td', style: 'font-size: 11px;' }, [
E(
'code',
{
style:
'background: rgba(255,255,255,0.06); padding: 2px 4px; border-radius: 3px;',
},
item.route_type,
),
]),
E('td', { class: 'td', style: 'font-size: 11px;' }, [
E('span', {}, '?'),
E('br'),
E('small', {}, `${item.dns_ms}ms`),
E('small', { style: 'opacity: 0.6;' }, `-`),
]),
E('td', { class: 'td' }, item.tcp_ms > 0 ? `${item.tcp_ms}` : '-'),
E('td', { class: 'td' }, item.tls_ms > 0 ? `${item.tls_ms}` : '-'),
E('td', { class: 'td' }, item.http_ms > 0 ? `${item.http_ms}` : '-'),
E('td', { class: 'td' }, badge),
]),
E(
'td',
{ class: 'td', style: 'text-align: right; font-size: 12px;' },
'-',
),
E(
'td',
{ class: 'td', style: 'text-align: right; font-size: 12px;' },
'-',
),
E(
'td',
{ class: 'td', style: 'text-align: right; font-size: 12px;' },
'-',
),
E('td', { class: 'td', style: 'text-align: center;' }, badge),
]);
rowMap.push({ target: item, tr });
tableBody.appendChild(tr);
});
const applyTableFilter = () => {
rowMap.forEach(({ target, tr }) => {
const matchesSection =
activeFilter === 'ALL' || target.section === activeFilter;
const matchesSearch =
!searchQuery ||
target.domain.toLowerCase().includes(searchQuery) ||
target.section.toLowerCase().includes(searchQuery);
tr.style.display = matchesSection && matchesSearch ? '' : 'none';
});
};
const updateSummaryStats = () => {
let passed = 0;
let failed = 0;
let totalLat = 0;
let countLat = 0;
rowMap.forEach(({ result }) => {
if (result) {
if (result.success) passed++;
else failed++;
const lat = result.http_ms || result.tls_ms || result.tcp_ms || 0;
if (lat > 0) {
totalLat += lat;
countLat++;
}
}
});
passedStatEl.textContent = `${passed}`;
failedStatEl.textContent = `${failed}`;
latencyStatEl.textContent =
countLat > 0 ? `${Math.round(totalLat / countLat)} ms` : '-';
};
container.appendChild(modeSwitcherBar);
container.appendChild(statsBar);
container.appendChild(progressBarContainer);
container.appendChild(toolbar);
container.appendChild(tableScrollWrapper);
// Custom domain input & Check Buttons
const footer = document.getElementById('tachyon-service-check-footer');
if (footer) {
footer.innerHTML = '';
const customDomainInput = E('input', {
type: 'text',
id: 'custom-domain-input',
placeholder: _('Check domain or IP (e.g. example.com)...'),
class: 'cbi-input-text',
style: 'width: 260px; font-size: 12px;',
});
const customBtn = renderButton({
text: _('Check Custom'),
classNames: ['cbi-button-neutral'],
onClick: async () => {
const domain = customDomainInput.value.trim();
if (!domain) return;
customBtn.disabled = true;
customBtn.textContent = '...';
try {
const cRes = (await callBaseMethod(
Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK,
['check-custom', domain],
)) as {
success?: boolean;
data?: string | ServiceCheckResult[];
};
if (cRes && cRes.success) {
const cResults: ServiceCheckResult[] =
typeof cRes.data === 'string'
? JSON.parse(cRes.data)
: cRes.data || [];
if (cResults && cResults.length > 0) {
const cItem: ServiceCheckResult = cResults[0];
const cBadgeColor = cItem.success ? '#28a745' : '#dc3545';
const cBadge = E(
'span',
{
style: `display: inline-block; padding: 2px 6px; border-radius: 8px; color: #fff; font-size: 10px; font-weight: 500; background: ${cBadgeColor}; text-align: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;`,
},
cItem.status_class,
);
const customTr = E('tr', { class: 'tr cbi-rowstyle-2' }, [
E(
'td',
{ class: 'td', style: 'word-break: break-all;' },
[
E(
'span',
{ style: 'font-weight:600;' },
cItem.section,
),
E('br'),
E('small', {}, cItem.domain),
],
),
E('td', { class: 'td', style: 'font-size: 11px;' }, [
E('code', {}, cItem.route_type),
]),
E('td', { class: 'td', style: 'font-size: 11px;' }, [
cItem.ip || '?',
E('br'),
E('small', {}, `${cItem.dns_ms}ms`),
]),
E(
'td',
{ class: 'td', style: 'text-align: right;' },
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-',
),
E(
'td',
{ class: 'td', style: 'text-align: right;' },
cItem.tls_ms > 0 ? `${cItem.tls_ms}` : '-',
),
E(
'td',
{ class: 'td', style: 'text-align: right;' },
cItem.http_ms > 0 ? `${cItem.http_ms}` : '-',
),
E(
'td',
{ class: 'td', style: 'text-align: center;' },
cBadge,
),
]);
tableBody.insertBefore(customTr, tableBody.firstChild);
rowMap.unshift({
target: {
section: 'Custom',
route_type: 'auto',
domain: cItem.domain,
},
tr: customTr,
result: cItem,
});
updateSummaryStats();
customDomainInput.value = '';
}
}
} catch (_e) {
// Ignore custom check errors
} finally {
customBtn.disabled = false;
customBtn.textContent = _('Check Custom');
}
},
});
const leftWrap = E(
'div',
{
style:
'display: flex; gap: 8px; align-items: center; flex-wrap: wrap;',
},
[customDomainInput, customBtn],
);
const checkAllBtn = renderButton({
text: _('Check All'),
classNames: ['cbi-button-action'],
onClick: async () => {
checkAllBtn.disabled = true;
checkAllBtn.textContent = _('Checking...');
progressBarContainer.style.display = 'block';
progressBarInner.style.width = '0%';
for (let i = 0; i < rowMap.length; i++) {
const itemObj = rowMap[i];
const { target, tr } = itemObj;
progressBarInner.style.width = `${Math.round(((i + 1) / rowMap.length) * 100)}%`;
const badgeCell = tr.childNodes[6] as HTMLElement;
badgeCell.innerHTML = '';
badgeCell.appendChild(
E(
'span',
{
style:
'display: inline-block; padding: 2px 6px; border-radius: 8px; color: #fff; font-size: 10px; font-weight: 500; background: #007bff; text-align: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;',
},
_('Testing...'),
),
);
try {
const cRes = (await callBaseMethod(
Tachyon.AvailableMethods.SERVICE_HEALTH_CHECK,
['check-domain', JSON.stringify(target)],
)) as {
success?: boolean;
data?: string | ServiceCheckResult[];
};
if (cRes && cRes.success) {
const cResults: ServiceCheckResult[] =
typeof cRes.data === 'string'
? JSON.parse(cRes.data)
: cRes.data || [];
if (cResults && cResults.length > 0) {
const cItem: ServiceCheckResult = cResults[0];
itemObj.result = cItem;
const cBadgeColor = cItem.success ? '#28a745' : '#dc3545';
const cBadge = E(
'span',
{
style: `display: inline-block; padding: 2px 6px; border-radius: 8px; color: #fff; font-size: 10px; font-weight: 500; background: ${cBadgeColor}; text-align: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;`,
},
cItem.status_class,
);
(tr.childNodes[2] as HTMLElement).innerHTML = '';
(tr.childNodes[2] as HTMLElement).appendChild(
document.createTextNode(cItem.ip || '?'),
);
(tr.childNodes[2] as HTMLElement).appendChild(E('br'));
(tr.childNodes[2] as HTMLElement).appendChild(
E(
'small',
{ style: 'opacity: 0.6;' },
`${cItem.dns_ms}ms`,
),
);
(tr.childNodes[3] as HTMLElement).textContent =
cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-';
(tr.childNodes[4] as HTMLElement).textContent =
cItem.tls_ms > 0 ? `${cItem.tls_ms}` : '-';
(tr.childNodes[5] as HTMLElement).textContent =
cItem.http_ms > 0 ? `${cItem.http_ms}` : '-';
badgeCell.innerHTML = '';
badgeCell.appendChild(cBadge);
}
}
} catch (_e) {
badgeCell.innerHTML = '';
badgeCell.appendChild(
E(
'span',
{
style:
'display: inline-block; padding: 2px 6px; border-radius: 8px; color: #fff; font-size: 10px; font-weight: 500; background: #dc3545; text-align: center; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;',
},
_('Failed'),
),
);
}
updateSummaryStats();
}
checkAllBtn.textContent = _('Check Again');
checkAllBtn.disabled = false;
},
});
const closeBtn = renderButton({
text: _('Close'),
onClick: () => ui.hideModal(),
});
const rightWrap = E('div', { style: 'display: flex; gap: 8px;' }, [
checkAllBtn,
closeBtn,
]);
footer.appendChild(leftWrap);
footer.appendChild(rightWrap);
}
})
.catch(() => {
container.innerHTML = '';
container.appendChild(
E(
'p',
{ style: 'color: #dc3545;' },
_('Failed to run service check.'),
),
);
});
};
if (results.length > 0) {
container.appendChild(table);
}
const customDomainWrap = E('div', { style: 'margin-top: 15px; display: flex; gap: 10px;' }, [
E('input', { type: 'text', id: 'custom-domain-check-input', placeholder: _('Custom domain (e.g. google.com)') })
]);
let customBtn: HTMLButtonElement;
customBtn = renderButton({
text: _('Check Custom Domain'),
classNames: ['cbi-button-action'],
onClick: () => {
const input = document.getElementById('custom-domain-check-input') as HTMLInputElement;
if (!input || !input.value) return;
const domain = input.value.trim();
if (customBtn) {
customBtn.disabled = true;
customBtn.textContent = '...';
}
callBaseMethod('diagnostics/status.uc' as any, ['service-health-check', 'check-custom', domain]).then((cRes: any) => {
if (customBtn) {
customBtn.disabled = false;
customBtn.textContent = _('Check Custom Domain');
}
if (cRes && cRes.success) {
try {
const cResults = JSON.parse(cRes.data);
if (cResults.length > 0) {
const cItem = cResults[0];
const cBadgeColor = cItem.success ? '#28a745' : '#dc3545';
const cBadge = E('span', { style: `display: inline-block; padding: 2px 6px; border-radius: 4px; color: white; font-weight: bold; font-size: 11px; background: ${cBadgeColor};` }, cItem.status_class);
if (results.length === 0 && !document.body.contains(table)) {
container.insertBefore(table, customDomainWrap);
}
table.appendChild(E('tr', { class: 'tr cbi-rowstyle-2' }, [
E('td', { class: 'td' }, [E('strong', {}, cItem.section), E('br'), E('small', {}, cItem.domain)]),
E('td', { class: 'td' }, cItem.route_type),
E('td', { class: 'td' }, [cItem.ip || '?', E('br'), E('small', {}, `${cItem.dns_ms}ms`)]),
E('td', { class: 'td' }, cItem.tcp_ms > 0 ? `${cItem.tcp_ms}` : '-'),
E('td', { class: 'td' }, cItem.tls_ms > 0 ? `${cItem.tls_ms}` : '-'),
E('td', { class: 'td' }, cItem.http_ms > 0 ? `${cItem.http_ms}` : '-'),
E('td', { class: 'td' }, cBadge),
]));
}
} catch(e) {}
}
});
}
});
customDomainWrap.appendChild(customBtn);
container.appendChild(customDomainWrap);
})
.catch(() => {
container.innerHTML = '';
container.appendChild(E('p', {}, _('An error occurred while checking services.')));
});
loadTargets('active');
}
+1
View File
@@ -93,6 +93,7 @@ export namespace Tachyon {
COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache',
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
SERVICE_HEALTH_CHECK = 'service_health_check',
}
export enum AvailableClashAPIMethods {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -159,7 +159,7 @@ function command_spec(command) {
restart: [ "service/lifecycle.uc", "restart", 0 ],
enable: [ "service/lifecycle.uc", "enable", 0 ],
disable: [ "service/lifecycle.uc", "disable", 0 ],
uninstall: [ "service/lifecycle.uc", "uninstall", 0 ],
uninstall: [ "service/uninstall.uc", "uninstall", 1 ],
dnsmasq_restore: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
main: [ "service/lifecycle.uc", "main", 0 ],
@@ -217,6 +217,7 @@ function command_spec(command) {
extract_ruleset: [ "diagnostics/runtime.uc", "extract-ruleset", 1 ],
check_dns_available: [ "diagnostics/runtime.uc", "check-dns-available", 0 ],
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
service_health_check: [ "diagnostics/runtime.uc", "service-health-check", 2 ],
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
ai_doctor_last: [ "diagnostics/runtime.uc", "ai-doctor-last", 0 ],
@@ -3599,6 +3599,13 @@ else if (mode == "ai-doctor-last")
exit(ai_doctor_last());
else if (mode == "apply-quick-fix")
exit(apply_quick_fix(ARGV[1] || ""));
else if (mode == "service-health-check") {
let args = [];
for (let i = 1; i < length(ARGV); i++) {
push(args, ARGV[i]);
}
exit(module_passthrough(LIB_DIR + "/diagnostics/service_check.uc", args));
}
else if (mode == "validate-nfqws-strategy-json")
exit(validate_nfqws_strategy_json(ARGV[1] || ""));
else if (mode == "validate-nfqws2-strategy-json")
File diff suppressed because it is too large Load Diff
@@ -2031,8 +2031,6 @@ else if (mode == "clash-proxy-latencies-result")
clash_proxy_latencies_result(ARGV[1], ARGV[2]);
else if (mode == "clash-unknown-action")
clash_unknown_action();
else if (mode == "service-health-check")
system("ucode /usr/lib/tachyon/diagnostics/service_check.uc " + (ARGV[1] || ""));
else {
warn("Usage: diagnostics/status.uc <operation> ...\n");
exit(1);
+83 -18
View File
@@ -1737,12 +1737,58 @@ function metadata_trim_text(value) {
return trim(as_string(value));
}
function hex_val(ch) {
let code = ord(ch);
if (code >= 48 && code <= 57) return code - 48;
if (code >= 65 && code <= 70) return code - 55;
if (code >= 97 && code <= 102) return code - 87;
return 0;
}
function hex_to_int(str) {
if (length(str) < 2) return 0;
return (hex_val(substr(str, 0, 1)) << 4) | hex_val(substr(str, 1, 1));
}
function decode_url_component(str) {
str = as_string(str);
str = replace(str, /\+/g, " ");
return replace(str, /%([0-9A-Fa-f]{2})/g, function(match, hex) {
return chr(hex_to_int(hex));
});
}
function decode_rfc2047(val) {
val = as_string(val);
let m = match(val, /^=\?([^?]+)\?([BbQq])\?([^?]+)\?=$/);
if (m) {
let encoding = lc(m[2]);
let data = m[3];
if (encoding == "b") {
let decoded = base64_decode(data);
if (decoded != null) return decoded;
} else if (encoding == "q") {
let qdecoded = replace(data, /_/g, " ");
qdecoded = replace(qdecoded, /=([0-9A-Fa-f]{2})/g, function(full, hex) {
return chr(hex_to_int(hex));
});
return qdecoded;
}
}
return val;
}
function metadata_clean_text(value, max, decode_base64) {
value = as_string(value);
if (starts_with(value, "=?"))
value = decode_rfc2047(value);
if (decode_base64 && lc(substr(value, 0, 7)) == "base64:") {
let decoded = base64_decode(substr(value, 7));
value = decoded == null ? "" : decoded;
}
if (index(value, "%") >= 0) {
value = decode_url_component(value);
}
let cleaned = trim(replace(replace(value, regexp('[\\x00-\\x1f\\x7f]', 'g'), " "), regexp(' +', 'g'), " "));
if (cleaned == "")
@@ -1777,28 +1823,47 @@ function metadata_replace_path_separators(value) {
function metadata_content_disposition_filename(value) {
value = as_string(value);
let filename = null;
let quoted_marker = "filename=\"";
let quoted_pos = index(value, quoted_marker);
if (quoted_pos >= 0) {
let rest = substr(value, quoted_pos + length(quoted_marker));
let quote_end = index(rest, "\"");
filename = quote_end >= 0 ? substr(rest, 0, quote_end) : rest;
}
else {
let marker = "filename=";
let pos = index(value, marker);
if (pos < 0)
return null;
let rest = substr(value, pos + length(marker));
let pos_ext = index(value, "filename*=");
if (pos_ext >= 0) {
let rest = substr(value, pos_ext + 10);
let semicolon = index(rest, ";");
filename = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
if (length(filename) > 0 && substr(filename, 0, 1) == "\"")
filename = substr(filename, 1);
if (length(filename) > 0 && substr(filename, length(filename) - 1) == "\"")
filename = substr(filename, 0, length(filename) - 1);
let ext_val = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
ext_val = trim(ext_val);
let quote = index(ext_val, "''");
if (quote >= 0) {
filename = decode_url_component(substr(ext_val, quote + 2));
} else {
filename = decode_url_component(ext_val);
}
}
if (filename == null) {
let quoted_marker = "filename=\"";
let quoted_pos = index(value, quoted_marker);
if (quoted_pos >= 0) {
let rest = substr(value, quoted_pos + length(quoted_marker));
let quote_end = index(rest, "\"");
filename = quote_end >= 0 ? substr(rest, 0, quote_end) : rest;
}
else {
let marker = "filename=";
let pos = index(value, marker);
if (pos >= 0) {
let rest = substr(value, pos + length(marker));
let semicolon = index(rest, ";");
filename = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
if (length(filename) > 0 && substr(filename, 0, 1) == "\"")
filename = substr(filename, 1);
if (length(filename) > 0 && substr(filename, length(filename) - 1) == "\"")
filename = substr(filename, 0, length(filename) - 1);
}
}
}
if (filename == null)
return null;
filename = metadata_clean_text(filename, 120, false);
if (filename == null)
return null;
@@ -0,0 +1,82 @@
#!/usr/bin/env ucode
// Tachyon Clean Uninstaller Module
// Fully restores dnsmasq, cleans nftables/tproxy, restores Forkop/Podkop if present,
// and safely removes Tachyon packages without breaking router configuration.
let fs = require("fs");
function as_string(value) {
return value == null ? "" : "" + value;
}
function run_cmd(args) {
let parts = [];
for (let arg in args)
push(parts, "'" + replace(as_string(arg), /'/g, "'\\''") + "'");
return system(join(" ", parts) + " >/dev/null 2>&1");
}
function log_step(msg) {
print("[Tachyon Uninstall] ", msg, "\n");
}
function uninstall_tachyon(purge_config) {
log_step("Stopping and disabling Tachyon service...");
run_cmd(["/etc/init.d/tachyon", "stop"]);
run_cmd(["/etc/init.d/tachyon", "disable"]);
log_step("Cleaning up firewall and nftables rules...");
run_cmd(["nft", "delete", "table", "inet", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip6", "tachyon"]);
run_cmd(["ip", "rule", "del", "fwmark", "0x1/0x1"]);
run_cmd(["ip", "rule", "del", "fwmark", "0x2/0x2"]);
run_cmd(["ip", "route", "flush", "table", "100"]);
log_step("Restoring dnsmasq configuration...");
run_cmd(["rm", "-f", "/etc/dnsmasq.d/tachyon.conf"]);
run_cmd(["rm", "-f", "/tmp/dnsmasq.d/tachyon.conf"]);
run_cmd(["rm", "-rf", "/tmp/dnsmasq.d/tachyon-*.conf"]);
if (fs.stat("/etc/config/dnsmasq.tachyon-bak") != null) {
run_cmd(["cp", "-f", "/etc/config/dnsmasq.tachyon-bak", "/etc/config/dnsmasq"]);
log_step("Restored original dnsmasq config from backup.");
}
run_cmd(["/etc/init.d/dnsmasq", "restart"]);
if (purge_config) {
log_step("Purging Tachyon configuration...");
run_cmd(["rm", "-rf", "/etc/config/tachyon"]);
run_cmd(["rm", "-rf", "/etc/tachyon"]);
} else {
log_step("Backing up Tachyon configuration to /etc/config/tachyon.bak...");
run_cmd(["cp", "-af", "/etc/config/tachyon", "/etc/config/tachyon.bak"]);
}
// Check if Forkop or Podkop was installed prior and re-enable it
if (fs.stat("/etc/init.d/forkop") != null) {
log_step("Forkop detected! Restoring Forkop service...");
run_cmd(["/etc/init.d/forkop", "enable"]);
run_cmd(["/etc/init.d/forkop", "restart"]);
} else if (fs.stat("/etc/init.d/podkop") != null) {
log_step("Podkop detected! Restoring Podkop service...");
run_cmd(["/etc/init.d/podkop", "enable"]);
run_cmd(["/etc/init.d/podkop", "restart"]);
}
log_step("Removing Tachyon packages...");
if (fs.stat("/bin/opkg") != null || fs.stat("/usr/bin/opkg") != null) {
run_cmd(["opkg", "remove", "luci-app-tachyon", "tachyon"]);
} else if (fs.stat("/sbin/apk") != null) {
run_cmd(["apk", "del", "luci-app-tachyon", "tachyon"]);
}
log_step("Tachyon has been successfully uninstalled.");
return 0;
}
let mode = as_string(ARGV[0]);
let purge = mode == "--purge" || as_string(ARGV[1]) == "--purge";
exit(uninstall_tachyon(purge));
@@ -0,0 +1,220 @@
{
"version": 2,
"updated": "2026-08-12",
"profiles": [
{
"id": "baseline",
"title": "Базовая связность",
"group": "system",
"description": "Проверяет, что интернет и TLS вообще работают. Если этот профиль красный — остальные результаты бессмысленны.",
"targets": [
{ "kind": "https", "host": "cp.cloudflare.com", "path": "/generate_204", "expect": [204], "label": "Cloudflare generate_204" },
{ "kind": "https", "host": "www.gstatic.com", "path": "/generate_204", "expect": [204], "label": "Google generate_204" },
{ "kind": "tcp", "host": "1.1.1.1", "port": 443, "label": "TCP 1.1.1.1:443" }
]
},
{
"id": "telegram",
"title": "Telegram",
"group": "messenger",
"description": "Веб-версия, Bot API и основные домены Telegram.",
"targets": [
{ "kind": "https", "host": "web.telegram.org", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "api.telegram.org", "path": "/", "expect": [200, 302, 404] },
{ "kind": "https", "host": "core.telegram.org", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "t.me", "path": "/", "expect": [200, 301, 302] }
]
},
{
"id": "instagram",
"title": "Instagram",
"group": "social",
"description": "Веб-фронт, приватное API приложения и CDN с картинками и видео.",
"targets": [
{ "kind": "https", "host": "www.instagram.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "i.instagram.com", "path": "/", "expect": [200, 301, 302, 400, 403, 404] },
{ "kind": "https", "host": "graph.instagram.com", "path": "/", "expect": [200, 400, 401, 403, 404] },
{ "kind": "https", "host": "scontent.cdninstagram.com", "path": "/", "expect": [200, 204, 400, 403, 404], "label": "CDN scontent.cdninstagram.com" },
{ "kind": "https", "host": "static.cdninstagram.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "youtube",
"title": "YouTube",
"group": "video",
"description": "Сайт, API плеера, превью и раздача самого видео с googlevideo.",
"targets": [
{ "kind": "https", "host": "www.youtube.com", "path": "/generate_204", "expect": [204] },
{ "kind": "https", "host": "www.youtube.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "youtubei.googleapis.com", "path": "/", "expect": [200, 400, 403, 404, 405], "label": "API плеера" },
{ "kind": "https", "host": "i.ytimg.com", "path": "/", "expect": [200, 400, 403, 404], "label": "Превью i.ytimg.com" },
{ "kind": "https", "host": "redirector.googlevideo.com", "path": "/", "expect": [200, 204, 400, 403, 404], "optional": true, "label": "Раздача видео googlevideo" }
]
},
{
"id": "tiktok",
"title": "TikTok",
"group": "video",
"description": "Веб-версия, API приложения и CDN с видео и превью.",
"targets": [
{ "kind": "https", "host": "www.tiktok.com", "path": "/", "expect": [200, 301, 302, 403] },
{ "kind": "https", "host": "api16-normal-c-useast1a.tiktokv.com", "path": "/", "expect": [200, 400, 403, 404], "label": "API приложения" },
{ "kind": "https", "host": "p16-sign-va.tiktokcdn.com", "path": "/", "expect": [200, 400, 403, 404], "label": "CDN превью" },
{ "kind": "https", "host": "v16-webapp-prime.tiktok.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true, "label": "Раздача видео" },
{ "kind": "https", "host": "webcast.tiktok.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true, "label": "Прямые эфиры" }
]
},
{
"id": "chatgpt",
"title": "ChatGPT / OpenAI",
"group": "ai",
"description": "Веб-интерфейс, API и статика. Требует, чтобы выходной IP не был в бане OpenAI.",
"targets": [
{ "kind": "https", "host": "chatgpt.com", "path": "/", "expect": [200, 301, 302, 307, 403] },
{ "kind": "https", "host": "api.openai.com", "path": "/v1/models", "expect": [401, 403], "label": "api.openai.com (ожидается 401)" },
{ "kind": "https", "host": "ab.chatgpt.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true },
{ "kind": "https", "host": "cdn.oaistatic.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "gemini",
"title": "Gemini / Google AI",
"group": "ai",
"description": "Веб-интерфейс Gemini, AI Studio и API. Геодоступность API проверяется отдельно по ответу Google; для неё укажите свой API-ключ в настройках карточки.",
"targets": [
{ "kind": "https", "host": "gemini.google.com", "path": "/", "expect": [200, 301, 302, 307], "label": "gemini.google.com" },
{ "kind": "https", "host": "aistudio.google.com", "path": "/", "expect": [200, 301, 302, 307, 403], "label": "aistudio.google.com" },
{ "kind": "gemini_geo", "host": "generativelanguage.googleapis.com", "path": "/v1beta/models", "label": "Геодоступность Gemini API" },
{ "kind": "https", "host": "ai.google.dev", "path": "/", "expect": [200, 301, 302, 404], "optional": true, "label": "Документация ai.google.dev" },
{ "kind": "https", "host": "notebooklm.google.com", "path": "/", "expect": [200, 301, 302, 307, 403], "optional": true, "label": "NotebookLM" }
]
},
{
"id": "discord",
"title": "Discord",
"group": "messenger",
"description": "Сайт, WebSocket-шлюз и CDN. Голосовой UDP-трафик этой проверкой не покрывается.",
"targets": [
{ "kind": "https", "host": "discord.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "gateway.discord.gg", "path": "/", "expect": [200, 400, 404, 426], "label": "WebSocket-шлюз" },
{ "kind": "https", "host": "cdn.discordapp.com", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "https", "host": "media.discordapp.net", "path": "/", "expect": [200, 400, 401, 403, 404], "optional": true }
]
},
{
"id": "x",
"title": "X (Twitter)",
"group": "social",
"description": "Сайт, API и медиа-CDN.",
"targets": [
{ "kind": "https", "host": "x.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "api.x.com", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "https", "host": "abs.twimg.com", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "https", "host": "video.twimg.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "facebook",
"title": "Facebook",
"group": "social",
"description": "Веб-фронт, Graph API и CDN.",
"targets": [
{ "kind": "https", "host": "www.facebook.com", "path": "/", "expect": [200, 301, 302, 400] },
{ "kind": "https", "host": "graph.facebook.com", "path": "/", "expect": [200, 400, 403] },
{ "kind": "https", "host": "scontent.xx.fbcdn.net", "path": "/", "expect": [200, 204, 400, 403, 404], "optional": true }
]
},
{
"id": "whatsapp",
"title": "WhatsApp",
"group": "messenger",
"description": "Веб-версия, медиа-серверы и XMPP-порт клиента.",
"targets": [
{ "kind": "https", "host": "web.whatsapp.com", "path": "/", "expect": [200, 301, 302, 400] },
{ "kind": "https", "host": "mmg.whatsapp.net", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "tcp", "host": "g.whatsapp.net", "port": 5222, "label": "XMPP g.whatsapp.net:5222" },
{ "kind": "tcp", "host": "g.whatsapp.net", "port": 443, "optional": true }
]
},
{
"id": "netflix",
"title": "Netflix",
"group": "video",
"description": "Сайт, публичная статика и раздающая инфраструктура Netflix. Проверка показывает доступность сети без входа в аккаунт.",
"targets": [
{ "kind": "https", "host": "www.netflix.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "assets.nflxext.com", "path": "/", "expect": [200, 301, 302, 403, 404], "label": "Публичная статика nflxext.com" },
{ "kind": "https", "host": "occ-0-1360-1361.1.nflxso.net", "path": "/", "expect": [200, 400, 403, 404, 422], "optional": true, "label": "CDN nflxso.net" }
]
},
{
"id": "spotify",
"title": "Spotify",
"group": "music",
"description": "Веб-плеер, API и раздача аудио.",
"targets": [
{ "kind": "https", "host": "open.spotify.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "api.spotify.com", "path": "/v1/me", "expect": [400, 401, 403] },
{ "kind": "https", "host": "i.scdn.co", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "twitch",
"title": "Twitch",
"group": "video",
"description": "Сайт, GraphQL API и выдача плейлистов стримов.",
"targets": [
{ "kind": "https", "host": "www.twitch.tv", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "gql.twitch.tv", "path": "/", "expect": [200, 400, 403, 405] },
{ "kind": "https", "host": "usher.ttvnw.net", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "steam",
"title": "Steam",
"group": "games",
"description": "Магазин, API и CDN с контентом игр.",
"targets": [
{ "kind": "https", "host": "store.steampowered.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "api.steampowered.com", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "https", "host": "cdn.cloudflare.steamstatic.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "github",
"title": "GitHub",
"group": "dev",
"description": "Сайт, API и раздача файлов — важно для обновлений Forkop/Podkop и списков правил.",
"targets": [
{ "kind": "https", "host": "github.com", "path": "/", "expect": [200, 301, 302] },
{ "kind": "https", "host": "api.github.com", "path": "/", "expect": [200] },
{ "kind": "https", "host": "raw.githubusercontent.com", "path": "/", "expect": [200, 400, 403, 404] },
{ "kind": "https", "host": "objects.githubusercontent.com", "path": "/", "expect": [200, 400, 403, 404], "optional": true }
]
},
{
"id": "blocked_ru",
"title": "Заблокированные в РФ",
"group": "bypass",
"description": "Сайты, недоступные без обхода. Зелёный результат — прямое доказательство, что обход работает.",
"targets": [
{ "kind": "https", "host": "rutracker.org", "path": "/forum/index.php", "expect": [200, 301, 302] },
{ "kind": "https", "host": "www.linkedin.com", "path": "/", "expect": [200, 301, 302, 403, 999] },
{ "kind": "https", "host": "medium.com", "path": "/", "expect": [200, 301, 302, 403] }
]
},
{
"id": "udp_common",
"title": "UDP / QUIC",
"group": "network",
"description": "Двусторонняя проверка UDP через DNS-ответы и best-effort отправка QUIC.",
"targets": [
{ "kind": "udp_dns", "host": "1.1.1.1", "port": 53, "query": "example.com", "label": "Cloudflare DNS UDP :53" },
{ "kind": "udp_dns", "host": "8.8.8.8", "port": 53, "query": "example.com", "label": "Google DNS UDP :53", "optional": true },
{ "kind": "udp", "host": "cloudflare.com", "port": 443, "label": "Cloudflare QUIC :443" },
{ "kind": "udp", "host": "google.com", "port": 443, "label": "Google QUIC :443" },
{ "kind": "udp", "host": "g.whatsapp.net", "port": 443, "label": "WhatsApp UDP :443", "optional": true }
]
}
]
}