'use strict'; 'require view'; 'require form'; 'require uci'; 'require ui'; 'require poll'; 'require rpc'; 'require fs'; 'require tools.clashoo as clashoo'; function getThemeClass() { var h = document.documentElement; // Bootstrap: explicit data-bs-theme attribute wins first if (h.dataset.bsTheme === 'dark') return 'cl-theme-dark'; if (h.dataset.bsTheme === 'light') return 'cl-theme-light'; // Argon: explicit darkmode attribute if (h.dataset.darkmode === 'true') return 'cl-theme-dark'; // Argon: dark.css loaded in document = dark mode active (most reliable) var links = document.querySelectorAll('link[rel="stylesheet"]'); for (var i = 0; i < links.length; i++) { if (links[i].href && links[i].href.indexOf('dark.css') !== -1) return 'cl-theme-dark'; } // Generic: .dark class on root element if (h.classList.contains('dark')) return 'cl-theme-dark'; // Body background luminance (skip transparent/rgba backgrounds) var bg = window.getComputedStyle(document.body).backgroundColor; if (bg && bg.indexOf('rgba') === -1 && bg.indexOf('rgb') !== -1) { var m = bg.match(/\d+/g); if (m && m.length >= 3 && (parseInt(m[0]) + parseInt(m[1]) + parseInt(m[2])) / 3 < 100) return 'cl-theme-dark'; } return 'cl-theme-light'; } var callHostHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect: { '': {} } }); var callBackupExportPrepare = rpc.declare({ object: 'luci.clashoo', method: 'backup_export_prepare', expect: {} }); var callBackupExportCleanup = rpc.declare({ object: 'luci.clashoo', method: 'backup_export_cleanup', params: ['path'], expect: {} }); var callBackupImportPrepare = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_prepare', expect: {} }); var callBackupImportChunk = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_chunk', params: ['path', 'offset', 'data'], expect: {} }); var callBackupImportCleanup = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_cleanup', params: ['path'], expect: {} }); var callBackupImportFile = rpc.declare({ object: 'luci.clashoo', method: 'backup_import_file', params: ['name', 'path'], expect: {} }); var callBackupReset = rpc.declare({ object: 'luci.clashoo', method: 'backup_reset', expect: {} }); var CSS = [ '.cl-wrap{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif}', '.cl-tabs{display:flex;border-bottom:2px solid rgba(128,128,128,.15);margin-bottom:18px}', '.cl-tab{padding:10px 20px;cursor:pointer;font-size:13px;opacity:.55;border-bottom:2px solid transparent;margin-bottom:-2px}', '.cl-tab.active{opacity:1;border-bottom-color:currentColor;font-weight:600}', '.cl-panel{display:none}.cl-panel.active{display:block}', '.cl-section{margin-bottom:20px}', '.cl-section h4{font-size:.95rem;font-weight:600;margin-bottom:10px;color:var(--title-color,rgba(92,102,120,.72));opacity:.95}', '.cl-save-bar{display:flex;gap:8px;margin-top:14px;padding-top:12px;border-top:1px solid rgba(128,128,128,.15)}', '.cl-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}', '.cl-log-area{font-family:monospace;font-size:11px;opacity:.75;max-height:300px;overflow-y:auto;border:1px solid rgba(128,128,128,.2);border-radius:8px;padding:10px;white-space:pre-wrap;margin-top:8px}', '.cl-log-tabs{display:flex;gap:8px;margin-bottom:8px}', '.cl-log-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:8px}', '.cl-log-toolbar label{font-size:11px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;margin:0}', /* Argon shifts label>checkbox down (top:.4rem); reset so flex centers it */ '.cl-log-toolbar input[type=checkbox]{position:static;top:auto;right:auto;margin:0}', /* unify select + button heights so the row lines up */ '.cl-log-toolbar>select,.cl-log-toolbar>button{height:26px;box-sizing:border-box;font-size:11px;line-height:1;padding:0 10px;margin:0}', '.cl-log-tab{padding:4px 12px;border:1px solid rgba(128,128,128,.2);border-radius:20px;font-size:12px;cursor:pointer;opacity:.6}', '.cl-log-tab.active{opacity:1;font-weight:600;background:rgba(128,128,128,.1)}', '.cl-log-line{display:block;padding:1px 4px;border-radius:3px}', /* Light theme: AA+ contrast on white bg (issue #16) */ '.cl-log-line.cl-log-info{color:#1f6b3f}', '.cl-log-line.cl-log-warn{color:#8b5a00}', '.cl-log-line.cl-log-error{color:#c0392b;background:rgba(192,57,43,.06)}', '.cl-log-line.cl-log-debug{color:#5a6270;opacity:.85}', '.cl-log-line.cl-log-hidden{display:none}', '.cl-log-ts{color:#5a6270;margin-right:8px}', '.cl-log-msg{color:inherit}', /* Dark theme overrides: original colors tuned for dark bg */ '.cl-theme-dark .cl-log-line.cl-log-info{color:#7fc7a8}', '.cl-theme-dark .cl-log-line.cl-log-warn{color:#e8b95a}', '.cl-theme-dark .cl-log-line.cl-log-error{color:#ea7878;background:rgba(234,120,120,.06)}', '.cl-theme-dark .cl-log-line.cl-log-debug{color:#7a8290;opacity:.7}', '.cl-theme-dark .cl-log-ts{color:#6b7480}', '.cl-dl-hint{margin-top:6px;font-size:12px;min-height:18px;line-height:1.4}', '.cl-component-card{padding:16px 18px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:var(--border-radius,var(--cl-radius,12px));background:var(--cl-card-bg,#fff);box-shadow:var(--card-shadow,var(--cl-card-shadow));margin:0 14px 14px}', '.cl-component-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:12px}', '.cl-component-head h4{display:flex;align-items:center;gap:8px;margin:0 0 4px !important;padding:0 !important;font-size:0.95rem;font-weight:600;color:var(--title-color,var(--cl-title-color,inherit));background:transparent !important}', '.cl-component-head h4:before{content:"";width:3px;height:14px;border-radius:2px;background:var(--primary-color,var(--cl-primary,#2f80ed));flex:0 0 3px}', '.cl-component-adv{margin-top:10px}', '.cl-component-adv-bar{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;cursor:pointer;user-select:none;font-size:13px;font-weight:700;background:var(--cl-control-bg,rgba(128,128,128,.04));border:1px solid var(--cl-control-border,rgba(128,128,128,.14));border-radius:8px}', '.cl-component-adv-bar:hover{background:var(--cl-primary-soft,rgba(0,122,255,.08))}', '.cl-component-adv-chevron{font-size:16px;font-weight:700;opacity:.55;transition:transform .2s}', '.cl-component-adv:not(.cl-closed) .cl-component-adv-chevron{transform:rotate(90deg)}', '.cl-component-adv-body{margin-top:8px}', '.cl-component-adv.cl-closed .cl-component-adv-body{display:none}', '.cl-component-sub{font-size:12px;color:rgba(120,130,150,.9);line-height:1.45}', '.cl-component-list{display:grid;gap:8px}', '.cl-component-row{display:grid;grid-template-columns:minmax(170px,1.1fr) minmax(180px,1.5fr) minmax(130px,.9fr) auto;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff)}', '.cl-component-name{font-weight:700;font-size:13px}', '.cl-component-desc{font-size:12px;color:rgba(120,130,150,.92);margin-top:2px}', '.cl-component-version{font-size:12px;line-height:1.45;color:rgba(90,100,120,.95);word-break:break-word}', '.cl-theme-dark .cl-component-version{color:rgba(210,220,235,.86)}', '.cl-component-status{font-size:12px;line-height:1.45;color:rgba(90,100,120,.95)}', '.cl-component-status.cl-st-running{color:#2f80ed}.cl-component-status.cl-st-success{color:#239b56}.cl-component-status.cl-st-failed{color:#d43f3a}', '.cl-component-log{margin-top:10px;font-family:monospace;font-size:11px;white-space:pre-wrap;max-height:140px;overflow:auto;padding:9px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff);color:#1f6b3f}', '.cl-theme-dark .cl-component-log{color:#a3d9ad}', '.cl-component-arch{margin-bottom:12px}', '.cl-component-arch-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:9px 12px;border:1px solid var(--cl-surface-border,rgba(128,128,128,.14));border-radius:8px;background:var(--cl-card-bg,#fff)}', '.cl-component-arch-label{font-size:13px;font-weight:700}', '.cl-component-arch-auto{font-size:12px;color:rgba(90,100,120,.82);line-height:1.2}', '.cl-theme-dark .cl-component-arch-auto{color:rgba(210,220,235,.78)}', '.cl-wrap .cl-component-arch-sel{font-size:12px!important;font-weight:700!important;padding:4px 9px!important;border:1px solid rgba(46,170,92,.28)!important;border-radius:6px!important;background:rgba(46,170,92,.12)!important;color:#24844d!important}', '.cl-wrap.cl-theme-dark .cl-component-arch-sel{background:rgba(104,199,135,.16)!important;color:#c6f6d3!important;border-color:rgba(104,199,135,.3)!important}', '.cl-component-arch-sel option{background:var(--background-color,#fff);color:inherit}', '.cl-component-arch-hint{font-size:12px;color:rgba(120,130,150,.9)}', '.cl-comp-var-box{display:inline-flex;gap:4px;margin-top:5px}', '.cl-comp-var{font-size:11px;padding:2px 10px;border-radius:10px;border:1px solid rgba(128,128,128,.3);background:transparent;color:inherit;cursor:pointer;min-width:52px}', '.cl-comp-var.on{background:rgba(var(--primary-rgb,0,122,255),.18);border-color:rgba(var(--primary-rgb,0,122,255),.64);color:var(--primary-color,#0b68dd);font-weight:800;box-shadow:0 0 0 1px rgba(var(--primary-rgb,0,122,255),.1) inset}', '.cl-comp-updatable{font-size:10px;font-weight:700;color:#239b56;margin-left:2px}', '@media(max-width:760px){.cl-component-head{display:block}.cl-component-head .btn{margin-top:10px}.cl-component-row{grid-template-columns:1fr;gap:6px}.cl-component-row .btn{width:auto;justify-self:start;padding-left:18px;padding-right:18px}}', /* align form.Map font with config page */ '.cl-panel .cbi-section>h3{font-size:13px !important;font-weight:600;margin-bottom:8px}', '.cl-panel .cbi-value-title{font-size:13px !important}', '.cl-panel .cbi-value-field input,.cl-panel .cbi-value-field select,.cl-panel .cbi-value-field textarea{font-size:13px !important}', '.cl-panel .cbi-section-descr,.cl-panel .cbi-value-helptext{font-size:12px !important}', '.cl-panel .cbi-section{margin-bottom:12px}', '.cl-wrap .cbi-section>h3,.cl-wrap .cbi-value-title,.cl-wrap .cbi-section-descr,.cl-wrap .cbi-value-helptext{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', '.cl-wrap .cbi-input-text,.cl-wrap .cbi-input-select,.cl-wrap select,.cl-wrap input,.cl-wrap textarea,.cl-wrap .btn,.cl-wrap .cbi-button{font-size:13px !important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif !important}', '.cl-wrap .btn,.cl-wrap .cbi-button{padding:4px 10px;line-height:1.35}', '#cbi-clashoo-lan_acl{overflow-x:visible!important}', '#cbi-clashoo-lan_acl .cbi-section-table{display:flex;width:100%;overflow:visible!important;flex-direction:column;border:0!important}', '#cbi-clashoo-lan_acl .cbi-section-thead{display:none}', '#cbi-clashoo-lan_acl .cbi-section-tbody{display:flex;width:100%;flex-direction:column}', '#cbi-clashoo-lan_acl .cbi-section-table-row{display:flex;width:100%;padding:14px 0;border-top:1px solid rgba(128,128,128,.16);flex-direction:column;align-items:stretch}', '#cbi-clashoo-lan_acl .cbi-section-table-row:first-child{border-top:0}', '#cbi-clashoo-lan_acl .cl-acl-empty-row,#cbi-clashoo-lan_acl .cl-acl-empty-row>*{border:0!important;box-shadow:none!important}', '#cbi-clashoo-lan_acl .cl-acl-value{width:100%;border:0!important;box-sizing:border-box}', '#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown{width:max-content;min-width:100%;max-width:calc(100vw - 48px)}', '@media(max-width:600px){#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown{width:100%!important;min-width:0;max-width:100%}#cbi-clashoo-lan_acl .cl-acl-value .cbi-dropdown[open]>ul.dropdown>li{height:auto;white-space:normal;overflow-wrap:anywhere;line-height:1.35}}', '#cbi-clashoo-lan_acl .cbi-section-actions{display:flex!important;justify-content:flex-end;width:auto!important;padding:8px 0 0!important;border:0!important}', '#cbi-clashoo-lan_acl .cbi-section-actions>div{display:flex;gap:8px}', '#cbi-clashoo-lan_acl .cbi-section-create{padding-top:10px}' ].join(''); /* component update: main area = clashoo/client only; rest in collapsible */ var COMPONENT_MAIN_IDS = { clashoo: true, luci: true }; function fastResolve(promise, timeoutMs, fallback) { var t = new Promise(function (resolve) { setTimeout(function () { resolve(fallback); }, timeoutMs); }); return Promise.race([L.resolveDefault(promise, fallback), t]); } function decorateSystemForm(root) { if (!root || !root.querySelectorAll) return; var fields = root.querySelectorAll('.cbi-value-field'); for (var i = 0; i < fields.length; i++) { if (fields[i] && fields[i].classList) fields[i].classList.add('cl-control-wrap'); } var sections = root.querySelectorAll('.cbi-section'); for (var j = 0; j < sections.length; j++) { if (sections[j] && sections[j].classList) sections[j].classList.add('cl-form-card'); } } function normalizeLanAclLayout(root) { var rows = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-section-table-row'); for (var r = 0; r < rows.length; r++) { if (!rows[r].querySelector('input, select, button, .cbi-dropdown')) rows[r].classList.add('cl-acl-empty-row'); } var cells = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-section-table-row > td.cbi-value-field:not(.cl-acl-value)'); for (var i = 0; i < cells.length; i++) { var cell = cells[i]; var control = cell.firstElementChild; if (!control) continue; cell.parentElement.classList.add('cbi-section-node', 'cl-acl-node'); var field = document.createElement('div'); field.className = 'cbi-value-field'; field.appendChild(control); var label = document.createElement('label'); label.className = 'cbi-value-title'; label.textContent = cell.getAttribute('data-title') || ''; cell.textContent = ''; cell.className = 'cbi-value cl-acl-value'; cell.appendChild(label); cell.appendChild(field); } } function syncLanAclControlWidth(root) { var reference = root.querySelector('#cbi-clashoo-config-bypass_port_mode select'); if (!reference) return; var referenceWidth = reference.getBoundingClientRect().width; if (!referenceWidth) return; var section = root.querySelector('#cbi-clashoo-lan_acl'); var sectionRect = section.getBoundingClientRect(); var sectionStyle = window.getComputedStyle(section); var contentRight = sectionRect.right - parseFloat(sectionStyle.paddingRight || 0); var lists = root.querySelectorAll('#cbi-clashoo-lan_acl .cbi-dynlist'); for (var i = 0; i < lists.length; i++) { var left = lists[i].getBoundingClientRect().left; var width = Math.min(referenceWidth, contentRight - left); lists[i].style.width = Math.max(0, width) + 'px'; lists[i].style.minWidth = '0'; lists[i].style.maxWidth = 'none'; var controls = lists[i].querySelectorAll('.add-item, .cbi-dropdown'); for (var j = 0; j < controls.length; j++) { controls[j].style.width = '100%'; controls[j].style.minWidth = '0'; controls[j].style.maxWidth = '100%'; controls[j].style.boxSizing = 'border-box'; } } } function watchLanAclLayout(root) { normalizeLanAclLayout(root); syncLanAclControlWidth(root); var observer = new MutationObserver(function () { normalizeLanAclLayout(root); syncLanAclControlWidth(root); }); observer.observe(root, { childList: true, subtree: true }); window.addEventListener('resize', function () { syncLanAclControlWidth(root); }); } function randomSecret(len) { var chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'; var out = ''; var n = Math.max(6, parseInt(len, 10) || 6); for (var i = 0; i < n; i++) { out += chars.charAt(Math.floor(Math.random() * chars.length)); } return out; } function enhanceDashPasswordField(root) { if (!root || !root.querySelector) return; var input = root.querySelector('input[id$=".dash_pass"], input[name$=".dash_pass"]'); if (!input || input.dataset.clEnhanced === '1') return; input.dataset.clEnhanced = '1'; input.type = 'password'; input.autocomplete = 'new-password'; var parent = input.parentNode; if (!parent) return; /* Remove LuCI default password helper controls (e.g. stray "*" button) */ var children = parent.children ? Array.prototype.slice.call(parent.children) : []; children.forEach(function (el) { if (!el || el === input) return; var tag = (el.tagName || '').toUpperCase(); var isBtnLike = tag === 'BUTTON' || (tag === 'INPUT' && String(el.type || '').toLowerCase() === 'button') || ((el.className || '').indexOf('cbi-button') >= 0); if (isBtnLike) parent.removeChild(el); }); var wrap = E('div', { 'class': 'cl-pass-wrap' }); parent.insertBefore(wrap, input); wrap.appendChild(input); var eyeBtn = E('button', { type: 'button', 'class': 'btn cbi-button cl-pass-btn', title: _("Show/Hide"), click: function (ev) { ev.preventDefault(); var show = input.type === 'password'; input.type = show ? 'text' : 'password'; eyeBtn.textContent = show ? '🙈' : '👁'; } }, '👁'); var genBtn = E('button', { type: 'button', 'class': 'btn cbi-button-action cl-pass-btn cl-pass-gen', click: function (ev) { ev.preventDefault(); input.type = 'text'; input.value = randomSecret(6); eyeBtn.textContent = '🙈'; input.dispatchEvent(new Event('change', { bubbles: true })); } }, _("Random")); wrap.appendChild(eyeBtn); wrap.appendChild(genBtn); } function clearClashooDirty() { var applyPromise; try { applyPromise = (L.uci && typeof L.uci.callApply === 'function') ? Promise.resolve(L.uci.callApply(0, false)).catch(function () {}) : Promise.resolve(); } catch (e) { applyPromise = Promise.resolve(); } return applyPromise.then(function () { try { if (L.ui && L.ui.changes && L.ui.changes.changes) { delete L.ui.changes.changes.clashoo; var n = Object.keys(L.ui.changes.changes).length; if (typeof L.ui.changes.renderChangeIndicator === 'function') L.ui.changes.renderChangeIndicator(n); else if (typeof L.ui.changes.setIndicator === 'function') L.ui.changes.setIndicator(n); } } catch (e) {} }); } function saveCommitApplyMaybeReload(m, runningMsg, stoppedMsg) { return clashoo.status() .then(function (st) { return !!(st && st.running); }) .catch(function () { return false; }) .then(function (running) { return m.save() .then(function () { return clashoo.commitConfig(); }) .then(function () { return running ? clashoo.reload() : { success: true, skipped: true }; }) .then(function () { return clearClashooDirty(); }) .then(function () { ui.addNotification(null, E('p', running ? runningMsg : stoppedMsg)); window.setTimeout(function () { location.reload(); }, 300); }); }); } function coreChoiceFromConfig(coreType, dcore) { if (coreType === 'singbox') return dcore === '5' ? 'singbox-alpha' : 'singbox-stable'; if (dcore === '1') return 'smart'; if (dcore === '3') return 'mihomo-alpha'; return 'mihomo-stable'; } function coreChoiceTarget(choice) { var targets = { 'mihomo-stable': { core: 'mihomo', dcore: '2' }, 'mihomo-alpha': { core: 'mihomo', dcore: '3' }, 'smart': { core: 'mihomo', dcore: '1' }, 'singbox-stable': { core: 'singbox', dcore: '4' }, 'singbox-alpha': { core: 'singbox', dcore: '5' } }; return targets[choice] || targets['mihomo-stable']; } function waitForCoreTarget(target, attempts, sawRestart) { attempts = attempts == null ? 50 : attempts; sawRestart = !!sawRestart; return clashoo.status().then(function (st) { st = st || {}; var selected = st.core_type === target.core && String(st.dcore || '') === target.dcore; var transitioning = !st.running || st.health_status === 'stopped' || st.health_status === 'starting'; sawRestart = sawRestart || transitioning; if (sawRestart && selected && st.running && st.health_status !== 'stopped' && st.health_status !== 'fail') return st; if (selected && st.health_status === 'fail') throw new Error(_("The new core failed to start, please check the core log")); if (attempts <= 0) throw new Error(_("Timeout waiting for new core to start, please check the core log")); return new Promise(function (resolve) { setTimeout(resolve, 600); }).then(function () { return waitForCoreTarget(target, attempts - 1, sawRestart); }); }); } // tab persistence (hash + localStorage), shared shape with config.js so save/apply // reloads keep the active tab instead of snapping back to the first one function readSavedTab(key, fallback, allowed) { var raw = ''; if (window.location.hash) raw = window.location.hash.replace(/^#/, ''); if (!raw) { try { raw = window.localStorage.getItem(key) || ''; } catch (e) {} } return allowed.indexOf(raw) >= 0 ? raw : fallback; } function rememberTab(key, id) { try { window.localStorage.setItem(key, id); } catch (e) {} if (window.history && window.history.replaceState) window.history.replaceState(null, '', '#' + id); else window.location.hash = id; } function readBackupSliceBase64(file, start, end) { return new Promise(function (resolve, reject) { var reader = new FileReader(); reader.onload = function () { var result = String(reader.result || ''); var comma = result.indexOf(','); if (comma < 0) reject(new Error(_("Unable to read backup file"))); else resolve(result.substring(comma + 1)); }; reader.onerror = function () { reject(reader.error || new Error(_("Unable to read backup file"))); }; reader.readAsDataURL(file.slice(start, end)); }); } function uploadBackupChunks(file, path) { var offset = 0; var chunkSize = 32 * 1024; function next() { if (offset >= file.size) return Promise.resolve(); var end = Math.min(offset + chunkSize, file.size); var expected = end; return readBackupSliceBase64(file, offset, end) .then(function (data) { return callBackupImportChunk(path, offset, data); }) .then(function (r) { if (!r || !r.success) throw new Error((r && r.message) || _("Upload failed")); if (parseInt(r.offset, 10) !== expected) throw new Error(_("Upload offset mismatch")); offset = expected; return next(); }); } return next(); } return view.extend({ _tab: 'kernel', _logTab: 'plugin', load: function () { return Promise.all([ fastResolve(clashoo.getCpuArch(), 5000, ''), fastResolve(clashoo.getLogStatus(), 1200, {}), fastResolve(clashoo.readLog(), 1200, ''), uci.load('clashoo'), fastResolve(L.resolveDefault(callHostHints(), {}), 1500, {}) ]); }, render: function (data) { var self = this; var cpuArch = data[0] || ''; var logStatus = data[1] || {}; var runLog = data[2] || ''; var hostHints = data[4] || {}; this._hostHints = hostHints; if (!document.getElementById('cl-css')) { var s = document.createElement('style'); s.id = 'cl-css'; s.textContent = CSS; document.head.appendChild(s); } if (!document.getElementById('cl-css-ext')) { var link = document.createElement('link'); link.id = 'cl-css-ext'; link.rel = 'stylesheet'; link.href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; document.head.appendChild(link); } else { document.getElementById('cl-css-ext').href = L.resource('view/clashoo/clashoo.css') + '?v=20260806a1'; } // restore last-viewed tab so save/apply reloads stay put instead of jumping to kernel this._tab = readSavedTab('clashoo.system.tab', this._tab || 'kernel', ['kernel', 'rules', 'logs']); rememberTab('clashoo.system.tab', this._tab); var tabs = [ { id: 'kernel', label: _("Core and Data") }, { id: 'rules', label: _("Rules and Control") }, { id: 'logs', label: _("Log") } ]; var tabEls = {}, panelEls = {}; var tabBar = E('div', { 'class': 'cl-tabs' }, tabs.map(function (t) { var el = E('div', { 'class': 'cl-tab' + (self._tab === t.id ? ' active' : ''), click: function () { Object.keys(tabEls).forEach(function (k) { tabEls[k].className = 'cl-tab' + (k === t.id ? ' active' : ''); panelEls[k].className = 'cl-panel' + (k === t.id ? ' active' : ''); }); self._tab = t.id; rememberTab('clashoo.system.tab', t.id); } }, t.label); tabEls[t.id] = el; return el; }) ); var kernelPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'kernel' ? ' active' : ''), id: 'cl-panel-kernel' }); panelEls['kernel'] = kernelPanel; this._buildComponentUpdatePanel(kernelPanel, cpuArch); this._buildBackupPanel(kernelPanel); this._buildKernelPanel(kernelPanel, cpuArch); var rulesPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'rules' ? ' active' : ''), id: 'cl-panel-rules' }); panelEls['rules'] = rulesPanel; this._buildRulesForm(rulesPanel); var logsPanel = E('div', { 'class': 'cl-panel' + (this._tab === 'logs' ? ' active' : ''), id: 'cl-panel-logs' }, this._buildLogsPanel(runLog) ); panelEls['logs'] = logsPanel; this._tabEls = tabEls; this._panelEls = panelEls; poll.add(L.bind(this._pollLogs, this), 8); return E('div', { 'class': 'cl-wrap clashoo-container cl-system-page cl-form-page ' + getThemeClass() }, [tabBar, kernelPanel, rulesPanel, logsPanel]); }, _detectMihomoArch: function (raw) { if (!raw) return ''; if (raw === 'x86_64') return 'amd64-compatible'; if (/^aarch64/.test(raw)) return 'arm64'; if (/^armv7|^arm_cortex-a[7-9]|^arm_cortex-a1[0-9]/.test(raw)) return 'armv7'; if (/^armv6|^arm_cortex-a[56]/.test(raw)) return 'armv6'; if (/^arm/.test(raw)) return 'armv5'; if (/^i[3-6]86/.test(raw)) return '386'; if (/^mips64el/.test(raw)) return 'mips64le'; if (/^mips64/.test(raw)) return 'mips64'; if (/^mipsel/.test(raw)) return 'mipsle'; if (/^mips/.test(raw)) return 'mips'; return ''; }, _buildComponentUpdatePanel: function (container, cpuArch) { var self = this; this._compCpuArch = cpuArch || ''; this._compVariant = this._compVariant || {}; this._compLatest = this._compLatest || {}; var listEl = E('div', { 'class': 'cl-component-list' }); var advListEl = E('div', { 'class': 'cl-component-list' }); this._compAdvListEl = advListEl; var logEl = E('div', { 'class': 'cl-component-log', style: 'display:none' }, ''); this._compLogEl = logEl; var archWrap = E('div', { 'class': 'cl-component-arch' }); this._compArchWrap = archWrap; var refreshBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral', click: function (ev) { ev.preventDefault(); refreshBtn.disabled = true; refreshBtn.textContent = _("Checking…"); self._refreshComponentUpdatePanel(listEl, logEl, true); } }, _("Check Updates")); this._compRefreshBtn = refreshBtn; /* advanced: core/data, collapsed by default to keep page compact. CPU arch stays outside — it decides core binary arch */ var advWrap = E('div', { 'class': 'cl-component-adv cl-closed' }, [ E('div', { 'class': 'cl-component-adv-bar' }, [ E('span', {}, _("Advanced Settings (core / data components)")), E('span', { 'class': 'cl-component-adv-chevron' }, '›') ]), E('div', { 'class': 'cl-component-adv-body' }, [advListEl]) ]); advWrap.firstChild.addEventListener('click', function () { advWrap.classList.toggle('cl-closed'); }); /* wrap in cl-component-map, reuse LuCI .cbi-map container 浅色出现灰托盘、暗色满宽对齐,与下方表单卡片同构 */ container.appendChild(E('div', { 'class': 'cl-component-map' }, [ E('div', { 'class': 'cl-component-card' }, [ E('div', { 'class': 'cl-component-head' }, [ E('div', {}, [ E('h4', {}, _("Component Updates")), E('div', { 'class': 'cl-component-sub' }, _("Update components individually. Click Check Updates to refresh versions.")) ]), refreshBtn ]), archWrap, listEl, advWrap, logEl ]) ])); this._refreshComponentUpdatePanel(listEl, logEl, false); }, /* 备份与还原:折腾内核/配置前的兜底。导出当前配置为压缩包,避免 RPC 大包限制; 还原默认配置则把 UCI 设置重置为出厂值(订阅/配置文件保留)。仅含配置,不含内核。 */ _buildBackupPanel: function (container) { var exportBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, _("Export Backup")); exportBtn.addEventListener('click', function (ev) { ev.preventDefault(); exportBtn.disabled = true; var orig = exportBtn.textContent; exportBtn.textContent = _("Exporting…"); var done = function () { exportBtn.disabled = false; exportBtn.textContent = orig; }; var exportPath = ''; callBackupExportPrepare().then(function (r) { if (!r || !r.success) { throw new Error((r && r.message) || _("Unknown error")); } exportPath = r.path; return fs.read_direct(r.path, 'blob').then(function (blob) { return { blob: blob, filename: r.filename || 'clashoo-backup.tar.gz' }; }); }).then(function (download) { var url = URL.createObjectURL(download.blob); var a = E('a', { href: url, download: download.filename }); document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); clashoo.toast(_("Backup exported"), { kind: 'ok' }); }).catch(function (e) { clashoo.toast(_("Export failed: ") + (e.message || e), { kind: 'err' }); }).finally(function () { done(); if (exportPath) callBackupExportCleanup(exportPath).catch(function () {}); }); }); var importInput = E('input', { type: 'file', accept: '.tar.gz,.json', style: 'display:none' }); var importBtn = E('button', { 'class': 'btn cbi-button cbi-button-neutral' }, _("Import Restore")); importBtn.addEventListener('click', function (ev) { ev.preventDefault(); importInput.value = ''; importInput.click(); }); importInput.addEventListener('change', function () { var file = importInput.files && importInput.files[0]; if (!file) return; var lower = String(file.name || '').toLowerCase(); var maxSize = /\.json$/.test(lower) ? 8 * 1024 * 1024 : /\.tar\.gz$/.test(lower) ? 32 * 1024 * 1024 : 0; if (!maxSize) { clashoo.toast(_("Import failed: ") + _("Only .tar.gz and legacy .json backup files are supported"), { kind: 'err' }); importInput.value = ''; return; } if (file.size > maxSize) { clashoo.toast(_("Import failed: ") + _("Backup file is too large"), { kind: 'err' }); importInput.value = ''; return; } if (!confirm(_("Importing will overwrite all current configuration and settings with the backup. Existing subscriptions/configuration files will be replaced. Continue?"))) { importInput.value = ''; return; } importBtn.disabled = true; var uploadPath = ''; callBackupImportPrepare().then(function (r) { if (!r || !r.success) throw new Error((r && r.message) || _("Unknown error")); uploadPath = r.path; return uploadBackupChunks(file, uploadPath); }).then(function () { return callBackupImportFile(file.name, uploadPath); }).then(function (r) { if (r && r.success) clashoo.toast(r.message || _("Backup restored, restarting service"), { kind: 'ok' }); else throw new Error((r && r.message) || _("Unknown error")); }).catch(function (err) { clashoo.toast(_("Import failed: ") + (err.message || err), { kind: 'err' }); }).finally(function () { if (uploadPath) callBackupImportCleanup(uploadPath).catch(function () {}); importBtn.disabled = false; importInput.value = ''; }); }); var resetBtn = E('button', { 'class': 'btn cbi-button cbi-button-negative' }, _("Restore Defaults")); resetBtn.addEventListener('click', function (ev) { ev.preventDefault(); if (!confirm(_("Restore Clashoo settings to factory defaults (subscriptions and configuration files are kept). Continue?"))) return; resetBtn.disabled = true; var orig = resetBtn.textContent; resetBtn.textContent = _("Restoring…"); var done = function () { resetBtn.disabled = false; resetBtn.textContent = orig; }; callBackupReset().then(function (r) { done(); if (r && r.success) clashoo.toast(r.message || _("Factory defaults restored"), { kind: 'ok' }); else clashoo.toast(_("Restore failed: ") + ((r && r.message) || _("Unknown error")), { kind: 'err' }); }).catch(function (e) { done(); clashoo.toast(_("Restore failed: ") + (e.message || e), { kind: 'err' }); }); }); container.appendChild(E('div', { 'class': 'cl-component-map' }, [ E('div', { 'class': 'cl-component-card' }, [ E('div', { 'class': 'cl-component-head' }, [ E('div', {}, [ E('h4', {}, _("Backup and Restore")), E('div', { 'class': 'cl-component-sub' }, [ _("Export a backup before changing cores or configuration, so it can be restored if needed. Includes configuration and settings only, not cores."), E('br'), E('strong', {}, _("Backup files contain subscription and node credentials. Store them securely.")) ]) ]) ]), importInput, E('div', { 'class': 'cl-backup-actions' }, [exportBtn, importBtn, resetBtn]) ]) ])); }, _replaceChildren: function (node, children) { while (node.firstChild) node.removeChild(node.firstChild); children.forEach(function (child) { if (child) node.appendChild(child); }); }, /* latest map key: mihomo/sing-box with variant; smart fixed; pkg by id */ _compLatestKey: function (comp, variant) { if (comp.id === 'smart') return 'smart'; if (comp.variant) return comp.id + '_' + (variant || 'stable'); return comp.id; }, _compNorm: function (s) { return String(s || '').toLowerCase().replace(/^v/, '').replace(/~/g, '.'); }, _compVariantOf: function (comp) { if (!comp.variant) return ''; if (this._compVariant[comp.id]) return this._compVariant[comp.id]; var inst = String(comp.installed_version || '').toLowerCase(); return /(?:alpha|beta|rc|pre)/.test(inst) ? 'alpha' : 'stable'; }, _compInstalledVersion: function (comp, variant) { if (comp && comp.installed_versions && comp.installed_versions[variant]) return comp.installed_versions[variant]; if (comp && comp.variant) return ''; return (comp && comp.installed_version) || ''; }, /* install != latest = updatable (alpha compared by hash)*/ _compUpdatable: function (comp, latestMap) { if (!comp) return false; /* lgbm has no version; compare local vs remote model sha256 (kind=data skips generic path) */ if (comp.id === 'lgbm') { var rem = latestMap.lgbm, ins = comp.installed_version; return !!(rem && ins && this._compNorm(rem) !== this._compNorm(ins)); } if (comp.kind === 'data') return false; var variant = this._compVariantOf(comp); var inst = this._compInstalledVersion(comp, variant); if (!inst || inst === _("Not installed") || inst === _("Unknown")) return false; var latest = latestMap[this._compLatestKey(comp, variant)]; if (!latest) return false; return this._compNorm(latest) !== this._compNorm(inst); }, _componentStatusText: function (comp, globalRunning) { if (comp.status === 'running') return comp.message || _("Updating"); if (comp.status === 'success') return comp.message || _("Completed"); if (comp.status === 'failed') return comp.message || _("Failed"); if (globalRunning) return _("Wait for the current task to complete"); return ''; }, /* CPU arch: auto-detected + manual override (download_core UCI)*/ _renderComponentArchRow: function (arch) { var self = this; arch = arch || {}; var sys = arch.system || _("Unknown"); var detected = this._detectMihomoArch(this._compCpuArch || sys) || ''; var cur = arch.download_core || detected || ''; var archList = ['amd64-compatible', 'amd64-v1', 'amd64-v2', 'amd64-v3', 'arm64', 'armv7', 'armv6', 'armv5', '386', 'mips', 'mipsle', 'mips64', 'mips64le']; var opts = archList.map(function (a) { return E('option', { value: a, selected: a === cur ? '' : null }, a); }); if (!cur) opts.unshift(E('option', { value: '', selected: '' }, _("Not detected, please select"))); var sel = E('select', { 'class': 'cl-component-arch-sel' }, opts); sel.addEventListener('change', function () { uci.set('clashoo', 'config', 'download_core', sel.value); uci.save() .then(function () { return clashoo.commitConfig(); }) .then(function () { return clearClashooDirty(); }) .then(function () { clashoo.toast(_("CPU architecture set to ") + sel.value, { kind: 'info' }); }) .catch(function () {}); /* re-render row: show hint when mismatched */ if (self._compArchWrap) self._replaceChildren(self._compArchWrap, [self._renderComponentArchRow({ system: sys, download_core: sel.value })]); }); var row = [ E('span', { 'class': 'cl-component-arch-label' }, _("CPU Architecture")), E('span', { 'class': 'cl-component-arch-auto' }, _("Auto-detected")), sel ]; /* no hint when auto-detected matches; only show when manual choice differs */ if (detected && cur !== detected) row.push(E('span', { 'class': 'cl-component-arch-hint' }, _("Recommended ") + detected + _(" (auto-detected by device)"))); else if (!detected && !cur) row.push(E('span', { 'class': 'cl-component-arch-hint' }, _("Could not detect the device architecture; select it manually before updating the core."))); return E('div', { 'class': 'cl-component-arch-row' }, row); }, _renderComponentRow: function (comp, globalRunning, latestMap, listEl, logEl) { var self = this; var statusText = this._componentStatusText(comp, globalRunning); var inst = comp.installed_version || ''; var instStable = !/(?:alpha|beta|rc|pre)/i.test(inst); /* mihomo / sing-box:行内稳定 / Alpha 切换 */ var variant = ''; var variantBox = null; if (comp.variant) { if (!self._compVariant[comp.id]) self._compVariant[comp.id] = instStable ? 'stable' : 'alpha'; variant = self._compVariant[comp.id]; var mkV = function (v, label) { var active = variant === v; var b = E('button', { 'class': 'cl-comp-var' + (active ? ' on' : '') }, (active ? '✓ ' : '') + label); b.addEventListener('click', function (ev) { ev.preventDefault(); self._compVariant[comp.id] = v; self._refreshComponentUpdatePanel(listEl, logEl, false); }); return b; }; variantBox = E('div', { 'class': 'cl-comp-var-box' }, [mkV('stable', _("Stable")), mkV('alpha', 'Alpha')]); inst = this._compInstalledVersion(comp, variant) || _("Not installed"); } var latest = latestMap[this._compLatestKey(comp, variant || this._compVariantOf(comp))]; if (!statusText && latest) statusText = _("Remote: %s").replace('%s', latest); /* 绿色「可更新」徽标:最新版与已装不一致时显示 */ var badge = this._compUpdatable(comp, latestMap) ? E('span', { 'class': 'cl-comp-updatable' }, _("Update available")) : null; var btn = E('button', { 'class': 'btn cbi-button cbi-button-apply', disabled: globalRunning ? 'disabled' : null, click: function (ev) { ev.preventDefault(); btn.disabled = true; btn.textContent = _("Submitting"); if (self._compLogEl) self._compLogEl.style.display = ''; clashoo.componentUpdate(comp.id, variant).then(function (res) { if (res && res.success) clashoo.toast(_("Component update task submitted"), { kind: 'info' }); else clashoo.toast((res && res.message) || _("Failed to submit component update task"), { kind: 'error' }); setTimeout(function () { self._refreshComponentUpdatePanel(listEl, logEl, false); }, 700); }); } }, comp.status === 'running' ? _("Updating") : _("Update")); var verChildren = [E('span', {}, _("Current: %s").replace('%s', inst || _("Unknown")) + ' ')]; if (badge) verChildren.push(badge); var verBlock = [E('div', {}, verChildren)]; if (variantBox) verBlock.push(variantBox); return E('div', { 'class': 'cl-component-row' }, [ E('div', {}, [ E('div', { 'class': 'cl-component-name' }, comp.name || comp.id), E('div', { 'class': 'cl-component-desc' }, comp.description || '') ]), E('div', { 'class': 'cl-component-version' }, verBlock), E('div', { 'class': 'cl-component-status cl-st-' + (comp.status || 'idle') }, statusText), btn ]); }, _refreshComponentUpdatePanel: function (listEl, logEl, doCheck) { var self = this; if (this._componentPollTimer) { clearTimeout(this._componentPollTimer); this._componentPollTimer = null; } clashoo.componentStatus().then(function (data) { var comps = data.components || []; if (self._compArchWrap) self._replaceChildren(self._compArchWrap, [self._renderComponentArchRow(data.arch)]); var paint = function () { var mainComps = [], advComps = []; comps.forEach(function (comp) { (COMPONENT_MAIN_IDS[comp.id] ? mainComps : advComps).push(comp); }); var render = function (comp) { return self._renderComponentRow(comp, !!data.running, self._compLatest || {}, listEl, logEl); }; self._replaceChildren(listEl, mainComps.map(render)); if (self._compAdvListEl) self._replaceChildren(self._compAdvListEl, advComps.map(render)); }; paint(); logEl.textContent = data.log || data.last_log || _("No component update logs yet"); if (data.running) { /* 有任务运行:展开日志,取消收起计时 */ if (self._compLogHideTimer) { clearTimeout(self._compLogHideTimer); self._compLogHideTimer = null; } logEl.style.display = ''; self._componentPollTimer = setTimeout(function () { self._refreshComponentUpdatePanel(listEl, logEl, false); }, 2000); } else { /* 任务结束:日志若展开着,8 秒后自动收起 */ if (logEl.style.display !== 'none' && !self._compLogHideTimer) { self._compLogHideTimer = setTimeout(function () { logEl.style.display = 'none'; self._compLogHideTimer = null; }, 8000); } if (doCheck) { clashoo.componentCheckUpdates().then(function (r) { self._compLatest = (r && r.latest) || {}; paint(); var n = comps.reduce(function (acc, c) { return acc + (self._compUpdatable(c, self._compLatest) ? 1 : 0); }, 0); if (self._compRefreshBtn) { self._compRefreshBtn.disabled = false; self._compRefreshBtn.textContent = _("Check Updates"); } clashoo.toast(n > 0 ? _("%d updates available").replace('%d', String(n)) : _("All components are up to date"), { kind: n > 0 ? 'info' : 'success' }); }).catch(function () { if (self._compRefreshBtn) { self._compRefreshBtn.disabled = false; self._compRefreshBtn.textContent = _("Check Updates"); } clashoo.toast(_("Failed to check updates, please check the network"), { kind: 'error' }); }); } } }); }, _buildKernelPanel: function (container, cpuArch) { var self = this; var detectedArch = this._detectMihomoArch(cpuArch); var m = new form.Map('clashoo', '', ''); var s, o; var originalCoreChoice = coreChoiceFromConfig( uci.get('clashoo', 'config', 'core_type') || 'mihomo', uci.get('clashoo', 'config', 'dcore') || '2' ); var selectedCoreChoice = originalCoreChoice; s = m.section(form.NamedSection, 'config', 'clashoo', _("Backend Core")); s.addremove = false; o = s.option(form.ListValue, '_core_choice', _("Core Type")); o.value('mihomo-stable', _("mihomo Stable")); o.value('mihomo-alpha', 'mihomo Alpha'); o.value('smart', 'Smart'); o.value('singbox-stable', _("sing-box Stable")); o.value('singbox-alpha', 'sing-box Alpha'); o.cfgvalue = function () { return originalCoreChoice; }; o.write = function (sectionId, value) { selectedCoreChoice = value; }; o.remove = function () {}; o.description = ''; var coreChoiceOption = o; s = m.section(form.NamedSection, 'config', 'clashoo', _("Dashboard Settings")); s.addremove = false; o = s.option(form.Value, 'dash_port', _("Dashboard Port")); o.placeholder = '9090'; o = s.option(form.Value, 'dash_pass', _("Access Secret")); o.placeholder = 'clashoo'; o = s.option(form.ListValue, 'dashboard_panel', _("Panel UI")); ['metacubexd','yacd','zashboard','razord'].forEach(function(p){ o.value(p,p); }); s = m.section(form.NamedSection, 'config', 'clashoo', _("Core Download")); s.addremove = false; o = s.option(form.Value, 'core_mirror_prefix', _("GitHub acceleration prefix")); o.placeholder = 'https://gh-proxy.com/'; o.description = _("When downloading core/data components, this prefix will be used first (a slash is required at the end). Leave blank to use the built-in gh-proxy.com and ghfast.top alternative sources."); o.rmempty = true; m.render().then(function (node) { decorateSystemForm(node); enhanceDashPasswordField(node); container.appendChild(node); var saveKernelForm = function (applyNow) { var choice = coreChoiceOption.formvalue('config') || selectedCoreChoice || originalCoreChoice; var target = coreChoiceTarget(choice); var changed = choice !== originalCoreChoice; return m.save() .then(function () { return clashoo.commitConfig(); }) .then(function () { if (applyNow) return clashoo.setCore(target.core, target.dcore, 'apply'); if (changed) return clashoo.setCore(target.core, target.dcore, 'save'); return { success: true }; }) .then(function (res) { if (!res || res.success === false) throw new Error((res && res.message) || _("Failed to save core configuration")); if (applyNow && res.restarting) return waitForCoreTarget(target).then(function () { return res; }); return res; }) .then(function (res) { return clearClashooDirty().then(function () { return res; }); }) .then(function (res) { var msg; if (!applyNow) msg = changed ? _("Core type saved, takes effect on next start or restart") : _("Configuration saved"); else if (res.restarting) msg = _("Configuration saved, switched and started selected core"); else msg = _("Configuration saved, service remains stopped"); ui.addNotification(null, E('p', msg)); window.setTimeout(function () { location.reload(); }, 300); }); }; container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ E('button', { 'class': 'cbi-button', click: function () { saveKernelForm(false) .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); }}, _("Save Configuration")), E('button', { 'class': 'btn cbi-button-action', click: function () { saveKernelForm(true) .catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); }}, _("Apply Configuration")) ])); }); }, _buildRulesForm: function (container) { var m = new form.Map('clashoo', '', ''); var s, o; s = m.section(form.NamedSection, 'config', 'clashoo', _("Bypass Rules")); s.addremove = false; o = s.option(form.Flag, 'bypass_china', _("Bypass China IPv4")); o.default = '0'; o.rmempty = false; o = s.option(form.Flag, 'bypass_china_ipv6', _("Bypass China IPv6")); o.default = '0'; o.rmempty = false; o.cfgvalue = function (section_id) { var value = uci.get('clashoo', section_id, 'bypass_china_ipv6'); return (value != null) ? value : uci.get('clashoo', section_id, 'bypass_china'); }; o = s.option(form.ListValue, 'bypass_port_mode', _("Bypass Ports")); o.value('all', _("All Ports")); o.value('common', _("Common Ports")); o.value('custom', _("Custom")); o.default = 'all'; o = s.option(form.Value, 'bypass_port_custom', _("Custom Ports")); o.depends('bypass_port_mode', 'custom'); o.placeholder = '22,53,80,443,8080,8443'; o.datatype = 'string'; o.rmempty = true; o = s.option(form.Flag, 'sniffer_streaming', _("Sniffer (streaming compatibility)")); o.default = '1'; o.rmempty = false; o.description = _("When enabled, sniffer configuration is injected automatically to improve streaming domain detection and routing stability."); s = m.section(form.TableSection, 'lan_acl', _("Access Control")); s.addremove = true; s.anonymous = true; s.sortable = true; s.description = _("Rules are matched from top to bottom. Unmatched devices use DNS takeover and proxy by default. A rule with IPv4, IPv6 and MAC all empty matches every device."); var hints = this._hostHints || {}; var hostOptions = [], host6Options = [], macOptions = []; var seen = {}, seen6 = {}; Object.keys(hints).forEach(function (mac) { var h = hints[mac] || {}; var macU = mac.toUpperCase(); var addrs = h.ipaddrs || (h.ipv4 ? [h.ipv4] : []); var addrs6 = h.ip6addrs || []; var label = h.name || addrs[0] || addrs6[0] || ''; addrs.forEach(function (ip) { if (ip && !seen[ip]) { seen[ip] = true; hostOptions.push([ip, ip + ' (' + macU + ')']); } }); addrs6.forEach(function (ip) { if (ip && !seen6[ip]) { seen6[ip] = true; host6Options.push([ip, ip + ' (' + macU + ')']); } }); macOptions.push([macU, label ? macU + ' (' + label + ')' : macU]); }); o = s.option(form.Flag, 'enabled', _("Enable")); o.default = '1'; o.rmempty = false; o = s.option(form.DynamicList, 'ip', _("IPv4")); o.placeholder = '192.168.1.100'; o.datatype = 'ip4addr'; hostOptions.forEach(function (kv) { o.value(kv[0], kv[1]); }); o = s.option(form.DynamicList, 'ip6', _("IPv6")); o.placeholder = 'fd00::100'; o.datatype = 'ip6addr'; host6Options.forEach(function (kv) { o.value(kv[0], kv[1]); }); o = s.option(form.DynamicList, 'mac', _("MAC")); o.placeholder = '00:11:22:33:44:55'; o.datatype = 'macaddr'; macOptions.forEach(function (kv) { o.value(kv[0], kv[1]); }); o = s.option(form.Flag, 'dns', _("DNS Takeover")); o.default = '1'; o.rmempty = false; o = s.option(form.Flag, 'proxy', _("Proxy")); o.default = '1'; o.rmempty = false; s = m.section(form.NamedSection, 'config', 'clashoo', _("Automation Tasks")); s.addremove = false; o = s.option(form.Flag, 'auto_update', _("Regularly update rule data")); o.description = _("Rule data includes China allowlist (direct IP ranges) and GeoIP / GeoSite databases"); o = s.option(form.Value, 'auto_update_time', _("Update interval (hours)")); o = s.option(form.Flag, 'auto_clear_log', _("Scheduled Log Cleanup")); o = s.option(form.Value, 'clear_time',_("Cleanup interval (hours)")); o = s.option(form.ListValue, 'geoip_source', _("GeoIP Source")); o.value('3', 'Loyalsoldier'); o.value('5', 'v2fly'); o.value('4', _("Custom")); o.default = '3'; o = s.option(form.Value, 'geoip_mmdb_url', 'Country.mmdb URL'); o.depends('geoip_source', '4'); o.placeholder = 'https://…/Country.mmdb'; o = s.option(form.Value, 'geosite_url', 'geosite.dat URL'); o.depends('geoip_source', '4'); o.placeholder = 'https://…/geosite.dat'; o = s.option(form.Value, 'geoip_dat_url', 'geoip.dat URL'); o.depends('geoip_source', '4'); o.placeholder = 'https://…/geoip.dat'; m.render().then(function (node) { normalizeLanAclLayout(node); decorateSystemForm(node); container.appendChild(node); watchLanAclLayout(node); container.appendChild(E('div', { 'class': 'cl-save-bar' }, [ E('button', { 'class': 'cbi-button', click: function () { m.save().then(function () { return clashoo.commitConfig(); }) .then(function () { return clearClashooDirty(); }) .then(function () { location.reload(); }) .catch(function (e) { ui.addNotification(null, E('p', _("Save failed: ") + (e.message || e))); }); }}, _("Save Configuration")), E('button', { 'class': 'btn cbi-button-action', click: function () { saveCommitApplyMaybeReload(m, _("Configuration saved and service hot-reloaded"), _("Configuration saved, service is not running")) .catch(function (e) { ui.addNotification(null, E('p', _("Operation failed: ") + (e.message || e))); }); }}, _("Apply Configuration")) ])); }); }, _buildLogsPanel: function (runLog) { var self = this; var logTypes = [ { id: 'plugin', label: _("Plugin Log"), read: clashoo.readLog.bind(clashoo), clear: clashoo.clearLog.bind(clashoo) }, { id: 'core', label: _("Core Log"), read: clashoo.readCoreLog.bind(clashoo), clear: clashoo.clearCoreLog.bind(clashoo) }, { id: 'update', label: _("Update Log"), read: clashoo.readUpdateMergedLog.bind(clashoo), clear: clashoo.clearUpdateMergedLog.bind(clashoo) } ]; var MAX_LINES = 5000; var state = { autoScroll: true, paused: false, filter: '' }; function esc(s) { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /* Expose helpers for _pollLogs */ self._buildLogLine = buildLine; self._applyLogFilter = applyFilter; /* parse level token; no keyword (e.g. mihomo info lines with [info] stripped) => info */ function detectLevelToken(ln) { var m = ln.match(/\b(DEBUG|INFO|WARN(?:ING)?|ERROR|FATAL|PANIC)\b/i); if (!m) return 'info'; var l = m[1].toUpperCase(); if (l === 'DEBUG') return 'debug'; if (l === 'INFO') return 'info'; if (l.startsWith('WARN')) return 'warn'; if (l === 'FATAL' || l === 'PANIC') return 'fatal'; return 'error'; } function levelClass(token) { if (token === 'debug') return 'cl-log-debug'; if (token === 'warn') return 'cl-log-warn'; if (token === 'error' || token === 'fatal') return 'cl-log-error'; return 'cl-log-info'; } var MONTHS = { Jan:'01',Feb:'02',Mar:'03',Apr:'04',May:'05',Jun:'06',Jul:'07',Aug:'08',Sep:'09',Oct:'10',Nov:'11',Dec:'12' }; /* render a single line as colored HTML */ function buildLine(ln) { var token = detectLevelToken(ln); var cls = levelClass(token); var ts = '', msg = ln; /* core syslog: Mon DD HH:MM:SS YYYY → MM-DD HH:MM:SS */ var cm = ln.match(/^(\w{3})\s+(\d{1,2})\s+(\d{2}:\d{2}:\d{2})\s+\d{4}\s+\S+\.(\S+)\s+\S+\[\d+\]:\s+(.*)$/); if (cm) { ts = (MONTHS[cm[1]] || cm[1]) + '-' + cm[2].padStart(2,'0') + ' ' + cm[3]; msg = cm[5] || ln; } /* plugin / update: MM-DD HH:MM:SS [warn] message */ else if (/^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\s/.test(ln)) { ts = ln.substring(0,14); msg = ln.substring(15); /* update: MM-DD HH:MM:SS - message → strip dash */ if (msg.indexOf(' - ') === 0) msg = msg.substring(3); } if (ts) { return '
' + esc(ts) + '' + esc(msg) + '
'; } return '
' + esc(ln) + '
'; } function renderLines(text) { var lines = text.split('\n').filter(function(l) { return l; }); var html = ''; for (var i = 0; i < lines.length; i++) { html += buildLine(lines[i]); } return html; } function applyFilter() { var f = state.filter; /* '' = all, else exact level: info/warn/error/fatal */ var els = document.querySelectorAll('#cl-log-area .cl-log-line'); for (var i = 0; i < els.length; i++) { if (!f || els[i].getAttribute('data-level') === f) els[i].classList.remove('cl-log-hidden'); else els[i].classList.add('cl-log-hidden'); } } var logTabEls = {}; var logArea = E('div', { 'class': 'cl-log-area', id: 'cl-log-area' }); logArea.innerHTML = runLog ? renderLines(runLog) : _("(empty)"); var clearBtn = null; function activateLogTab(id) { var logType = logTypes.find(function (lt) { return lt.id === id; }) || logTypes[0]; Object.keys(logTabEls).forEach(function (k) { logTabEls[k].className = 'cl-log-tab' + (k === logType.id ? ' active' : ''); }); self._logTab = logType.id; syncClearButton(); return logType.read().then(function (content) { logArea.innerHTML = (content && content.trim()) ? renderLines(content) : _("(empty)"); applyFilter(); if (state.autoScroll) logArea.scrollTop = logArea.scrollHeight; }); } this._activateLogTab = activateLogTab; var logTabBar = E('div', { 'class': 'cl-log-tabs' }, logTypes.map(function (lt) { var el = E('span', { 'class': 'cl-log-tab' + (self._logTab === lt.id ? ' active' : ''), click: function () { activateLogTab(lt.id); } }, lt.label); logTabEls[lt.id] = el; return el; }) ); var currentType = function () { return logTypes.find(function (lt) { return lt.id === self._logTab; }) || logTypes[0]; }; function syncClearButton() { if (!clearBtn) return; var ct = currentType(); clearBtn.disabled = !ct.clear; } /* ---- toolbar ---- */ var cbAuto = E('input', { type: 'checkbox', checked: 'checked' }); cbAuto.addEventListener('change', function () { state.autoScroll = cbAuto.checked; }); var cbPause = E('input', { type: 'checkbox' }); cbPause.addEventListener('change', function () { state.paused = cbPause.checked; }); var selFilter = E('select', { 'class': 'cbi-button' }, [ E('option', { value: '' }, _("All")), E('option', { value: 'info' }, 'INFO'), E('option', { value: 'warn' }, 'WARN'), E('option', { value: 'error' }, 'ERROR'), E('option', { value: 'fatal' }, 'FATAL') ]); selFilter.addEventListener('change', function () { state.filter = selFilter.value; applyFilter(); }); var btnClearView = E('button', { 'class': 'cbi-button' }, _("Clear View")); btnClearView.addEventListener('click', function () { logArea.innerHTML = ''; }); var btnDownload = E('button', { 'class': 'cbi-button' }, _("Download")); btnDownload.addEventListener('click', function () { var blob = new Blob([logArea.textContent || ''], { type: 'text/plain' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'clashoo-' + new Date().toISOString().replace(/[:.]/g, '-') + '.log'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }); clearBtn = E('button', { 'class': 'cbi-button' }, _("Clear File")); clearBtn.addEventListener('click', function () { var ct = currentType(); if (!ct.clear) return; if (!confirm(_("Clear the current log file? This cannot be undone."))) return; ct.clear().then(function () { logArea.innerHTML = ''; }); }); syncClearButton(); var toolbar = E('div', { 'class': 'cl-log-toolbar' }, [ E('label', {}, [cbAuto, _("Auto Scroll")]), E('label', {}, [cbPause, _("Pause")]), selFilter, btnClearView, btnDownload, clearBtn ]); return E('div', { 'class': 'cl-section cl-card cl-log-card' }, [ E('h4', {}, _("Log")), logTabBar, toolbar, logArea ]); }, _pollLogs: function () { if (this._tab !== 'logs') return Promise.resolve(); var self = this; var el = document.getElementById('cl-log-area'); if (!el) return Promise.resolve(); var cbPause = el.parentNode.querySelector('input[type="checkbox"]:nth-of-type(2)'); if (cbPause && cbPause.checked) return Promise.resolve(); var logFns = { plugin: clashoo.readLog.bind(clashoo), core: clashoo.readCoreLog.bind(clashoo), update: clashoo.readUpdateMergedLog.bind(clashoo) }; var readFn = logFns[this._logTab] || logFns.plugin; return readFn().then(function (content) { if (!content || !content.trim()) { el.innerHTML = _("(empty)"); return; } var html = ''; var lines = content.split('\n').filter(function(l) { return l; }); for (var i = 0; i < lines.length; i++) { html += self._buildLogLine(lines[i]); } el.innerHTML = html; self._applyLogFilter(); var cbAuto = el.parentNode.querySelector('input[type="checkbox"]:first-of-type'); if (!cbAuto || cbAuto.checked) el.scrollTop = el.scrollHeight; }); }, _switchTab: function (id) { var tabEls = this._tabEls || {}; var panelEls = this._panelEls || {}; Object.keys(tabEls).forEach(function (k) { tabEls[k].className = 'cl-tab' + (k === id ? ' active' : ''); panelEls[k].className = 'cl-panel' + (k === id ? ' active' : ''); }); this._tab = id; rememberTab('clashoo.system.tab', id); }, handleSaveApply: null, handleSave: null, handleReset: null });