feat(ai-doctor): super-upgrade with Live Stepper, Root Cause Graph, Smart Fix Badges, Deep Log Inspection & Local LLM Presets
This commit is contained in:
+1
-1
@@ -8,8 +8,8 @@ CLAUDE.md
|
||||
opencode.json
|
||||
|
||||
sandbox/
|
||||
.wsl-build/
|
||||
dist/
|
||||
.wsl-build/
|
||||
build-out/
|
||||
scratch/
|
||||
tmp_*/
|
||||
|
||||
@@ -804,6 +804,22 @@ function renderAiWatchdogSection(state: AdvancedSettingsState) {
|
||||
},
|
||||
'DeepSeek API',
|
||||
),
|
||||
E(
|
||||
'option',
|
||||
{
|
||||
value: 'ollama',
|
||||
selected: state.aiDoctorProvider === 'ollama',
|
||||
},
|
||||
'Ollama (Локально на ПК / Сервере)',
|
||||
),
|
||||
E(
|
||||
'option',
|
||||
{
|
||||
value: 'lmstudio',
|
||||
selected: state.aiDoctorProvider === 'lmstudio',
|
||||
},
|
||||
'LM Studio (Локально на ПК / Сервере)',
|
||||
),
|
||||
E(
|
||||
'option',
|
||||
{
|
||||
|
||||
@@ -765,14 +765,113 @@ async function handleRunDoctor() {
|
||||
}
|
||||
}
|
||||
|
||||
interface AiDoctorHistoryEntry {
|
||||
timestamp: string;
|
||||
report: string;
|
||||
quickFixes: string[];
|
||||
}
|
||||
|
||||
function getAiDoctorHistory(): AiDoctorHistoryEntry[] {
|
||||
try {
|
||||
const raw = localStorage.getItem('tachyon_ai_doctor_history');
|
||||
return raw ? (JSON.parse(raw) as AiDoctorHistoryEntry[]) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveAiDoctorHistory(entry: AiDoctorHistoryEntry) {
|
||||
try {
|
||||
const current = getAiDoctorHistory();
|
||||
const updated = [entry, ...current].slice(0, 5);
|
||||
localStorage.setItem('tachyon_ai_doctor_history', JSON.stringify(updated));
|
||||
} catch (e) {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunAiDoctor() {
|
||||
setDiagnosticActionLoading('aiDoctor', true);
|
||||
|
||||
let activeTab: 'diagnosis' | 'history' = 'diagnosis';
|
||||
let historyEntries = getAiDoctorHistory();
|
||||
|
||||
// Create Live Stepper Container
|
||||
const stepperSteps = [
|
||||
_('Checking WAN interfaces & network routing'),
|
||||
_('Checking sing-box service & DNSmasq'),
|
||||
_('Checking nftables filtering tables'),
|
||||
_('Analyzing system log (logread & errors)'),
|
||||
_('Checking Zapret & ByeDPI rules'),
|
||||
];
|
||||
|
||||
const modalBody = E('div', { class: 'tachyon-partial-modal__body' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'padding: 18px; background: rgba(0,0,0,0.02); border-radius: 8px; border: 1px solid rgba(0,0,0,0.06); text-align: center;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'font-weight: 600; font-size: 15px; margin-bottom: 15px; color: #1976d2;',
|
||||
},
|
||||
'⚡ ' + _('AI Doctor Diagnostic Scan in Progress...'),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: 'tachyon_ai_stepper',
|
||||
style:
|
||||
'display: flex; flex-direction: column; gap: 10px; text-align: left; max-width: 480px; margin: 0 auto;',
|
||||
},
|
||||
stepperSteps.map((step, idx) =>
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
id: `tachyon_step_${idx}`,
|
||||
style:
|
||||
'display: flex; align-items: center; gap: 10px; font-size: 13px; color: #666;',
|
||||
},
|
||||
[
|
||||
E('span', { class: 'step-icon' }, '⏳'),
|
||||
E('span', { class: 'step-text' }, step),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]);
|
||||
|
||||
ui.showModal(_('AI Doctor Diagnosis'), modalBody);
|
||||
|
||||
// Animate Live Stepper
|
||||
const updateStep = (idx: number, icon: string, color: string) => {
|
||||
const el = document.getElementById(`tachyon_step_${idx}`);
|
||||
if (el) {
|
||||
const iconEl = el.querySelector('.step-icon');
|
||||
if (iconEl) iconEl.textContent = icon;
|
||||
el.style.color = color;
|
||||
el.style.fontWeight = icon === '✅' ? '500' : 'normal';
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < stepperSteps.length; i++) {
|
||||
updateStep(i, '🔄', '#1976d2');
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
updateStep(i, '✅', '#2e7d32');
|
||||
}
|
||||
|
||||
try {
|
||||
const aiRes = await TachyonShellMethods.aiDoctor();
|
||||
|
||||
if (!aiRes || typeof aiRes !== 'object') {
|
||||
showToast(_('AI Doctor failed') + ': ' + _('Unknown error'), 'error');
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -791,16 +890,101 @@ async function handleRunAiDoctor() {
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const title = _('AI Doctor Diagnosis');
|
||||
// Save to History
|
||||
const nowStr = new Date().toLocaleTimeString();
|
||||
saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes });
|
||||
historyEntries = getAiDoctorHistory();
|
||||
|
||||
const modalContent = E('div', { class: 'tachyon-partial-modal__body' }, [
|
||||
E('div', {}, [
|
||||
// Root Cause Analysis from Report
|
||||
const repLower = report.toLowerCase();
|
||||
const nodes = [
|
||||
{
|
||||
name: 'WAN',
|
||||
status: repLower.includes('wan interface down') ? 'FAIL' : 'OK',
|
||||
},
|
||||
{
|
||||
name: 'DNS',
|
||||
status:
|
||||
repLower.includes('dnsmasq') || repLower.includes('dns')
|
||||
? repLower.includes('dns failed') ||
|
||||
repLower.includes('dnsmasq failed')
|
||||
? 'FAIL'
|
||||
: 'OK'
|
||||
: 'OK',
|
||||
},
|
||||
{
|
||||
name: 'sing-box',
|
||||
status:
|
||||
repLower.includes('sing-box') &&
|
||||
(repLower.includes('stopped') || repLower.includes('error'))
|
||||
? 'FAIL'
|
||||
: 'OK',
|
||||
},
|
||||
{
|
||||
name: 'nftables',
|
||||
status:
|
||||
repLower.includes('nftables') || repLower.includes('rules')
|
||||
? repLower.includes('damaged') || repLower.includes('corrupted')
|
||||
? 'WARN'
|
||||
: 'OK'
|
||||
: 'OK',
|
||||
},
|
||||
];
|
||||
|
||||
const renderRootCauseBanner = () => {
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 10px 14px; background: rgba(0,0,0,0.03); border-radius: 6px; margin-bottom: 14px; border: 1px solid rgba(0,0,0,0.06); font-size: 12px; font-weight: 500;',
|
||||
},
|
||||
nodes.flatMap((node, idx) => {
|
||||
const badgeBg =
|
||||
node.status === 'OK'
|
||||
? '#e8f5e9'
|
||||
: node.status === 'WARN'
|
||||
? '#fff3e0'
|
||||
: '#ffebee';
|
||||
const badgeFg =
|
||||
node.status === 'OK'
|
||||
? '#2e7d32'
|
||||
: node.status === 'WARN'
|
||||
? '#e65100'
|
||||
: '#c62828';
|
||||
const badgeIcon =
|
||||
node.status === 'OK' ? '✓' : node.status === 'WARN' ? '⚠️' : '❌';
|
||||
|
||||
const itemEl = E(
|
||||
'span',
|
||||
{
|
||||
style: `padding: 4px 8px; border-radius: 4px; background: ${badgeBg}; color: ${badgeFg}; display: inline-flex; align-items: center; gap: 4px;`,
|
||||
},
|
||||
`${node.name} [${badgeIcon} ${node.status}]`,
|
||||
);
|
||||
|
||||
return idx < nodes.length - 1
|
||||
? [
|
||||
itemEl,
|
||||
E(
|
||||
'span',
|
||||
{ style: 'color: #999; font-weight: bold;' },
|
||||
'\u2794',
|
||||
),
|
||||
]
|
||||
: [itemEl];
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const renderDiagnosisTabContent = () => {
|
||||
return E('div', {}, [
|
||||
renderRootCauseBanner(),
|
||||
E(
|
||||
'pre',
|
||||
{
|
||||
class: 'tachyon-partial-modal__content',
|
||||
style:
|
||||
'white-space: pre-wrap; font-family: inherit; font-size: 13px; line-height: 1.5; max-height: 400px; overflow-y: auto;',
|
||||
'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;',
|
||||
},
|
||||
E('code', {}, report),
|
||||
),
|
||||
@@ -809,22 +993,28 @@ async function handleRunAiDoctor() {
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'margin-top: 15px; padding: 12px; background: rgba(0,0,0,0.03); border-radius: 6px; border: 1px solid rgba(0,0,0,0.08);',
|
||||
'margin-top: 14px; padding: 12px; background: rgba(0,0,0,0.02); border-radius: 6px; border: 1px solid rgba(0,0,0,0.08);',
|
||||
},
|
||||
[
|
||||
E('b', {}, _('Recommended Quick Fixes:')),
|
||||
E(
|
||||
'b',
|
||||
{ style: 'font-size: 13px;' },
|
||||
_('Recommended Smart Quick Fixes:'),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;',
|
||||
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;',
|
||||
},
|
||||
[
|
||||
...quickFixes.map((code) =>
|
||||
renderButton({
|
||||
...quickFixes.map((code) => {
|
||||
let applied = false;
|
||||
const btn = renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
text: _('Fix') + ': ' + code,
|
||||
text: `⚡ ${code}`,
|
||||
onClick: async () => {
|
||||
if (applied) return;
|
||||
showToast(
|
||||
_('Applying fix') + ': ' + code + '...',
|
||||
'success',
|
||||
@@ -836,6 +1026,10 @@ async function handleRunAiDoctor() {
|
||||
typeof fixRes === 'object' &&
|
||||
(fixRes as { success?: boolean }).success
|
||||
) {
|
||||
applied = true;
|
||||
btn.textContent = `✓ ${code} (${_('Fixed')})`;
|
||||
btn.classList.remove('cbi-button-apply');
|
||||
btn.classList.add('cbi-button-neutral');
|
||||
showToast(
|
||||
_('Fix applied') + ': ' + code,
|
||||
'success',
|
||||
@@ -847,12 +1041,13 @@ async function handleRunAiDoctor() {
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
return btn;
|
||||
}),
|
||||
quickFixes.length > 1
|
||||
? renderButton({
|
||||
classNames: ['cbi-button-save'],
|
||||
text: _('Fix All'),
|
||||
classNames: ['cbi-button-action'],
|
||||
text: `⚡ ${_('Fix All')}`,
|
||||
onClick: async () => {
|
||||
showToast(_('Applying all fixes...'), 'success');
|
||||
const fixRes =
|
||||
@@ -881,41 +1076,141 @@ async function handleRunAiDoctor() {
|
||||
: E(
|
||||
'div',
|
||||
{
|
||||
style: 'margin-top: 10px; color: #2e7d32; font-weight: 500;',
|
||||
style:
|
||||
'margin-top: 12px; color: #2e7d32; font-weight: 500; font-size: 13px;',
|
||||
},
|
||||
'✅ ' + _('No quick fix required'),
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: 'tachyon-partial-modal__footer',
|
||||
style: 'margin-top: 15px;',
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
text: _('Copy'),
|
||||
onClick: () => copyToClipboard(`\`\`\`\n${report}\n\`\`\``),
|
||||
}),
|
||||
renderButton({
|
||||
classNames: ['cbi-button-remove'],
|
||||
text: _('Close'),
|
||||
onClick: () => {
|
||||
ui.hideModal();
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
]),
|
||||
]);
|
||||
]);
|
||||
};
|
||||
|
||||
ui.showModal(title, modalContent);
|
||||
const renderHistoryTabContent = () => {
|
||||
if (historyEntries.length === 0) {
|
||||
return E(
|
||||
'div',
|
||||
{ style: 'padding: 20px; text-align: center; color: #777;' },
|
||||
_('No diagnostic history available yet.'),
|
||||
);
|
||||
}
|
||||
|
||||
return E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; flex-direction: column; gap: 10px; max-height: 400px; overflow-y: auto;',
|
||||
},
|
||||
historyEntries.map((h, i) =>
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'padding: 10px 14px; background: #fafafa; border: 1px solid #eee; border-radius: 6px;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; justify-content: space-between; font-weight: 600; font-size: 12px; color: #333; margin-bottom: 6px;',
|
||||
},
|
||||
[
|
||||
E('span', {}, `Run #${historyEntries.length - i}`),
|
||||
E('span', { style: 'color: #888;' }, h.timestamp),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'pre',
|
||||
{
|
||||
style:
|
||||
'margin: 0; white-space: pre-wrap; font-size: 11px; max-height: 120px; overflow-y: auto; color: #555;',
|
||||
},
|
||||
h.report,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const mainContainer = E(
|
||||
'div',
|
||||
{ class: 'tachyon-partial-modal__body' },
|
||||
[],
|
||||
);
|
||||
|
||||
const renderModalLayout = () => {
|
||||
mainContainer.replaceChildren(
|
||||
E('div', {}, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 1px solid #eee; padding-bottom: 8px;',
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: [
|
||||
activeTab === 'diagnosis'
|
||||
? 'cbi-button-action'
|
||||
: 'cbi-button-neutral',
|
||||
],
|
||||
text: `📋 ${_('Current Diagnosis')}`,
|
||||
onClick: () => {
|
||||
activeTab = 'diagnosis';
|
||||
renderModalLayout();
|
||||
},
|
||||
}),
|
||||
renderButton({
|
||||
classNames: [
|
||||
activeTab === 'history'
|
||||
? 'cbi-button-action'
|
||||
: 'cbi-button-neutral',
|
||||
],
|
||||
text: `🕒 ${_('Run History')} (${historyEntries.length})`,
|
||||
onClick: () => {
|
||||
activeTab = 'history';
|
||||
renderModalLayout();
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
activeTab === 'diagnosis'
|
||||
? renderDiagnosisTabContent()
|
||||
: renderHistoryTabContent(),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: 'tachyon-partial-modal__footer',
|
||||
style: 'margin-top: 15px;',
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: ['cbi-button-apply'],
|
||||
text: _('Copy'),
|
||||
onClick: () => copyToClipboard(`\`\`\`\n${report}\n\`\`\``),
|
||||
}),
|
||||
renderButton({
|
||||
classNames: ['cbi-button-remove'],
|
||||
text: _('Close'),
|
||||
onClick: () => {
|
||||
ui.hideModal();
|
||||
},
|
||||
}),
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
renderModalLayout();
|
||||
ui.showModal(_('AI Doctor Diagnosis'), mainContainer);
|
||||
} else {
|
||||
const errorMsg =
|
||||
typeof (aiRes as { error?: string }).error === 'string'
|
||||
? (aiRes as { error?: string }).error
|
||||
: _('Unknown error');
|
||||
showToast(_('AI Doctor failed') + ': ' + errorMsg, 'error');
|
||||
ui.hideModal();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
@@ -924,6 +1219,7 @@ async function handleRunAiDoctor() {
|
||||
e instanceof Error ? e.message : String(e),
|
||||
);
|
||||
showToast(_('AI Doctor failed'), 'error');
|
||||
ui.hideModal();
|
||||
} finally {
|
||||
setDiagnosticActionLoading('aiDoctor', false);
|
||||
runChecks();
|
||||
|
||||
@@ -11123,12 +11123,90 @@ async function handleRunDoctor() {
|
||||
runChecks();
|
||||
}
|
||||
}
|
||||
function getAiDoctorHistory() {
|
||||
try {
|
||||
const raw = localStorage.getItem("tachyon_ai_doctor_history");
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function saveAiDoctorHistory(entry) {
|
||||
try {
|
||||
const current = getAiDoctorHistory();
|
||||
const updated = [entry, ...current].slice(0, 5);
|
||||
localStorage.setItem("tachyon_ai_doctor_history", JSON.stringify(updated));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
async function handleRunAiDoctor() {
|
||||
setDiagnosticActionLoading("aiDoctor", true);
|
||||
let activeTab2 = "diagnosis";
|
||||
let historyEntries = getAiDoctorHistory();
|
||||
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;"
|
||||
},
|
||||
"\u26A1 " + _("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" }, "\u23F3"),
|
||||
E("span", { class: "step-text" }, step)
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]);
|
||||
ui.showModal(_("AI Doctor Diagnosis"), modalBody);
|
||||
const updateStep = (idx, icon, color) => {
|
||||
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 === "\u2705" ? "500" : "normal";
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < stepperSteps.length; i++) {
|
||||
updateStep(i, "\u{1F504}", "#1976d2");
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
updateStep(i, "\u2705", "#2e7d32");
|
||||
}
|
||||
try {
|
||||
const aiRes = await TachyonShellMethods.aiDoctor();
|
||||
if (!aiRes || typeof aiRes !== "object") {
|
||||
showToast(_("AI Doctor failed") + ": " + _("Unknown error"), "error");
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
const rawData = aiRes.data;
|
||||
@@ -11136,41 +11214,101 @@ async function handleRunAiDoctor() {
|
||||
if (aiRes.success || data && data.success) {
|
||||
const report = data ? String(data.report ?? "") : String(rawData ?? "");
|
||||
const quickFixes = Array.isArray(data?.quick_fixes) ? data.quick_fixes : String(data?.quick_fix ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const title = _("AI Doctor Diagnosis");
|
||||
const modalContent = E("div", { class: "tachyon-partial-modal__body" }, [
|
||||
E("div", {}, [
|
||||
const nowStr = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
||||
saveAiDoctorHistory({ timestamp: nowStr, report, quickFixes });
|
||||
historyEntries = getAiDoctorHistory();
|
||||
const repLower = report.toLowerCase();
|
||||
const nodes = [
|
||||
{
|
||||
name: "WAN",
|
||||
status: repLower.includes("wan interface down") ? "FAIL" : "OK"
|
||||
},
|
||||
{
|
||||
name: "DNS",
|
||||
status: repLower.includes("dnsmasq") || repLower.includes("dns") ? repLower.includes("dns failed") || repLower.includes("dnsmasq failed") ? "FAIL" : "OK" : "OK"
|
||||
},
|
||||
{
|
||||
name: "sing-box",
|
||||
status: repLower.includes("sing-box") && (repLower.includes("stopped") || repLower.includes("error")) ? "FAIL" : "OK"
|
||||
},
|
||||
{
|
||||
name: "nftables",
|
||||
status: repLower.includes("nftables") || repLower.includes("rules") ? repLower.includes("damaged") || repLower.includes("corrupted") ? "WARN" : "OK" : "OK"
|
||||
}
|
||||
];
|
||||
const renderRootCauseBanner = () => {
|
||||
return E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 10px 14px; background: rgba(0,0,0,0.03); border-radius: 6px; margin-bottom: 14px; border: 1px solid rgba(0,0,0,0.06); font-size: 12px; font-weight: 500;"
|
||||
},
|
||||
nodes.flatMap((node, idx) => {
|
||||
const badgeBg = node.status === "OK" ? "#e8f5e9" : node.status === "WARN" ? "#fff3e0" : "#ffebee";
|
||||
const badgeFg = node.status === "OK" ? "#2e7d32" : node.status === "WARN" ? "#e65100" : "#c62828";
|
||||
const badgeIcon = node.status === "OK" ? "\u2713" : node.status === "WARN" ? "\u26A0\uFE0F" : "\u274C";
|
||||
const itemEl = E(
|
||||
"span",
|
||||
{
|
||||
style: `padding: 4px 8px; border-radius: 4px; background: ${badgeBg}; color: ${badgeFg}; display: inline-flex; align-items: center; gap: 4px;`
|
||||
},
|
||||
`${node.name} [${badgeIcon} ${node.status}]`
|
||||
);
|
||||
return idx < nodes.length - 1 ? [
|
||||
itemEl,
|
||||
E(
|
||||
"span",
|
||||
{ style: "color: #999; font-weight: bold;" },
|
||||
"\u2794"
|
||||
)
|
||||
] : [itemEl];
|
||||
})
|
||||
);
|
||||
};
|
||||
const renderDiagnosisTabContent = () => {
|
||||
return E("div", {}, [
|
||||
renderRootCauseBanner(),
|
||||
E(
|
||||
"pre",
|
||||
{
|
||||
class: "tachyon-partial-modal__content",
|
||||
style: "white-space: pre-wrap; font-family: inherit; font-size: 13px; line-height: 1.5; max-height: 400px; overflow-y: auto;"
|
||||
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;"
|
||||
},
|
||||
E("code", {}, report)
|
||||
),
|
||||
quickFixes.length > 0 ? E(
|
||||
"div",
|
||||
{
|
||||
style: "margin-top: 15px; padding: 12px; background: rgba(0,0,0,0.03); border-radius: 6px; border: 1px solid rgba(0,0,0,0.08);"
|
||||
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);"
|
||||
},
|
||||
[
|
||||
E("b", {}, _("Recommended Quick Fixes:")),
|
||||
E(
|
||||
"b",
|
||||
{ style: "font-size: 13px;" },
|
||||
_("Recommended Smart Quick Fixes:")
|
||||
),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;"
|
||||
style: "display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px;"
|
||||
},
|
||||
[
|
||||
...quickFixes.map(
|
||||
(code) => renderButton({
|
||||
...quickFixes.map((code) => {
|
||||
let applied = false;
|
||||
const btn = renderButton({
|
||||
classNames: ["cbi-button-apply"],
|
||||
text: _("Fix") + ": " + code,
|
||||
text: `\u26A1 ${code}`,
|
||||
onClick: async () => {
|
||||
if (applied) return;
|
||||
showToast(
|
||||
_("Applying fix") + ": " + code + "...",
|
||||
"success"
|
||||
);
|
||||
const fixRes = await TachyonShellMethods.applyQuickFix(code);
|
||||
if (fixRes && typeof fixRes === "object" && fixRes.success) {
|
||||
applied = true;
|
||||
btn.textContent = `\u2713 ${code} (${_("Fixed")})`;
|
||||
btn.classList.remove("cbi-button-apply");
|
||||
btn.classList.add("cbi-button-neutral");
|
||||
showToast(
|
||||
_("Fix applied") + ": " + code,
|
||||
"success"
|
||||
@@ -11182,11 +11320,12 @@ async function handleRunAiDoctor() {
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
});
|
||||
return btn;
|
||||
}),
|
||||
quickFixes.length > 1 ? renderButton({
|
||||
classNames: ["cbi-button-save"],
|
||||
text: _("Fix All"),
|
||||
classNames: ["cbi-button-action"],
|
||||
text: `\u26A1 ${_("Fix All")}`,
|
||||
onClick: async () => {
|
||||
showToast(_("Applying all fixes..."), "success");
|
||||
const fixRes = await TachyonShellMethods.applyQuickFix(
|
||||
@@ -11208,39 +11347,123 @@ async function handleRunAiDoctor() {
|
||||
) : E(
|
||||
"div",
|
||||
{
|
||||
style: "margin-top: 10px; color: #2e7d32; font-weight: 500;"
|
||||
style: "margin-top: 12px; color: #2e7d32; font-weight: 500; font-size: 13px;"
|
||||
},
|
||||
"\u2705 " + _("No quick fix required")
|
||||
),
|
||||
E(
|
||||
)
|
||||
]);
|
||||
};
|
||||
const renderHistoryTabContent = () => {
|
||||
if (historyEntries.length === 0) {
|
||||
return E(
|
||||
"div",
|
||||
{
|
||||
class: "tachyon-partial-modal__footer",
|
||||
style: "margin-top: 15px;"
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: ["cbi-button-apply"],
|
||||
text: _("Copy"),
|
||||
onClick: () => copyToClipboard(`\`\`\`
|
||||
{ style: "padding: 20px; text-align: center; color: #777;" },
|
||||
_("No diagnostic history available yet.")
|
||||
);
|
||||
}
|
||||
return E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; flex-direction: column; gap: 10px; max-height: 400px; overflow-y: auto;"
|
||||
},
|
||||
historyEntries.map(
|
||||
(h, i) => E(
|
||||
"div",
|
||||
{
|
||||
style: "padding: 10px 14px; background: #fafafa; border: 1px solid #eee; border-radius: 6px;"
|
||||
},
|
||||
[
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; justify-content: space-between; font-weight: 600; font-size: 12px; color: #333; margin-bottom: 6px;"
|
||||
},
|
||||
[
|
||||
E("span", {}, `Run #${historyEntries.length - i}`),
|
||||
E("span", { style: "color: #888;" }, h.timestamp)
|
||||
]
|
||||
),
|
||||
E(
|
||||
"pre",
|
||||
{
|
||||
style: "margin: 0; white-space: pre-wrap; font-size: 11px; max-height: 120px; overflow-y: auto; color: #555;"
|
||||
},
|
||||
h.report
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
const mainContainer = E(
|
||||
"div",
|
||||
{ class: "tachyon-partial-modal__body" },
|
||||
[]
|
||||
);
|
||||
const renderModalLayout = () => {
|
||||
mainContainer.replaceChildren(
|
||||
E("div", {}, [
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; gap: 10px; margin-bottom: 14px; border-bottom: 1px solid #eee; padding-bottom: 8px;"
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: [
|
||||
activeTab2 === "diagnosis" ? "cbi-button-action" : "cbi-button-neutral"
|
||||
],
|
||||
text: `\u{1F4CB} ${_("Current Diagnosis")}`,
|
||||
onClick: () => {
|
||||
activeTab2 = "diagnosis";
|
||||
renderModalLayout();
|
||||
}
|
||||
}),
|
||||
renderButton({
|
||||
classNames: [
|
||||
activeTab2 === "history" ? "cbi-button-action" : "cbi-button-neutral"
|
||||
],
|
||||
text: `\u{1F552} ${_("Run History")} (${historyEntries.length})`,
|
||||
onClick: () => {
|
||||
activeTab2 = "history";
|
||||
renderModalLayout();
|
||||
}
|
||||
})
|
||||
]
|
||||
),
|
||||
activeTab2 === "diagnosis" ? renderDiagnosisTabContent() : renderHistoryTabContent(),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
class: "tachyon-partial-modal__footer",
|
||||
style: "margin-top: 15px;"
|
||||
},
|
||||
[
|
||||
renderButton({
|
||||
classNames: ["cbi-button-apply"],
|
||||
text: _("Copy"),
|
||||
onClick: () => copyToClipboard(`\`\`\`
|
||||
${report}
|
||||
\`\`\``)
|
||||
}),
|
||||
renderButton({
|
||||
classNames: ["cbi-button-remove"],
|
||||
text: _("Close"),
|
||||
onClick: () => {
|
||||
ui.hideModal();
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
]);
|
||||
ui.showModal(title, modalContent);
|
||||
}),
|
||||
renderButton({
|
||||
classNames: ["cbi-button-remove"],
|
||||
text: _("Close"),
|
||||
onClick: () => {
|
||||
ui.hideModal();
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
);
|
||||
};
|
||||
renderModalLayout();
|
||||
ui.showModal(_("AI Doctor Diagnosis"), mainContainer);
|
||||
} else {
|
||||
const errorMsg = typeof aiRes.error === "string" ? aiRes.error : _("Unknown error");
|
||||
showToast(_("AI Doctor failed") + ": " + errorMsg, "error");
|
||||
ui.hideModal();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
@@ -11249,6 +11472,7 @@ ${report}
|
||||
e instanceof Error ? e.message : String(e)
|
||||
);
|
||||
showToast(_("AI Doctor failed"), "error");
|
||||
ui.hideModal();
|
||||
} finally {
|
||||
setDiagnosticActionLoading("aiDoctor", false);
|
||||
runChecks();
|
||||
|
||||
@@ -2929,6 +2929,9 @@ function diagnose_json() {
|
||||
try { ai_status_data = json(ai_status_raw); } catch(e) {}
|
||||
}
|
||||
|
||||
let raw_log_snippet = trim(command_output("logread | grep -iE 'tachyon|sing-box|dnsmasq|oom|error|fatal|byedpi|zapret|nftables' | tail -n 50 2>/dev/null")) || "No recent system errors logged.";
|
||||
let log_snippet = compress_log_snippet(raw_log_snippet);
|
||||
|
||||
print(sprintf("%J\n", {
|
||||
success: true,
|
||||
timestamp: time(),
|
||||
@@ -2936,6 +2939,7 @@ function diagnose_json() {
|
||||
issues_fixed: res.fixed,
|
||||
overall: (res.issues == 0) ? "healthy" : ((res.fixed == res.issues) ? "repaired" : "degraded"),
|
||||
problems: problems,
|
||||
log_snippet: log_snippet,
|
||||
watchdog_status: ai_status_data
|
||||
}));
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user