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:
@@ -19,15 +19,15 @@ function stripIllegalReturn(code) {
|
|||||||
|
|
||||||
const files = await glob([
|
const files = await glob([
|
||||||
'src/**/*.ts',
|
'src/**/*.ts',
|
||||||
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/**/*.js',
|
'src/**/*.tsx',
|
||||||
], {
|
], {
|
||||||
|
cwd: __dirname,
|
||||||
ignore: [
|
ignore: [
|
||||||
'**/*.test.ts',
|
'**/*.test.ts',
|
||||||
'**/main.js',
|
|
||||||
'../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js',
|
|
||||||
],
|
],
|
||||||
absolute: true,
|
absolute: true,
|
||||||
});
|
});
|
||||||
|
console.log('Found files:', files.length);
|
||||||
|
|
||||||
const results = {};
|
const results = {};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
+207
-5572
File diff suppressed because it is too large
Load Diff
+151
-3305
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fe-app-tachyon",
|
"name": "fe-app-tachyon",
|
||||||
"version": "1.2.76",
|
"version": "1.2.75",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
renderRunAction,
|
renderRunAction,
|
||||||
renderSystemInfo,
|
renderSystemInfo,
|
||||||
renderServiceCheckModal,
|
renderServiceCheckModal,
|
||||||
|
renderAiChatModal,
|
||||||
} from './partials';
|
} from './partials';
|
||||||
import { TachyonShellMethods } from '../../methods';
|
import { TachyonShellMethods } from '../../methods';
|
||||||
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
|
import { fetchServicesInfo } from '../../fetchers/fetchServicesInfo';
|
||||||
@@ -624,9 +625,9 @@ async function handleServiceRuntimeAction({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheckServicesAction() {
|
function handleCheckServicesAction() {
|
||||||
renderServiceCheckModal();
|
renderServiceCheckModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRestart() {
|
async function handleRestart() {
|
||||||
await handleServiceRuntimeAction({
|
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() {
|
async function handleRunAiDoctor() {
|
||||||
setDiagnosticActionLoading('aiDoctor', true);
|
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 {
|
try {
|
||||||
const aiRes = await TachyonShellMethods.aiDoctor();
|
const aiRes = await TachyonShellMethods.aiDoctor();
|
||||||
|
|
||||||
if (!aiRes || typeof aiRes !== 'object') {
|
if (!aiRes || typeof aiRes !== 'object') {
|
||||||
showToast(_('AI Doctor failed') + ': ' + _('Unknown error'), 'error');
|
showToast(_('AI Doctor failed') + ': ' + _('Unknown error'), 'error');
|
||||||
ui.hideModal();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,10 +850,15 @@ async function handleRunAiDoctor() {
|
|||||||
? (rawData as Record<string, unknown>)
|
? (rawData as Record<string, unknown>)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if ((aiRes as { success?: boolean }).success || (data && data.success)) {
|
if (
|
||||||
const report = data ? String(data.report ?? '') : String(rawData ?? '');
|
(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)
|
const quickFixes: string[] = Array.isArray(data?.quick_fixes)
|
||||||
? (data.quick_fixes as string[])
|
? (data!.quick_fixes as string[])
|
||||||
: String(data?.quick_fix ?? '')
|
: String(data?.quick_fix ?? '')
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
@@ -898,7 +867,8 @@ async function handleRunAiDoctor() {
|
|||||||
// Save to History
|
// Save to History
|
||||||
const nowStr = new Date().toLocaleTimeString();
|
const nowStr = new Date().toLocaleTimeString();
|
||||||
saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes });
|
saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes });
|
||||||
historyEntries = getAiDoctorHistory();
|
let historyEntries = getAiDoctorHistory();
|
||||||
|
let activeTab: 'diagnosis' | 'history' = 'diagnosis';
|
||||||
|
|
||||||
// Root Cause Analysis from Report
|
// Root Cause Analysis from Report
|
||||||
const repLower = report.toLowerCase();
|
const repLower = report.toLowerCase();
|
||||||
@@ -941,28 +911,34 @@ async function handleRunAiDoctor() {
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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) => {
|
nodes.flatMap((node, idx) => {
|
||||||
const badgeBg =
|
const badgeBg =
|
||||||
node.status === 'OK'
|
node.status === 'OK'
|
||||||
? '#e8f5e9'
|
? 'rgba(40, 167, 69, 0.18)'
|
||||||
: node.status === 'WARN'
|
: node.status === 'WARN'
|
||||||
? '#fff3e0'
|
? 'rgba(255, 193, 7, 0.18)'
|
||||||
: '#ffebee';
|
: '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 =
|
const badgeFg =
|
||||||
node.status === 'OK'
|
node.status === 'OK'
|
||||||
? '#2e7d32'
|
? '#28a745'
|
||||||
: node.status === 'WARN'
|
: node.status === 'WARN'
|
||||||
? '#e65100'
|
? '#ffc107'
|
||||||
: '#c62828';
|
: '#dc3545';
|
||||||
const badgeIcon =
|
const badgeIcon =
|
||||||
node.status === 'OK' ? '✓' : node.status === 'WARN' ? '⚠️' : '❌';
|
node.status === 'OK' ? '✓' : node.status === 'WARN' ? '⚠️' : '❌';
|
||||||
|
|
||||||
const itemEl = E(
|
const itemEl = E(
|
||||||
'span',
|
'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}]`,
|
`${node.name} [${badgeIcon} ${node.status}]`,
|
||||||
);
|
);
|
||||||
@@ -970,11 +946,7 @@ async function handleRunAiDoctor() {
|
|||||||
return idx < nodes.length - 1
|
return idx < nodes.length - 1
|
||||||
? [
|
? [
|
||||||
itemEl,
|
itemEl,
|
||||||
E(
|
E('span', { style: 'opacity: 0.5; font-weight: bold;' }, '➔'),
|
||||||
'span',
|
|
||||||
{ style: 'color: #999; font-weight: bold;' },
|
|
||||||
'\u2794',
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
: [itemEl];
|
: [itemEl];
|
||||||
}),
|
}),
|
||||||
@@ -982,14 +954,14 @@ async function handleRunAiDoctor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderDiagnosisTabContent = () => {
|
const renderDiagnosisTabContent = () => {
|
||||||
return E('div', {}, [
|
return E('div', { style: 'width: 100%; box-sizing: border-box;' }, [
|
||||||
renderRootCauseBanner(),
|
renderRootCauseBanner(),
|
||||||
E(
|
E(
|
||||||
'pre',
|
'pre',
|
||||||
{
|
{
|
||||||
class: 'tachyon-partial-modal__content',
|
class: 'tachyon-partial-modal__content',
|
||||||
style:
|
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),
|
E('code', {}, report),
|
||||||
),
|
),
|
||||||
@@ -998,13 +970,13 @@ async function handleRunAiDoctor() {
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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(
|
E(
|
||||||
'b',
|
'b',
|
||||||
{ style: 'font-size: 13px;' },
|
{ style: 'font-size: 13px; color: #28a745;' },
|
||||||
_('Recommended Smart Quick Fixes:'),
|
'🛠️ ' + _('Recommended Quick Fixes:'),
|
||||||
),
|
),
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
@@ -1012,69 +984,42 @@ async function handleRunAiDoctor() {
|
|||||||
style:
|
style:
|
||||||
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;',
|
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;',
|
||||||
},
|
},
|
||||||
[
|
quickFixes.map((code) => {
|
||||||
...quickFixes.map((code) => {
|
let applied = false;
|
||||||
let applied = false;
|
const btn = renderButton({
|
||||||
const btn = renderButton({
|
classNames: ['cbi-button-apply'],
|
||||||
classNames: ['cbi-button-apply'],
|
text: `⚡ ${code}`,
|
||||||
text: `⚡ ${code}`,
|
onClick: async () => {
|
||||||
onClick: async () => {
|
if (applied) return;
|
||||||
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(
|
showToast(
|
||||||
_('Applying fix') + ': ' + code + '...',
|
_('Fix applied') + ': ' + code,
|
||||||
'success',
|
'success',
|
||||||
);
|
);
|
||||||
const fixRes =
|
} else {
|
||||||
await TachyonShellMethods.applyQuickFix(code);
|
showToast(
|
||||||
if (
|
_('Failed to apply fix') + ': ' + code,
|
||||||
fixRes &&
|
'error',
|
||||||
typeof fixRes === 'object' &&
|
);
|
||||||
(fixRes as { success?: boolean }).success
|
}
|
||||||
) {
|
},
|
||||||
applied = true;
|
});
|
||||||
btn.textContent = `✓ ${code} (${_('Fixed')})`;
|
return btn;
|
||||||
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', {}),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -1082,7 +1027,7 @@ async function handleRunAiDoctor() {
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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'),
|
'✅ ' + _('No quick fix required'),
|
||||||
),
|
),
|
||||||
@@ -1093,7 +1038,7 @@ async function handleRunAiDoctor() {
|
|||||||
if (historyEntries.length === 0) {
|
if (historyEntries.length === 0) {
|
||||||
return E(
|
return E(
|
||||||
'div',
|
'div',
|
||||||
{ style: 'padding: 20px; text-align: center; color: #777;' },
|
{ style: 'padding: 20px; text-align: center; opacity: 0.6;' },
|
||||||
_('No diagnostic history available yet.'),
|
_('No diagnostic history available yet.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1102,32 +1047,32 @@ async function handleRunAiDoctor() {
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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) =>
|
historyEntries.map((h, i) =>
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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(
|
E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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', {}, `#${historyEntries.length - i}`),
|
||||||
E('span', { style: 'color: #888;' }, h.timestamp),
|
E('span', { style: 'opacity: 0.65;' }, h.timestamp),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
E(
|
E(
|
||||||
'pre',
|
'pre',
|
||||||
{
|
{
|
||||||
style:
|
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,
|
h.report,
|
||||||
),
|
),
|
||||||
@@ -1139,7 +1084,10 @@ async function handleRunAiDoctor() {
|
|||||||
|
|
||||||
const mainContainer = E(
|
const mainContainer = E(
|
||||||
'div',
|
'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',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
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({
|
renderButton({
|
||||||
@@ -1159,7 +1107,7 @@ async function handleRunAiDoctor() {
|
|||||||
? 'cbi-button-action'
|
? 'cbi-button-action'
|
||||||
: 'cbi-button-neutral',
|
: 'cbi-button-neutral',
|
||||||
],
|
],
|
||||||
text: `📋 ${_('Current Diagnosis')}`,
|
text: _('Current Diagnosis'),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
activeTab = 'diagnosis';
|
activeTab = 'diagnosis';
|
||||||
renderModalLayout();
|
renderModalLayout();
|
||||||
@@ -1171,8 +1119,9 @@ async function handleRunAiDoctor() {
|
|||||||
? 'cbi-button-action'
|
? 'cbi-button-action'
|
||||||
: 'cbi-button-neutral',
|
: 'cbi-button-neutral',
|
||||||
],
|
],
|
||||||
text: `🕒 ${_('Run History')} (${historyEntries.length})`,
|
text: `${_('History')} (${historyEntries.length})`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
historyEntries = getAiDoctorHistory();
|
||||||
activeTab = 'history';
|
activeTab = 'history';
|
||||||
renderModalLayout();
|
renderModalLayout();
|
||||||
},
|
},
|
||||||
@@ -1186,20 +1135,14 @@ async function handleRunAiDoctor() {
|
|||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class: 'tachyon-partial-modal__footer',
|
class: 'tachyon-partial-modal__footer',
|
||||||
style: 'margin-top: 15px;',
|
style:
|
||||||
|
'margin-top: 15px; display: flex; justify-content: flex-end; gap: 8px;',
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
renderButton({
|
renderButton({
|
||||||
classNames: ['cbi-button-apply'],
|
classNames: ['cbi-button-neutral'],
|
||||||
text: _('Copy'),
|
|
||||||
onClick: () => copyToClipboard(`\`\`\`\n${report}\n\`\`\``),
|
|
||||||
}),
|
|
||||||
renderButton({
|
|
||||||
classNames: ['cbi-button-remove'],
|
|
||||||
text: _('Close'),
|
text: _('Close'),
|
||||||
onClick: () => {
|
onClick: () => ui.hideModal(),
|
||||||
ui.hideModal();
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1215,7 +1158,6 @@ async function handleRunAiDoctor() {
|
|||||||
? (aiRes as { error?: string }).error
|
? (aiRes as { error?: string }).error
|
||||||
: _('Unknown error');
|
: _('Unknown error');
|
||||||
showToast(_('AI Doctor failed') + ': ' + errorMsg, 'error');
|
showToast(_('AI Doctor failed') + ': ' + errorMsg, 'error');
|
||||||
ui.hideModal();
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -1224,48 +1166,13 @@ async function handleRunAiDoctor() {
|
|||||||
e instanceof Error ? e.message : String(e),
|
e instanceof Error ? e.message : String(e),
|
||||||
);
|
);
|
||||||
showToast(_('AI Doctor failed'), 'error');
|
showToast(_('AI Doctor failed'), 'error');
|
||||||
ui.hideModal();
|
|
||||||
} finally {
|
} finally {
|
||||||
setDiagnosticActionLoading('aiDoctor', false);
|
setDiagnosticActionLoading('aiDoctor', false);
|
||||||
runChecks();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleViewLogs() {
|
function handleOpenAiChat() {
|
||||||
setDiagnosticActionLoading('viewLogs', true);
|
renderAiChatModal();
|
||||||
|
|
||||||
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 handleShowSingBoxConfig() {
|
async function handleShowSingBoxConfig() {
|
||||||
@@ -1480,6 +1387,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
|||||||
onClick: handleRunAiDoctor,
|
onClick: handleRunAiDoctor,
|
||||||
disabled: diagnosticsActions.aiDoctor.loading,
|
disabled: diagnosticsActions.aiDoctor.loading,
|
||||||
},
|
},
|
||||||
|
aiChat: {
|
||||||
|
loading: false,
|
||||||
|
visible: true,
|
||||||
|
onClick: handleOpenAiChat,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
checkServices: {
|
checkServices: {
|
||||||
visible: true,
|
visible: true,
|
||||||
loading: diagnosticsActions.checkServices.loading,
|
loading: diagnosticsActions.checkServices.loading,
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export * from './renderAvailableActions';
|
|||||||
export * from './renderCheckSection';
|
export * from './renderCheckSection';
|
||||||
export * from './renderRunAction';
|
export * from './renderRunAction';
|
||||||
export * from './renderServiceCheckModal';
|
export * from './renderServiceCheckModal';
|
||||||
|
export * from './renderAiChatModal';
|
||||||
export * from './renderSystemInfo';
|
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;
|
globalCheck: ActionProps;
|
||||||
doctor: ActionProps;
|
doctor: ActionProps;
|
||||||
aiDoctor: ActionProps;
|
aiDoctor: ActionProps;
|
||||||
|
aiChat?: ActionProps;
|
||||||
viewLogs: ActionProps;
|
viewLogs: ActionProps;
|
||||||
showSingBoxConfig: ActionProps;
|
showSingBoxConfig: ActionProps;
|
||||||
generateBugReport: ActionProps;
|
generateBugReport: ActionProps;
|
||||||
@@ -43,6 +44,7 @@ export function renderAvailableActions({
|
|||||||
globalCheck,
|
globalCheck,
|
||||||
doctor,
|
doctor,
|
||||||
aiDoctor,
|
aiDoctor,
|
||||||
|
aiChat,
|
||||||
viewLogs,
|
viewLogs,
|
||||||
showSingBoxConfig,
|
showSingBoxConfig,
|
||||||
generateBugReport,
|
generateBugReport,
|
||||||
@@ -128,9 +130,19 @@ export function renderAvailableActions({
|
|||||||
disabled: aiDoctor.disabled,
|
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, [
|
...insertIf(checkServices.visible, [
|
||||||
renderButton({
|
renderButton({
|
||||||
classNames: ['cbi-button-apply'],
|
classNames: ['cbi-button-action'],
|
||||||
onClick: checkServices.onClick,
|
onClick: checkServices.onClick,
|
||||||
icon: renderSquareChartGanttIcon24,
|
icon: renderSquareChartGanttIcon24,
|
||||||
text: _('Check Services'),
|
text: _('Check Services'),
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { callBaseMethod } from '../../../methods/shell/callBaseMethod';
|
import { callBaseMethod } from '../../../methods/shell/callBaseMethod';
|
||||||
import { renderButton } from '../../../../partials';
|
import { renderButton } from '../../../../partials';
|
||||||
|
import { Tachyon } from '../../../types';
|
||||||
|
|
||||||
interface ServiceCheckResult {
|
interface ServiceCheckTarget {
|
||||||
section: string;
|
section: string;
|
||||||
route_type: string;
|
route_type: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceCheckResult extends ServiceCheckTarget {
|
||||||
ip: string;
|
ip: string;
|
||||||
dns_ms: number;
|
dns_ms: number;
|
||||||
tcp_ms: number;
|
tcp_ms: number;
|
||||||
@@ -15,152 +19,776 @@ interface ServiceCheckResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderServiceCheckModal() {
|
export function renderServiceCheckModal() {
|
||||||
const container = E('div', { class: 'tachyon-service-check-modal' }, [
|
let currentTargetMode: 'active' | 'all' = 'active';
|
||||||
E('p', {}, _('Fetching service status, please wait...')),
|
|
||||||
E('div', { class: 'spinning' }, '...'),
|
|
||||||
]);
|
|
||||||
|
|
||||||
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,
|
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({
|
renderButton({
|
||||||
text: _('Close'),
|
text: _('Close'),
|
||||||
onClick: () => ui.hideModal()
|
onClick: () => ui.hideModal(),
|
||||||
})
|
}),
|
||||||
])
|
],
|
||||||
]));
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
callBaseMethod('diagnostics/status.uc' as any, [
|
ui.showModal(_('Service Health Check'), modalContent);
|
||||||
'service-health-check',
|
|
||||||
])
|
|
||||||
.then((res: any) => {
|
|
||||||
container.innerHTML = '';
|
|
||||||
if (!res.success) {
|
|
||||||
container.appendChild(E('p', {}, _('Failed to execute service check.')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results: ServiceCheckResult[] = [];
|
const loadTargets = (targetMode: 'active' | 'all') => {
|
||||||
try {
|
currentTargetMode = targetMode;
|
||||||
if (typeof res.data === 'string') {
|
container.innerHTML = '';
|
||||||
results = JSON.parse(res.data);
|
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) {
|
let targets: ServiceCheckTarget[] = [];
|
||||||
container.appendChild(E('p', {}, _('No domains found to check.')));
|
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' }, [
|
if (targets.length === 0) {
|
||||||
E('tr', { class: 'tr cbi-section-table-titles' }, [
|
container.appendChild(
|
||||||
E('th', { class: 'th' }, _('Section / Domain')),
|
E('p', {}, _('No targets found in section configs.')),
|
||||||
E('th', { class: 'th' }, _('Outbound')),
|
);
|
||||||
E('th', { class: 'th' }, _('IP / DNS')),
|
return;
|
||||||
E('th', { class: 'th' }, _('TCP (ms)')),
|
}
|
||||||
E('th', { class: 'th' }, _('TLS (ms)')),
|
|
||||||
E('th', { class: 'th' }, _('HTTP (ms)')),
|
|
||||||
E('th', { class: 'th' }, _('Verdict')),
|
|
||||||
]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
results.forEach((item) => {
|
// Mode Switcher Toolbar
|
||||||
const badgeColor = item.success
|
const activeSectionsBtn = renderButton({
|
||||||
? '#28a745'
|
text: _('Active Sections'),
|
||||||
: item.status_class.includes('Timeout') || item.status_class.includes('HTTP') || item.status_class.includes('Reset')
|
classNames: [
|
||||||
? '#dc3545'
|
targetMode === 'active'
|
||||||
: '#ffc107';
|
? 'cbi-button-action'
|
||||||
|
: 'cbi-button-neutral',
|
||||||
|
],
|
||||||
|
onClick: () => loadTargets('active'),
|
||||||
|
});
|
||||||
|
activeSectionsBtn.style.fontSize = '12px';
|
||||||
|
activeSectionsBtn.style.padding = '3px 12px';
|
||||||
|
|
||||||
const badge = E(
|
const allProfilesBtn = renderButton({
|
||||||
'span',
|
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(
|
// Extract unique sections for filter tabs
|
||||||
E('tr', { class: 'tr cbi-rowstyle-1' }, [
|
const sectionNames = Array.from(new Set(targets.map((t) => t.section)));
|
||||||
E('td', { class: 'td' }, [
|
let activeFilter = 'ALL';
|
||||||
E('strong', {}, item.section),
|
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('br'),
|
||||||
E('small', {}, item.domain),
|
E('small', { style: 'opacity: 0.8;' }, item.domain),
|
||||||
]),
|
]),
|
||||||
E('td', { class: 'td' }, item.route_type),
|
E('td', { class: 'td', style: 'font-size: 11px;' }, [
|
||||||
E('td', { class: 'td' }, [
|
E(
|
||||||
item.ip || '?',
|
'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('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(
|
||||||
E('td', { class: 'td' }, item.tls_ms > 0 ? `${item.tls_ms}` : '-'),
|
'td',
|
||||||
E('td', { class: 'td' }, item.http_ms > 0 ? `${item.http_ms}` : '-'),
|
{ class: 'td', style: 'text-align: right; font-size: 12px;' },
|
||||||
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: 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) {
|
loadTargets('active');
|
||||||
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.')));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export namespace Tachyon {
|
|||||||
COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache',
|
COMPONENT_UPDATE_CHECK_CACHE = 'component_update_check_cache',
|
||||||
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
|
SUBSCRIPTION_UPDATE_ASYNC = 'subscription_update_async',
|
||||||
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
|
SUBSCRIPTION_UPDATE_STATUS = 'subscription_update_status',
|
||||||
|
SERVICE_HEALTH_CHECK = 'service_health_check',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AvailableClashAPIMethods {
|
export enum AvailableClashAPIMethods {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+80
-2291
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -159,7 +159,7 @@ function command_spec(command) {
|
|||||||
restart: [ "service/lifecycle.uc", "restart", 0 ],
|
restart: [ "service/lifecycle.uc", "restart", 0 ],
|
||||||
enable: [ "service/lifecycle.uc", "enable", 0 ],
|
enable: [ "service/lifecycle.uc", "enable", 0 ],
|
||||||
disable: [ "service/lifecycle.uc", "disable", 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 ],
|
dnsmasq_restore: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
|
||||||
restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
|
restore_dnsmasq: [ "service/lifecycle.uc", "dnsmasq-restore", 0 ],
|
||||||
main: [ "service/lifecycle.uc", "main", 0 ],
|
main: [ "service/lifecycle.uc", "main", 0 ],
|
||||||
@@ -217,6 +217,7 @@ function command_spec(command) {
|
|||||||
extract_ruleset: [ "diagnostics/runtime.uc", "extract-ruleset", 1 ],
|
extract_ruleset: [ "diagnostics/runtime.uc", "extract-ruleset", 1 ],
|
||||||
check_dns_available: [ "diagnostics/runtime.uc", "check-dns-available", 0 ],
|
check_dns_available: [ "diagnostics/runtime.uc", "check-dns-available", 0 ],
|
||||||
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
||||||
|
service_health_check: [ "diagnostics/runtime.uc", "service-health-check", 2 ],
|
||||||
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
||||||
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
||||||
ai_doctor_last: [ "diagnostics/runtime.uc", "ai-doctor-last", 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());
|
exit(ai_doctor_last());
|
||||||
else if (mode == "apply-quick-fix")
|
else if (mode == "apply-quick-fix")
|
||||||
exit(apply_quick_fix(ARGV[1] || ""));
|
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")
|
else if (mode == "validate-nfqws-strategy-json")
|
||||||
exit(validate_nfqws_strategy_json(ARGV[1] || ""));
|
exit(validate_nfqws_strategy_json(ARGV[1] || ""));
|
||||||
else if (mode == "validate-nfqws2-strategy-json")
|
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]);
|
clash_proxy_latencies_result(ARGV[1], ARGV[2]);
|
||||||
else if (mode == "clash-unknown-action")
|
else if (mode == "clash-unknown-action")
|
||||||
clash_unknown_action();
|
clash_unknown_action();
|
||||||
else if (mode == "service-health-check")
|
|
||||||
system("ucode /usr/lib/tachyon/diagnostics/service_check.uc " + (ARGV[1] || ""));
|
|
||||||
else {
|
else {
|
||||||
warn("Usage: diagnostics/status.uc <operation> ...\n");
|
warn("Usage: diagnostics/status.uc <operation> ...\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
@@ -1737,12 +1737,58 @@ function metadata_trim_text(value) {
|
|||||||
return trim(as_string(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) {
|
function metadata_clean_text(value, max, decode_base64) {
|
||||||
value = as_string(value);
|
value = as_string(value);
|
||||||
|
if (starts_with(value, "=?"))
|
||||||
|
value = decode_rfc2047(value);
|
||||||
if (decode_base64 && lc(substr(value, 0, 7)) == "base64:") {
|
if (decode_base64 && lc(substr(value, 0, 7)) == "base64:") {
|
||||||
let decoded = base64_decode(substr(value, 7));
|
let decoded = base64_decode(substr(value, 7));
|
||||||
value = decoded == null ? "" : decoded;
|
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'), " "));
|
let cleaned = trim(replace(replace(value, regexp('[\\x00-\\x1f\\x7f]', 'g'), " "), regexp(' +', 'g'), " "));
|
||||||
if (cleaned == "")
|
if (cleaned == "")
|
||||||
@@ -1777,28 +1823,47 @@ function metadata_replace_path_separators(value) {
|
|||||||
function metadata_content_disposition_filename(value) {
|
function metadata_content_disposition_filename(value) {
|
||||||
value = as_string(value);
|
value = as_string(value);
|
||||||
let filename = null;
|
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, ";");
|
let semicolon = index(rest, ";");
|
||||||
filename = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
|
let ext_val = semicolon >= 0 ? substr(rest, 0, semicolon) : rest;
|
||||||
if (length(filename) > 0 && substr(filename, 0, 1) == "\"")
|
ext_val = trim(ext_val);
|
||||||
filename = substr(filename, 1);
|
let quote = index(ext_val, "''");
|
||||||
if (length(filename) > 0 && substr(filename, length(filename) - 1) == "\"")
|
if (quote >= 0) {
|
||||||
filename = substr(filename, 0, length(filename) - 1);
|
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);
|
filename = metadata_clean_text(filename, 120, false);
|
||||||
if (filename == null)
|
if (filename == null)
|
||||||
return 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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user