diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile index ae52b719..70eae4a4 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns -PKG_VERSION:=1.7.12 -PKG_RELEASE:=20 +PKG_VERSION:=1.7.13 +PKG_RELEASE:=21 LUCI_TITLE:=LuCI Support for mosdns LUCI_PKGARCH:=all diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js index aff5637d..6595a1c1 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js @@ -6,214 +6,100 @@ return view.extend({ render() { - let m, s, o; - - m = new form.Map('mosdns', _('Rule Settings'), + const m = new form.Map('mosdns', _('Rule Settings'), _('The list of rules only apply to \'Default Config\' profiles.')); - s = m.section(form.TypedSection); + const s = m.section(form.TypedSection); s.anonymous = true; s.sortable = true; - s.tab('whitelist', _('White Lists')); - s.tab('blocklist', _('Block Lists')); - s.tab('greylist', _('Grey Lists')); - s.tab('ddnslist', _('DDNS Lists')); - s.tab('hostslist', _('Hosts')); - s.tab('redirectlist', _('Redirect')); - s.tab('localptrlist', _('Block PTR')); - s.tab('streamingmedialist', _('Streaming Media')); - - // --- White Lists --- - o = s.taboption('whitelist', form.TextValue, '_whitelist', - null, - '' - + _('Added domain names always permit resolution using \'local DNS\' with the highest priority (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/whitelist.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/whitelist.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/whitelist.txt', '').catch(e => { + const handleSaveError = e => { ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - - // --- Block Lists --- - o = s.taboption('blocklist', form.TextValue, '_blocklist', - null, - '' - + _('Added domain names will block DNS resolution (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/blocklist.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/blocklist.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); }; - o.remove = section_id => fs.write('/etc/mosdns/rule/blocklist.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - // --- Grey Lists --- - o = s.taboption('greylist', form.TextValue, '_greylist', - null, - '' - + _('Added domain names will always use \'Remote DNS\' for resolution (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/greylist.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/greylist.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/greylist.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); + const rules = [ + { + name: 'whitelist', + title: _('White Lists'), + file: '/etc/mosdns/rule/whitelist.txt', + desc: '' + + _('Added domain names always permit resolution using \'local DNS\' with the highest priority (one domain per line, supports domain matching rules).') + + '' + }, + { + name: 'blocklist', + title: _('Block Lists'), + file: '/etc/mosdns/rule/blocklist.txt', + desc: '' + + _('Added domain names will block DNS resolution (one domain per line, supports domain matching rules).') + + '' + }, + { + name: 'greylist', + title: _('Grey Lists'), + file: '/etc/mosdns/rule/greylist.txt', + desc: '' + + _('Added domain names will always use \'Remote DNS\' for resolution (one domain per line, supports domain matching rules).') + + '' + }, + { + name: 'ddnslist', + title: _('DDNS Lists'), + file: '/etc/mosdns/rule/ddnslist.txt', + desc: '' + + _('Added domain names will always use \'Local DNS\' for resolution, with a forced TTL of 5 seconds, and results will not be cached (one domain per line, supports domain matching rules).') + + '' + }, + { + name: 'hostslist', + title: _('Hosts'), + file: '/etc/mosdns/rule/hosts.txt', + desc: '' + + _('Custom Hosts rewrite, for example: baidu.com 10.0.0.1 (one rule per line, supports domain matching rules).') + + '' + }, + { + name: 'redirectlist', + title: _('Redirect'), + file: '/etc/mosdns/rule/redirect.txt', + desc: '' + + _('Redirecting requests for domain names. Request domain A, but return records for domain B, for example: baidu.com qq.com (one rule per line).') + + '' + }, + { + name: 'localptrlist', + title: _('Block PTR'), + file: '/etc/mosdns/rule/local-ptr.txt', + desc: '' + + _('Added domain names will block PTR requests (one domain per line, supports domain matching rules).') + + '' + }, + { + name: 'streamingmedialist', + title: _('Streaming Media'), + file: '/etc/mosdns/rule/streaming.txt', + desc: '' + + _('When enabling \'Custom Stream Media DNS\', added domains will always use the \'Streaming Media DNS server\' for resolution (one domain per line, supports domain matching rules).') + + '' + } + ]; - // --- DDNS Lists --- - o = s.taboption('ddnslist', form.TextValue, '_ddnslist', - null, - '' - + _('Added domain names will always use \'Local DNS\' for resolution, with a forced TTL of 5 seconds, and results will not be cached (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/ddnslist.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/ddnslist.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/ddnslist.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); + rules.forEach(rule => { + s.tab(rule.name, rule.title); - // --- Hosts --- - o = s.taboption('hostslist', form.TextValue, '_hostslist', - null, - '' - + _('Custom Hosts rewrite, for example: baidu.com 10.0.0.1 (one rule per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/hosts.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/hosts.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/hosts.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - - // --- Redirect --- - o = s.taboption('redirectlist', form.TextValue, '_redirectlist', - null, - '' - + _('Redirecting requests for domain names. Request domain A, but return records for domain B, for example: baidu.com qq.com (one rule per line).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/redirect.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/redirect.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/redirect.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - - // --- Block PTR --- - o = s.taboption('localptrlist', form.TextValue, '_localptrlist', - null, - '' - + _('Added domain names will block PTR requests (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/local-ptr.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/local-ptr.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/local-ptr.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - - // --- Streaming Media --- - o = s.taboption('streamingmedialist', form.TextValue, '_streamingmedialist', - null, - '' - + _('When enabling \'Custom Stream Media DNS\', added domains will always use the \'Streaming Media DNS server\' for resolution (one domain per line, supports domain matching rules).') - + '' - ); - o.rows = 25; - o.cfgvalue = section_id => fs.trimmed('/etc/mosdns/rule/streaming.txt').catch(e => ''); - o.write = function(section_id, formvalue) { - return this.cfgvalue(section_id).then(value => { - if (value === formvalue) { - return; - } - return fs.write('/etc/mosdns/rule/streaming.txt', (formvalue.trim() ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : '')) - .catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); - }); - }); - }; - o.remove = section_id => fs.write('/etc/mosdns/rule/streaming.txt', '').catch(e => { - ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e.message))); + const o = s.taboption(rule.name, form.TextValue, '_' + rule.name, null, rule.desc); + o.rows = 25; + o.cfgvalue = () => fs.trimmed(rule.file).catch(() => ''); + o.write = function(section_id, formvalue) { + return this.cfgvalue(section_id).then(value => { + if (value === formvalue) { + return; + } + const content = (formvalue && formvalue.trim()) ? formvalue.trim().replace(/\r\n/g, '\n') + '\n' : ''; + return fs.write(rule.file, content).catch(handleSaveError); + }); + }; + o.remove = () => fs.write(rule.file, '').catch(handleSaveError); }); return m.render(); diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js index c08bbbda..af115c9f 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js @@ -1,5 +1,6 @@ 'use strict'; 'require dom'; +'require fs'; 'require poll'; 'require rpc'; 'require ui'; @@ -38,10 +39,18 @@ const callClearQueryLogs = rpc.declare({ expect: { '': {} } }); +const callGetLeases = rpc.declare({ + object: 'luci.mosdns', + method: 'get_leases', + expect: { '': {} } +}); + let filterVal = 'all'; let searchVal = ''; let pageIdx = 0; const PAGE_SIZE = 20; +const WHITELIST_FILE = '/etc/mosdns/rule/whitelist.txt'; +const BLOCKLIST_FILE = '/etc/mosdns/rule/blocklist.txt'; let isUserPaused = false; let nodeStats; @@ -52,12 +61,35 @@ let statsElements = null; let currentBadgeState = null; let lastTopJson = ''; let lastLogsJson = ''; +let clientLeases = {}; const cleanIP = ip => { if (!ip) return '-'; return ip.replace(/^::ffff:/i, ''); }; +const getClientDisplay = ip => { + const cleaned = cleanIP(ip); + if (!cleaned || cleaned === '-') return { display: '-', title: '-', host: null, ip: '-' }; + + const host = clientLeases[cleaned] || clientLeases[cleaned.toLowerCase()] || clientLeases[ip] || clientLeases[ip?.toLowerCase?.()]; + if (host && host !== '*' && host !== 'unknown' && host !== '') { + return { + display: `${host} (${cleaned})`, + title: `${host} (${cleaned})`, + host: host, + ip: cleaned + }; + } + + return { + display: cleaned, + title: cleaned, + host: null, + ip: cleaned + }; +}; + const injectStyles = () => { if (document.getElementById('mosdns-statistics-styles')) return; @@ -94,25 +126,55 @@ const injectStyles = () => { '.dns-latency-timeout { color: #dc2626; font-weight: 600; }', '.mosdns-table td { vertical-align: middle !important; padding: 0.45rem 0.6rem !important; }', '.mosdns-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }', + '.mosdns-log-row { cursor: pointer; transition: background 0.15s ease; }', + '.mosdns-log-row:hover { background: rgba(125,125,125,0.05); }', + '@media (max-width: 992px) {', + ' .mosdns-table { display: block !important; width: 100% !important; border: none !important; }', + ' .mosdns-table tbody { display: block !important; width: 100% !important; }', + ' .mosdns-table .table-titles, .mosdns-table .col-client, .mosdns-table .col-answers { display: none !important; }', + ' .mosdns-table .mosdns-log-row { display: grid !important; grid-template-columns: 1fr auto !important; grid-template-rows: auto auto !important; grid-template-areas: "domain status" "time latency" !important; align-items: center !important; padding: 0.6rem 0.75rem !important; border-bottom: 1px solid rgba(125,125,125,0.12) !important; border-radius: 6px !important; margin-bottom: 0.25rem !important; cursor: pointer !important; -webkit-tap-highlight-color: transparent !important; }', + ' .mosdns-table .mosdns-log-row:active { background: rgba(125,125,125,0.1) !important; }', + ' .mosdns-table .col-domain { grid-area: domain !important; display: flex !important; align-items: center !important; min-width: 0 !important; overflow: hidden !important; padding: 0 !important; border: none !important; }', + ' .mosdns-table .col-domain .mosdns-mono { overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; font-size: 0.88rem !important; font-weight: 600 !important; }', + ' .mosdns-table .col-domain .badge-qtype { flex-shrink: 0 !important; margin-left: 0.35rem !important; font-size: 0.68rem !important; }', + ' .mosdns-table .col-status { grid-area: status !important; display: flex !important; justify-content: flex-end !important; align-items: center !important; padding: 0 0 0 0.5rem !important; border: none !important; flex-shrink: 0 !important; }', + ' .mosdns-table .col-status .mosdns-badge { font-size: 0.72rem !important; min-width: 62px !important; padding: 0.15em 0.4em !important; }', + ' .mosdns-table .col-time { grid-area: time !important; display: block !important; font-size: 0.76rem !important; opacity: 0.6 !important; padding: 0.25rem 0 0 0 !important; border: none !important; white-space: nowrap !important; }', + ' .mosdns-table .col-latency { grid-area: latency !important; display: block !important; text-align: right !important; font-size: 0.78rem !important; font-weight: 600 !important; padding: 0.25rem 0 0 0.5rem !important; border: none !important; white-space: nowrap !important; }', + ' .mosdns-table .mosdns-empty-row { display: block !important; text-align: center !important; border-bottom: none !important; }', + ' .mosdns-table .mosdns-empty-row .td { display: block !important; width: 100% !important; padding: 2rem 0 !important; border: none !important; }', + '}', '.mosdns-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.85rem; padding-bottom: 0.75rem; border-bottom: 1px solid rgba(125,125,125,0.15); flex-wrap: wrap; gap: 0.5rem; }', '.mosdns-modal-domain { font-size: 1.05rem; font-weight: 700; word-break: break-all; display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem; }', - '.mosdns-modal-meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 0.6rem; margin-bottom: 1rem; }', - '.mosdns-modal-meta-item { background: rgba(125,125,125,0.04); border: 1px solid rgba(125,125,125,0.08); border-radius: 6px; padding: 0.5rem 0.75rem; }', + '.mosdns-modal-meta-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 0.6rem; margin-bottom: 1rem; }', + '.mosdns-modal-meta-item { background: rgba(125,125,125,0.04); border: 1px solid rgba(125,125,125,0.08); border-radius: 6px; padding: 0.5rem 0.75rem; min-width: 0; overflow: hidden; }', '.mosdns-modal-meta-item .meta-label { font-size: 0.75rem; opacity: 0.6; margin-bottom: 0.2rem; font-weight: 600; }', - '.mosdns-modal-meta-item .meta-val { font-size: 0.85rem; font-weight: 600; }', + '.mosdns-modal-meta-item .meta-val { font-size: 0.85rem; font-weight: 600; word-break: break-all; overflow-wrap: anywhere; }', '.mosdns-modal-section-title { font-size: 0.9rem; font-weight: 700; margin: 0.85rem 0 0.45rem 0; display: flex; align-items: center; justify-content: space-between; }', '.mosdns-answers-list { display: flex; flex-direction: column; gap: 0.35rem; max-height: 240px; overflow-y: auto; }', '.mosdns-answer-row { display: flex; justify-content: space-between; align-items: center; background: rgba(125,125,125,0.04); border: 1px solid rgba(125,125,125,0.08); border-radius: 6px; padding: 0.4rem 0.65rem; gap: 0.5rem; font-size: 0.82rem; }', '.mosdns-answer-data { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }', '.mosdns-answer-ttl { font-size: 0.75rem; opacity: 0.65; white-space: nowrap; }', + '.mosdns-action-btn { padding: 0.18rem 0.5rem !important; font-size: 0.72rem !important; line-height: 1.25 !important; border-radius: 4px !important; margin: 0 2px !important; cursor: pointer; transition: all 0.2s ease; font-weight: 600; text-decoration: none !important; display: inline-block; vertical-align: middle; }', + '.btn-allow { background: rgba(16, 185, 129, 0.12) !important; color: #059669 !important; border: 1px solid rgba(16, 185, 129, 0.3) !important; }', + '.btn-allow:hover { background: #059669 !important; color: #fff !important; }', + '.btn-block { background: rgba(239, 68, 68, 0.12) !important; color: #dc2626 !important; border: 1px solid rgba(239, 68, 68, 0.3) !important; }', + '.btn-block:hover { background: #dc2626 !important; color: #fff !important; }', '@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }', '@media (prefers-color-scheme: dark) {', ' .mosdns-stat-card, .mosdns-rank-panel, .mosdns-modal-meta-item, .mosdns-answer-row { background: rgba(255,255,255,0.03); border-color: rgba(255,255,255,0.08); box-shadow: none; }', ' .mosdns-sparkline-tooltip { background: #1e242b; border-color: rgba(255,255,255,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.5); }', + ' .mosdns-log-row:hover { background: rgba(255,255,255,0.04); }', + ' .mosdns-log-row:active { background: rgba(255,255,255,0.08) !important; }', + ' .mosdns-log-row { border-bottom-color: rgba(255,255,255,0.08) !important; }', ' .badge-danger { background: rgba(239, 68, 68, 0.2); color: #f87171; border-color: rgba(239, 68, 68, 0.35); }', ' .badge-teal { background: rgba(16, 185, 129, 0.2); color: #34d399; border-color: rgba(16, 185, 129, 0.35); }', ' .badge-primary { background: rgba(59, 130, 246, 0.2); color: #60a5fa; border-color: rgba(59, 130, 246, 0.35); }', ' .badge-neutral { background: rgba(156, 163, 175, 0.2); color: #9ca3af; border-color: rgba(156, 163, 175, 0.3); }', + ' .btn-allow { background: rgba(16, 185, 129, 0.2) !important; color: #34d399 !important; border-color: rgba(16, 185, 129, 0.35) !important; }', + ' .btn-allow:hover { background: #059669 !important; color: #fff !important; }', + ' .btn-block { background: rgba(239, 68, 68, 0.2) !important; color: #f87171 !important; border-color: rgba(239, 68, 68, 0.35) !important; }', + ' .btn-block:hover { background: #dc2626 !important; color: #fff !important; }', ' .dns-latency-fastest { color: #34d399; }', ' .dns-latency-normal { color: #60a5fa; }', ' .dns-latency-timeout { color: #f87171; }', @@ -221,10 +283,16 @@ const createSparklineSVG = (strokeColor, fillGradId) => { if (coord.time) { const d = new Date(coord.time); if (!isNaN(d)) { + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); const hh = String(d.getHours()).padStart(2, '0'); - timeStr = hh + ':00'; + const min = String(d.getMinutes()).padStart(2, '0'); + timeStr = `${yyyy}-${mm}-${dd} ${hh}:${min}`; + } else if (coord.time.length >= 16 && coord.time.includes('T')) { + timeStr = coord.time.slice(0, 10) + ' ' + coord.time.slice(11, 16); } else { - timeStr = coord.time.slice(11, 16) || coord.time; + timeStr = coord.time; } } @@ -238,7 +306,7 @@ const createSparklineSVG = (strokeColor, fillGradId) => { const tooltipX = (coord.x / width) * containerWidth; const ratio = coord.x / width; - if (ratio > 0.65) { + if (ratio > 0.5) { tooltip.style.left = 'auto'; tooltip.style.right = (containerWidth - tooltipX + 8) + 'px'; } else { @@ -356,6 +424,7 @@ const createOverviewStatsDOM = () => { const metricTotal = E('div', { class: 'metric-val' }, '-'); const subtextTotal = E('div', { class: 'subtext' }, '-'); const sparklineTotal = createSparklineSVG('#3b82f6', 'spark-grad-total'); + const badgeQps = E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live') + ' - QPS'); const badgeBlocked = E('span', { class: 'mosdns-badge badge-danger' }, '-'); const metricBlocked = E('div', { class: 'metric-val', style: 'color: #dc2626;' }, '-'); @@ -373,7 +442,7 @@ const createOverviewStatsDOM = () => { E('div', {}, [ E('div', { class: 'title-row' }, [ E('span', {}, _('DNS Queries Total')), - E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live')) + badgeQps ]), metricTotal, subtextTotal @@ -420,6 +489,7 @@ const createOverviewStatsDOM = () => { metricTotal, subtextTotal, sparklineTotal, + badgeQps, badgeBlocked, metricBlocked, sparklineBlocked, @@ -453,7 +523,8 @@ const updateOverviewStats = (stats, historyData) => { cached_queries: cached = 0, blocked_percentage: blocked_pct = 0, cached_percentage: cached_pct = 0, - avg_latency_ms: avg_ms = 0 + avg_latency_ms: avg_ms = 0, + qps = 0 } = stats; const points = historyData?.points || []; @@ -466,6 +537,7 @@ const updateOverviewStats = (stats, historyData) => { statsElements.metricTotal.textContent = total.toLocaleString(); statsElements.subtextTotal.textContent = _('Avg Processing') + ': ' + avg_ms + ' ms'; statsElements.sparklineTotal.update(totalItems, baseMax); + statsElements.badgeQps.textContent = _('● Live') + ' ' + (qps || 0) + ' QPS'; statsElements.badgeBlocked.textContent = blocked_pct + '%'; statsElements.metricBlocked.textContent = blocked.toLocaleString(); @@ -490,8 +562,14 @@ const renderTopRankings = topData => { const maxCount = Math.max(...items.map(i => i.count || 1)); return E('div', { style: 'display: flex; flex-direction: column;' }, items.map(item => { - let val = item[key] || '-'; - if (isClient) val = cleanIP(val); + let rawVal = item[key] || '-'; + let valText = rawVal; + let valTitle = rawVal; + if (isClient) { + const clientInfo = getClientDisplay(rawVal); + valText = clientInfo.display; + valTitle = clientInfo.title; + } const cnt = item.count || 0; const pct = Math.round((cnt / maxCount) * 100); @@ -500,8 +578,8 @@ const renderTopRankings = topData => { E('span', { class: 'mosdns-mono', style: 'font-size: 0.82rem; z-index: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-right: 0.5rem;', - title: val - }, val), + title: valTitle + }, valText), E('span', { class: 'mosdns-badge badge-neutral mosdns-mono', style: 'z-index: 1;' }, cnt.toLocaleString()) ]); }) @@ -534,12 +612,132 @@ const renderTopRankings = topData => { }; const updateTopRankings = topData => { + if (topData?.leases && typeof topData.leases === 'object') { + clientLeases = Object.assign(clientLeases, topData.leases); + } const json = JSON.stringify(topData || {}); if (json === lastTopJson) return; lastTopJson = json; dom.content(nodeTop, renderTopRankings(topData)); }; +const promptAddRule = (rawDomain, actionType) => { + const cleanDomain = (rawDomain || '').replace(/\.+$/, '').trim().toLowerCase(); + if (!cleanDomain) { + ui.addNotification(null, E('p', _('Domain name cannot be empty.')), 'warning'); + return; + } + + const isPermit = (actionType === 'permit' || actionType === 'allow' || actionType === 'whitelist'); + const targetFile = isPermit ? WHITELIST_FILE : BLOCKLIST_FILE; + const targetName = isPermit ? _('White Lists') : _('Block Lists'); + const opposingFile = isPermit ? BLOCKLIST_FILE : WHITELIST_FILE; + const opposingName = isPermit ? _('Block Lists') : _('White Lists'); + + const cleanRuleLine = l => { + let s = (l || '').trim().toLowerCase(); + if (!s || s.startsWith('#')) return null; + s = s.replace(/^\|\|/, '').replace(/\^.*$/, ''); + s = s.replace(/^(domain|full|keyword):/, ''); + return s.replace(/\.+$/, '').trim(); + }; + + const inputDomain = E('input', { + class: 'cbi-input-text', + type: 'text', + value: cleanDomain, + style: 'width: 100%; font-family: monospace; font-size: 0.95rem; margin-top: 0.5rem;' + }); + + const executeAddRule = () => { + const domainToAdd = inputDomain.value.replace(/\.+$/, '').trim().toLowerCase(); + if (!domainToAdd) { + ui.addNotification(null, E('p', _('Domain name cannot be empty.')), 'warning'); + return; + } + + ui.hideModal(); + + fs.read(targetFile).then(content => { + const rawLines = (content || '').split(/\r?\n/); + const existingRules = rawLines.map(cleanRuleLine).filter(Boolean); + const alreadyInTarget = existingRules.includes(domainToAdd); + + let appendPromise = Promise.resolve(); + if (!alreadyInTarget) { + let newContent = (content || '').trimEnd(); + if (newContent.length > 0) { + newContent += '\n' + domainToAdd + '\n'; + } else { + newContent = '# MosDNS Rules\n' + domainToAdd + '\n'; + } + appendPromise = fs.write(targetFile, newContent); + } + + return appendPromise.then(() => { + return fs.read(opposingFile).then(oppContent => { + if (!oppContent) return; + const oppLines = oppContent.split(/\r?\n/); + let changed = false; + const filtered = oppLines.filter(l => { + const trimmed = l.trim().toLowerCase(); + const cleaned = cleanRuleLine(l); + if (trimmed === domainToAdd || cleaned === domainToAdd) { + changed = true; + return false; + } + return true; + }); + if (changed) { + let oppNew = filtered.join('\n').trimEnd(); + if (oppNew.length > 0) oppNew += '\n'; + return fs.write(opposingFile, oppNew); + } + }); + }); + }).catch(err => { + ui.addNotification(null, E('p', _('Failed to update rule file: %s').format(err.message)), 'error'); + }); + }; + + inputDomain.addEventListener('keydown', e => { + if (e.key === 'Enter') { + e.preventDefault(); + executeAddRule(); + } + }); + + const title = isPermit ? _('Add to Whitelist') : _('Add to Blocklist'); + const btnClass = isPermit ? 'cbi-button-save' : 'cbi-button-reset'; + + ui.showModal(title, [ + E('div', { style: 'padding: 0.5rem 0;' }, [ + E('p', { style: 'margin-bottom: 0.5rem;' }, + isPermit + ? _('Add domain to the whitelist to permit DNS resolution:') + : _('Add domain to the blocklist to block DNS resolution:') + ), + E('label', { style: 'font-weight: 600; font-size: 0.85rem;' }, _('Domain:')), + inputDomain, + E('p', { class: 'cbi-value-description', style: 'margin-top: 0.4rem; font-size: 0.8rem; opacity: 0.7;' }, + _('Target file: %s (supports domain matching rules)').format(targetFile) + ) + ]), + E('div', { class: 'right', style: 'margin-top: 1.25rem; display: flex; justify-content: flex-end; gap: 0.5rem;' }, [ + E('button', { + class: 'btn cbi-button cbi-button-neutral', + click: ui.hideModal + }, _('Cancel')), + E('button', { + class: 'btn cbi-button ' + btnClass, + click: executeAddRule + }, _('Confirm')) + ]) + ]); + + setTimeout(() => inputDomain.focus(), 100); +}; + const showLogDetailsModal = item => { let statusBadge; if (item.is_blocked) { @@ -570,6 +768,8 @@ const showLogDetailsModal = item => { }, _('No DNS answer records returned.')); } + const clientInfo = getClientDisplay(item.client_ip); + const body = E('div', { style: 'padding: 0.25rem 0;' }, [ E('div', { class: 'mosdns-modal-header' }, [ E('div', { class: 'mosdns-modal-domain' }, [ @@ -585,7 +785,7 @@ const showLogDetailsModal = item => { E('div', { class: 'mosdns-modal-meta-grid' }, [ E('div', { class: 'mosdns-modal-meta-item' }, [ E('div', { class: 'meta-label' }, _('Client IP')), - E('div', { class: 'meta-val mosdns-mono' }, cleanIP(item.client_ip)) + E('div', { class: 'meta-val mosdns-mono' }, clientInfo.display) ]), E('div', { class: 'mosdns-modal-meta-item' }, [ E('div', { class: 'meta-label' }, _('Time')), @@ -610,7 +810,22 @@ const showLogDetailsModal = item => { ui.showModal(_('Query Log Details'), [ body, - E('div', { class: 'right', style: 'margin-top: 1.25rem;' }, [ + E('div', { class: 'right', style: 'margin-top: 1.25rem; display: flex; justify-content: space-between; align-items: center;' }, [ + item.is_blocked ? E('button', { + class: 'btn cbi-button mosdns-action-btn btn-allow', + style: 'padding: 0.35rem 0.75rem !important; font-size: 0.82rem !important;', + click: () => { + ui.hideModal(); + promptAddRule(item.domain, 'permit'); + } + }, _('Permit')) : E('button', { + class: 'btn cbi-button mosdns-action-btn btn-block', + style: 'padding: 0.35rem 0.75rem !important; font-size: 0.82rem !important;', + click: () => { + ui.hideModal(); + promptAddRule(item.domain, 'intercept'); + } + }, _('Intercept')), E('button', { class: 'btn cbi-button cbi-button-action', click: ui.hideModal @@ -639,26 +854,35 @@ const renderLogsTable = logsData => { ? item.answers.map(a => a.data + ' (' + a.type + ')').join(', ') : '-'; - return E('tr', { class: 'tr' }, [ - E('td', { class: 'td', style: 'font-size: 0.82rem; opacity: 0.7; white-space: nowrap;' }, formatTimestamp(item.timestamp)), - E('td', { class: 'td mosdns-mono', style: 'font-size: 0.82rem; white-space: nowrap;' }, cleanIP(item.client_ip)), - E('td', { class: 'td', style: 'max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', title: item.domain || '-' }, [ + const clientInfo = getClientDisplay(item.client_ip); + + return E('tr', { + class: 'tr mosdns-log-row', + title: _('Click to view full details'), + click: () => showLogDetailsModal(item) + }, [ + E('td', { class: 'td col-time', style: 'font-size: 0.82rem; opacity: 0.7; white-space: nowrap;' }, formatTimestamp(item.timestamp)), + E('td', { + class: 'td col-client mosdns-mono', + style: 'font-size: 0.82rem; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', + title: clientInfo.title + }, clientInfo.display), + E('td', { class: 'td col-domain', style: 'max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', title: item.domain || '-' }, [ E('span', { class: 'mosdns-mono', style: 'font-weight: 600;' }, item.domain || '-'), E('span', { class: 'badge-qtype' }, item.qtype || 'A') ]), - E('td', { class: 'td' }, statusBadge), + E('td', { class: 'td col-status' }, statusBadge), E('td', { - class: 'td mosdns-mono', - style: 'font-size: 0.82rem; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer;', - title: _('Click to view full details'), - click: () => showLogDetailsModal(item) + class: 'td col-answers mosdns-mono', + style: 'font-size: 0.82rem; max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', + title: _('Click to view full details') }, answersText), - E('td', { class: 'td mosdns-mono ' + getLatencyClass(item.elapsed_ms), style: 'text-align: right; font-size: 0.82rem;' }, item.elapsed_ms + ' ms') + E('td', { class: 'td col-latency mosdns-mono ' + getLatencyClass(item.elapsed_ms), style: 'text-align: right; font-size: 0.82rem;' }, item.elapsed_ms + ' ms') ]); }); if (!rows.length) { - rows.push(E('tr', { class: 'tr' }, [ + rows.push(E('tr', { class: 'tr mosdns-empty-row' }, [ E('td', { class: 'td', colspan: 6, style: 'text-align: center; opacity: 0.5; padding: 2rem;' }, _('No query log entries found.')) ])); } @@ -666,12 +890,12 @@ const renderLogsTable = logsData => { return E('div', {}, [ E('table', { class: 'table cbi-section-table mosdns-table', style: 'margin-top: 0.25rem;' }, [ E('tr', { class: 'tr table-titles' }, [ - E('th', { class: 'th', style: 'width: 85px;' }, _('Time')), - E('th', { class: 'th', style: 'width: 125px;' }, _('Client IP')), - E('th', { class: 'th' }, _('Domain & Record')), - E('th', { class: 'th', style: 'width: 90px;' }, _('Status')), - E('th', { class: 'th' }, _('Answers')), - E('th', { class: 'th', style: 'width: 90px; text-align: right;' }, _('Elapsed')) + E('th', { class: 'th col-time', style: 'width: 85px;' }, _('Time')), + E('th', { class: 'th col-client', style: 'width: 125px;' }, _('Client IP')), + E('th', { class: 'th col-domain' }, _('Domain & Record')), + E('th', { class: 'th col-status', style: 'width: 90px;' }, _('Status')), + E('th', { class: 'th col-answers' }, _('Answers')), + E('th', { class: 'th col-latency', style: 'width: 80px; text-align: right;' }, _('Elapsed')) ]), ...rows ]), @@ -753,7 +977,8 @@ return view.extend({ L.resolveDefault(callGetStats(), {}), L.resolveDefault(callGetTop(10), {}), L.resolveDefault(callGetLogs(PAGE_SIZE, 0, searchVal, filterVal), {}), - L.resolveDefault(callGetHistory(24), {}) + L.resolveDefault(callGetHistory(24), {}), + L.resolveDefault(callGetLeases(), {}) ]); }, @@ -764,6 +989,14 @@ return view.extend({ currentBadgeState = null; lastTopJson = ''; lastLogsJson = ''; + clientLeases = {}; + + if (data[4]?.leases) { + clientLeases = Object.assign(clientLeases, data[4].leases); + } + if (data[1]?.leases) { + clientLeases = Object.assign(clientLeases, data[1].leases); + } nodeStats = E('div', { id: 'overview-stats' }); nodeTop = E('div', { id: 'top-rankings' }); diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js index ebc0f4f6..33151478 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js @@ -86,9 +86,20 @@ return view.extend({ }); }, 1000); } else { - statusMsg.textContent = res.error || _('Failed to start update.'); + if (res.error && res.error.match(/Another update is already in progress/)) { + statusMsg.textContent = _('Another update is already in progress.'); + statusMsg.style.color = '#ff9900'; + callGetUpdateLog().then(logRes => { + if (logRes && logRes.log) { + logTextarea.value = logRes.log; + logTextarea.scrollTop = logTextarea.scrollHeight; + } + }); + } else { + statusMsg.textContent = res.error || _('Failed to start update.'); + statusMsg.style.color = '#ed4014'; + } statusMsg.classList.remove('spinning'); - statusMsg.style.color = '#ed4014'; closeButton.style.display = 'inline'; } }).catch(e => { diff --git a/luci-app-mosdns/po/templates/mosdns.pot b/luci-app-mosdns/po/templates/mosdns.pot index a136fad4..2f354186 100644 --- a/luci-app-mosdns/po/templates/mosdns.pot +++ b/luci-app-mosdns/po/templates/mosdns.pot @@ -17,32 +17,48 @@ msgstr "" msgid "API Options" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:31 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:718 +msgid "Add domain to the blocklist to block DNS resolution:" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:717 +msgid "Add domain to the whitelist to permit DNS resolution:" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:710 +msgid "Add to Blocklist" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:710 +msgid "Add to Whitelist" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:26 msgid "" "Added domain names always permit resolution using 'local DNS' with the " "highest priority (one domain per line, supports domain matching rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:103 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:50 msgid "" "Added domain names will always use 'Local DNS' for resolution, with a forced " "TTL of 5 seconds, and results will not be cached (one domain per line, " "supports domain matching rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:79 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:42 msgid "" "Added domain names will always use 'Remote DNS' for resolution (one domain " "per line, supports domain matching rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:55 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:34 msgid "" "Added domain names will block DNS resolution (one domain per line, supports " "domain matching rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:175 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:74 msgid "" "Added domain names will block PTR requests (one domain per line, supports " "domain matching rules)." @@ -74,16 +90,17 @@ msgstr "" msgid "Aliyun Public DNS (DNS over QUIC)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:640 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1026 msgid "All Queries" msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:90 msgid "Another update is already in progress." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:463 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:531 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:805 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:897 msgid "Answers" msgstr "" @@ -91,7 +108,7 @@ msgstr "" msgid "Apple domains optimization" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:667 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1053 msgid "" "Are you sure you want to clear all real-time query logs and top rankings?" msgstr "" @@ -104,17 +121,17 @@ msgstr "" msgid "Auto Save Stats Interval" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 msgid "" "Automatically update GeoIP and GeoSite databases as well as ad filtering " "rules through scheduled tasks." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:336 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:478 msgid "Average Processing Time" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:306 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 msgid "Avg Processing" msgstr "" @@ -137,19 +154,21 @@ msgid "" "using only A/AAAA records." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:19 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:31 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:633 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:635 msgid "Block Lists" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:24 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:71 msgid "Block PTR" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:641 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1027 msgid "Blocked Only" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:314 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:456 msgid "Blocked by Filters" msgstr "" @@ -171,23 +190,24 @@ msgstr "" msgid "Cache Prefetching" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:642 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1028 msgid "Cached Only" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:325 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:467 msgid "Cached Queries" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:672 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:730 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1058 msgid "Cancel" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:161 msgid "Check And Update" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:159 msgid "Check And Update GeoData." msgstr "" @@ -209,7 +229,7 @@ msgstr "" msgid "Cisco Public DNS (208.67.222.222)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:692 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1078 msgid "Clear" msgstr "" @@ -217,21 +237,22 @@ msgstr "" msgid "Clear logs" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:663 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:666 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1049 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1052 msgid "Clear query logs" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:511 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:861 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:878 msgid "Click to view full details" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:445 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:528 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:787 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:894 msgid "Client IP" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:475 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:832 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 msgid "Close" msgstr "" @@ -271,6 +292,10 @@ msgstr "" msgid "Configuration Editor" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:734 +msgid "Confirm" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:199 msgid "Custom China DNS" msgstr "" @@ -279,7 +304,7 @@ msgstr "" msgid "Custom Config" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:127 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:58 msgid "" "Custom Hosts rewrite, for example: baidu.com 10.0.0.1 (one rule per line, " "supports domain matching rules)." @@ -293,7 +318,7 @@ msgstr "" msgid "Custom Stream Media DNS" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:21 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:47 msgid "DDNS Lists" msgstr "" @@ -305,7 +330,7 @@ msgstr "" msgid "DNS Forward" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:302 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:444 msgid "DNS Queries Total" msgstr "" @@ -319,7 +344,7 @@ msgstr "" msgid "DNS redirect" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:149 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:160 msgid "Database Update" msgstr "" @@ -349,15 +374,24 @@ msgstr "" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:529 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:895 msgid "Domain & Record" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:532 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:627 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:655 +msgid "Domain name cannot be empty." +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:720 +msgid "Domain:" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:898 msgid "Elapsed" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 msgid "Enable Auto Database Update" msgstr "" @@ -403,35 +437,35 @@ msgstr "" msgid "Error" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:115 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:126 msgid "Every Day" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:120 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 msgid "Every Friday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:127 msgid "Every Monday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 msgid "Every Saturday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:133 msgid "Every Sunday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:119 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:130 msgid "Every Thursday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:128 msgid "Every Tuesday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:118 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:129 msgid "Every Wednesday" msgstr "" @@ -444,19 +478,23 @@ msgstr "" msgid "Failed to clean logs." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:686 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1072 msgid "Failed to clear query logs:" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:99 msgid "Failed to start update." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:970 msgid "Failed to update query logs:" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:658 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:699 +msgid "Failed to update rule file: %s" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1044 msgid "First Page / Resume" msgstr "" @@ -496,11 +534,11 @@ msgstr "" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:147 msgid "Full" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:134 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:145 msgid "Full: includes all Countries and Private IP addresses." msgstr "" @@ -512,7 +550,7 @@ msgstr "" msgid "GeoIP Tags" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 msgid "GeoIP Type" msgstr "" @@ -524,7 +562,7 @@ msgstr "" msgid "Geodata Update" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:141 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:152 msgid "GitHub Proxy" msgstr "" @@ -543,11 +581,11 @@ msgstr "" msgid "Grant UCI access for luci-app-mosdns" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:20 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:39 msgid "Grey Lists" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:22 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:55 msgid "Hosts" msgstr "" @@ -557,10 +595,10 @@ msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:410 msgid "" -"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www.cloudflare.com/ips-" -"v6" +"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www." +"cloudflare.com/ips-v6" msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:190 @@ -583,13 +621,17 @@ msgstr "" msgid "Info" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:828 +msgid "Intercept" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:347 msgid "" "Interval for the background thread to scan the cache for prefetching (in " "seconds)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:337 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:479 msgid "Latency" msgstr "" @@ -610,11 +652,11 @@ msgstr "" msgid "Listen Port" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:137 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 msgid "Little" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "Little: only include Mainland China and Private IP addresses." msgstr "" @@ -674,12 +716,12 @@ msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:33 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:106 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:70 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1094 #: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:3 msgid "MosDNS" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:279 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:510 msgid "" "MosDNS API is unreachable. Please ensure MosDNS is running and stats_api " "plugin is enabled." @@ -697,15 +739,15 @@ msgstr "" msgid "Netflix, Disney+, Hulu and streaming media rules list will use this DNS" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:561 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:927 msgid "Next" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:428 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:768 msgid "No DNS answer records returned." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:353 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:560 msgid "No data available" msgstr "" @@ -713,18 +755,22 @@ msgstr "" msgid "No log data." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:520 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:886 msgid "No query log entries found." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:904 msgid "Page %d / %d (%d entries)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:340 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:438 msgid "Per-query speed" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:821 +msgid "Permit" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:301 msgid "" "Please provide the IP address you use when accessing foreign websites. This " @@ -756,7 +802,7 @@ msgstr "" msgid "Prevent DNS Leaks" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:550 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:916 msgid "Previous" msgstr "" @@ -775,7 +821,7 @@ msgstr "" msgid "Quad9 Public DNS (9.9.9.9)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:469 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:811 msgid "Query Log Details" msgstr "" @@ -785,7 +831,7 @@ msgid "" "consume more memory)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:681 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1067 msgid "Query logs cleared successfully." msgstr "" @@ -793,15 +839,15 @@ msgstr "" msgid "RUNNING" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:712 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1098 msgid "Real-time Query Logs" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:63 msgid "Redirect" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:151 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:66 msgid "" "Redirecting requests for domain names. Request domain A, but return records " "for domain B, for example: baidu.com qq.com (one rule per line)." @@ -823,11 +869,11 @@ msgstr "" msgid "Ring Buffer Capacity" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:457 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:799 msgid "Rule Hit" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:11 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:9 msgid "Rule Settings" msgstr "" @@ -843,7 +889,7 @@ msgstr "" msgid "Save the cache locally and reload the cache dump on the next startup" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:629 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1015 msgid "Search domain or client IP..." msgstr "" @@ -855,7 +901,7 @@ msgstr "" msgid "Starting update..." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1094 #: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 msgid "Statistics" msgstr "" @@ -864,11 +910,11 @@ msgstr "" msgid "Stats Dump" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:530 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:896 msgid "Status" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:25 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:79 msgid "Streaming Media" msgstr "" @@ -884,6 +930,10 @@ msgstr "" msgid "TCP/DoT Connection Multiplexing" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:723 +msgid "Target file: %s (supports domain matching rules)" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:210 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:258 msgid "Tencent Public DNS (119.28.28.28)" @@ -898,7 +948,7 @@ msgstr "" msgid "Tencent Public DNS (DNS over HTTPS)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:12 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:10 msgid "The list of rules only apply to 'Default Config' profiles." msgstr "" @@ -916,20 +966,20 @@ msgid "" "content in yaml format." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:449 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:527 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:791 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:893 msgid "Time" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:386 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 msgid "Top Blocked Domains" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:393 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:606 msgid "Top Clients" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:379 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:592 msgid "Top Queried Domains" msgstr "" @@ -942,38 +992,23 @@ msgid "TrafficRoute Public DNS (180.184.2.2)" msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:494 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:43 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:48 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:67 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:72 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:91 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:96 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:115 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:120 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:139 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:144 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:163 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:168 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:187 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:192 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:211 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:216 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:17 msgid "Unable to save contents: %s" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 msgid "Update Cycle" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 msgid "Update GeoIP & GeoSite databases" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 msgid "Update Time" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:153 msgid "" "Update data files with GitHub Proxy, leave blank to disable proxy downloads." msgstr "" @@ -982,7 +1017,7 @@ msgstr "" msgid "Update failed" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 msgid "Update failed: %s" msgstr "" @@ -994,7 +1029,7 @@ msgstr "" msgid "Updating Database..." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:453 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:795 msgid "Upstream" msgstr "" @@ -1002,7 +1037,7 @@ msgstr "" msgid "Warning" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:199 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:82 msgid "" "When enabling 'Custom Stream Media DNS', added domains will always use the " "'Streaming Media DNS server' for resolution (one domain per line, supports " @@ -1015,7 +1050,9 @@ msgid "" "(domain list or AdGuardHome rules)." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:18 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:633 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:635 msgid "White Lists" msgstr "" @@ -1029,18 +1066,19 @@ msgstr "" msgid "Xinfeng Public DNS (114.114.115.115)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:154 msgid "https://gh-proxy.com" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:303 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:427 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:540 msgid "● Live" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:148 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:223 msgid "● Live Auto-refresh" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:152 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:227 msgid "❚❚ Paused (Page %d)" msgstr "" diff --git a/luci-app-mosdns/po/zh_Hans/mosdns.po b/luci-app-mosdns/po/zh_Hans/mosdns.po index da0ee102..cf02030d 100644 --- a/luci-app-mosdns/po/zh_Hans/mosdns.po +++ b/luci-app-mosdns/po/zh_Hans/mosdns.po @@ -27,7 +27,23 @@ msgstr "API 监听端口" msgid "API Options" msgstr "API 选项" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:31 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:718 +msgid "Add domain to the blocklist to block DNS resolution:" +msgstr "将域名添加到黑名单以拦截 DNS 解析:" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:717 +msgid "Add domain to the whitelist to permit DNS resolution:" +msgstr "将域名添加到白名单以放行 DNS 解析:" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:710 +msgid "Add to Blocklist" +msgstr "添加到黑名单" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:710 +msgid "Add to Whitelist" +msgstr "添加到白名单" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:26 msgid "" "Added domain names always permit resolution using 'local DNS' with the " "highest priority (one domain per line, supports domain matching rules)." @@ -35,7 +51,7 @@ msgstr "" "加入的域名始终允许使用 “本地 DNS” 进行解析,且优先级最高(每个域名一行,支持" "域名匹配规则)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:103 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:50 msgid "" "Added domain names will always use 'Local DNS' for resolution, with a forced " "TTL of 5 seconds, and results will not be cached (one domain per line, " @@ -44,20 +60,20 @@ msgstr "" "加入的域名始终使用 “本地 DNS” 进行解析,并且强制 TTL 5 秒,解析结果不会进入缓" "存(每个域名一行,支持域名匹配规则)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:79 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:42 msgid "" "Added domain names will always use 'Remote DNS' for resolution (one domain " "per line, supports domain matching rules)." msgstr "" "加入的域名始终使用 “远程 DNS” 进行解析(每个域名一行,支持域名匹配规则)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:55 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:34 msgid "" "Added domain names will block DNS resolution (one domain per line, supports " "domain matching rules)." msgstr "加入的域名将屏蔽 DNS 解析(每个域名一行,支持域名匹配规则)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:175 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:74 msgid "" "Added domain names will block PTR requests (one domain per line, supports " "domain matching rules)." @@ -89,16 +105,17 @@ msgstr "阿里云公共 DNS(DNS over HTTPS)" msgid "Aliyun Public DNS (DNS over QUIC)" msgstr "阿里云公共 DNS(DNS over QUIC)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:640 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1026 msgid "All Queries" msgstr "所有查询" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:90 msgid "Another update is already in progress." msgstr "另一个更新正在进行中。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:463 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:531 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:805 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:897 msgid "Answers" msgstr "应答结果" @@ -106,7 +123,7 @@ msgstr "应答结果" msgid "Apple domains optimization" msgstr "Apple 域名解析优化" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:667 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1053 msgid "" "Are you sure you want to clear all real-time query logs and top rankings?" msgstr "确定要清空所有实时查询日志和排行数据吗?" @@ -119,17 +136,17 @@ msgstr "自动保存缓存间隔(秒)" msgid "Auto Save Stats Interval" msgstr "自动保存统计间隔" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 msgid "" "Automatically update GeoIP and GeoSite databases as well as ad filtering " "rules through scheduled tasks." msgstr "通过定时任务自动更新 GeoIP 和 GeoSite 数据库以及广告过滤规则。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:336 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:478 msgid "Average Processing Time" msgstr "平均延迟" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:306 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 msgid "Avg Processing" msgstr "平均处理" @@ -154,19 +171,21 @@ msgstr "" "屏蔽 DNS 类型 65 记录(HTTPS/SVCB,用于 HTTP/3、ECH 等),强制仅使用 A/AAAA " "解析。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:19 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:31 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:633 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:635 msgid "Block Lists" msgstr "黑名单" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:24 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:71 msgid "Block PTR" msgstr "PTR 黑名单" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:641 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1027 msgid "Blocked Only" msgstr "仅拦截" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:314 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:456 msgid "Blocked by Filters" msgstr "拦截总数" @@ -188,23 +207,24 @@ msgstr "自动保存缓存" msgid "Cache Prefetching" msgstr "缓存预取" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:642 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1028 msgid "Cached Only" msgstr "仅缓存" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:325 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:467 msgid "Cached Queries" msgstr "缓存命中" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:672 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:730 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1058 msgid "Cancel" msgstr "取消" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:161 msgid "Check And Update" msgstr "检查并更新" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:159 msgid "Check And Update GeoData." msgstr "检查并更新 GeoData 数据库。" @@ -226,7 +246,7 @@ msgstr "思科公共 DNS(208.67.220.220)" msgid "Cisco Public DNS (208.67.222.222)" msgstr "思科公共 DNS(208.67.222.222)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:692 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1078 msgid "Clear" msgstr "清空" @@ -234,21 +254,22 @@ msgstr "清空" msgid "Clear logs" msgstr "清空日志" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:663 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:666 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1049 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1052 msgid "Clear query logs" msgstr "清除查询日志" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:511 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:861 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:878 msgid "Click to view full details" msgstr "点击查看完整详情" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:445 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:528 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:787 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:894 msgid "Client IP" msgstr "客户端 IP" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:475 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:832 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:35 msgid "Close" msgstr "关闭" @@ -288,6 +309,10 @@ msgstr "配置文件" msgid "Configuration Editor" msgstr "配置编辑器" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:734 +msgid "Confirm" +msgstr "确认" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:199 msgid "Custom China DNS" msgstr "自定义国内 DNS" @@ -296,7 +321,7 @@ msgstr "自定义国内 DNS" msgid "Custom Config" msgstr "自定义" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:127 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:58 msgid "" "Custom Hosts rewrite, for example: baidu.com 10.0.0.1 (one rule per line, " "supports domain matching rules)." @@ -311,7 +336,7 @@ msgstr "自选 IP" msgid "Custom Stream Media DNS" msgstr "自定义流媒体 DNS" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:21 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:47 msgid "DDNS Lists" msgstr "DDNS 域名" @@ -323,7 +348,7 @@ msgstr "DNS 缓存大小" msgid "DNS Forward" msgstr "DNS 转发" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:302 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:444 msgid "DNS Queries Total" msgstr "DNS 查询总数" @@ -337,7 +362,7 @@ msgstr "DNS 查询请求并发数,允许同时发起请求的上游 DNS 服务 msgid "DNS redirect" msgstr "DNS 重定向" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:149 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:160 msgid "Database Update" msgstr "数据库更新" @@ -368,15 +393,24 @@ msgstr "" msgid "DoH/TCP/DoT Connection Multiplexing idle timeout (default 30 seconds)" msgstr "DoH/TCP/DoT 连接复用空闲保持时间(默认 30 秒)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:529 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:895 msgid "Domain & Record" msgstr "域名与类型" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:532 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:627 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:655 +msgid "Domain name cannot be empty." +msgstr "域名不能为空" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:720 +msgid "Domain:" +msgstr "域名:" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:898 msgid "Elapsed" msgstr "耗时" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 msgid "Enable Auto Database Update" msgstr "启用自动更新" @@ -422,35 +456,35 @@ msgstr "填写需要导出的 GeoSite.dat 类别条目,允许添加多个标 msgid "Error" msgstr "错误" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:115 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:126 msgid "Every Day" msgstr "每天" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:120 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 msgid "Every Friday" msgstr "每周五" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:127 msgid "Every Monday" msgstr "每周一" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:121 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 msgid "Every Saturday" msgstr "每周六" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:122 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:133 msgid "Every Sunday" msgstr "每周日" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:119 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:130 msgid "Every Thursday" msgstr "每周四" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:117 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:128 msgid "Every Tuesday" msgstr "每周二" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:118 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:129 msgid "Every Wednesday" msgstr "每周三" @@ -463,19 +497,23 @@ msgstr "导出目录:/var/mosdns" msgid "Failed to clean logs." msgstr "清理日志失败:%s" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:686 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1072 msgid "Failed to clear query logs:" msgstr "清除查询日志失败:" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:99 msgid "Failed to start update." msgstr "启动更新失败" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:970 msgid "Failed to update query logs:" msgstr "更新查询日志失败:" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:658 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:699 +msgid "Failed to update rule file: %s" +msgstr "更新规则文件失败: %s" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1044 msgid "First Page / Resume" msgstr "重置并恢复" @@ -515,11 +553,11 @@ msgstr "强制将所有本地 DNS 查询重定向到 MosDNS,即 DNS 劫持。" msgid "Forward Dnsmasq Domain Name resolution requests to MosDNS" msgstr "将 Dnsmasq 域名解析请求转发到 MosDNS 服务器" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:147 msgid "Full" msgstr "全量" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:134 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:145 msgid "Full: includes all Countries and Private IP addresses." msgstr "全量:包含所有国家和私有 IP 地址。" @@ -531,7 +569,7 @@ msgstr "GeoData 导出" msgid "GeoIP Tags" msgstr "GeoIP 标签" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:131 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 msgid "GeoIP Type" msgstr "GeoIP 类型" @@ -543,7 +581,7 @@ msgstr "GeoSite 标签" msgid "Geodata Update" msgstr "更新数据库" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:141 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:152 msgid "GitHub Proxy" msgstr "GitHub 代理" @@ -562,11 +600,11 @@ msgstr "谷歌公共 DNS(8.8.8.8)" msgid "Grant UCI access for luci-app-mosdns" msgstr "授予 luci-app-mosdns 的 UCI 访问权限" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:20 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:39 msgid "Grey Lists" msgstr "灰名单" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:22 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:55 msgid "Hosts" msgstr "Hosts 列表" @@ -576,15 +614,15 @@ msgstr "IP 地址" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:410 msgid "" -"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www.cloudflare.com/ips-" -"v6" +"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www." +"cloudflare.com/ips-v6" msgstr "" -"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www.cloudflare.com/ips-" -"v6" +"IPv4 CIDR: https://www.cloudflare.com/ips-v4
IPv6 CIDR: https://www." +"cloudflare.com/ips-v6" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:190 msgid "" @@ -606,13 +644,17 @@ msgstr "空闲超时" msgid "Info" msgstr "信息" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:828 +msgid "Intercept" +msgstr "拦截" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:347 msgid "" "Interval for the background thread to scan the cache for prefetching (in " "seconds)." msgstr "后台线程扫描缓存以进行预取的时间间隔(单位:秒)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:337 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:479 msgid "Latency" msgstr "延迟" @@ -633,11 +675,11 @@ msgstr "监听地址" msgid "Listen Port" msgstr "监听端口" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:137 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:148 msgid "Little" msgstr "轻量" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:132 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "Little: only include Mainland China and Private IP addresses." msgstr "轻量:仅包含中国大陆和私有 IP 地址。" @@ -700,12 +742,12 @@ msgstr "修改 DNS 应答结果的最小 TTL 值 (秒),0 表示不修改" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:33 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:106 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:70 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1094 #: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:3 msgid "MosDNS" msgstr "MosDNS" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:279 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:510 msgid "" "MosDNS API is unreachable. Please ensure MosDNS is running and stats_api " "plugin is enabled." @@ -725,15 +767,15 @@ msgstr "未运行" msgid "Netflix, Disney+, Hulu and streaming media rules list will use this DNS" msgstr "自定义 Netflix、Disney+、Hulu 以及 “流媒体” 规则列表的 DNS 服务器" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:561 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:927 msgid "Next" msgstr "下一页" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:428 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:768 msgid "No DNS answer records returned." msgstr "无 DNS 应答记录" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:353 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:560 msgid "No data available" msgstr "暂无数据" @@ -741,18 +783,22 @@ msgstr "暂无数据" msgid "No log data." msgstr "无日志数据。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:520 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:886 msgid "No query log entries found." msgstr "未找到查询日志记录。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:904 msgid "Page %d / %d (%d entries)" msgstr "第 %d / %d 页(%d 条记录)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:340 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:438 msgid "Per-query speed" msgstr "单次查询速度" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:821 +msgid "Permit" +msgstr "放行" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:301 msgid "" "Please provide the IP address you use when accessing foreign websites. This " @@ -786,7 +832,7 @@ msgstr "当剩余 TTL 小于此值(秒)时触发预取" msgid "Prevent DNS Leaks" msgstr "防止 DNS 泄漏" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:550 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:916 msgid "Previous" msgstr "上一页" @@ -805,7 +851,7 @@ msgstr "Quad9 公共 DNS(149.112.112.112)" msgid "Quad9 Public DNS (9.9.9.9)" msgstr "Quad9 公共 DNS(9.9.9.9)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:469 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:811 msgid "Query Log Details" msgstr "查询日志详情" @@ -815,7 +861,7 @@ msgid "" "consume more memory)" msgstr "查询日志环形缓冲区容量(FIFO 覆盖,默认 2000,数值越大占用内存越多)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:681 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1067 msgid "Query logs cleared successfully." msgstr "查询日志清除成功。" @@ -823,15 +869,15 @@ msgstr "查询日志清除成功。" msgid "RUNNING" msgstr "运行中" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:712 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1098 msgid "Real-time Query Logs" msgstr "实时查询日志" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:63 msgid "Redirect" msgstr "重定向" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:151 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:66 msgid "" "Redirecting requests for domain names. Request domain A, but return records " "for domain B, for example: baidu.com qq.com (one rule per line)." @@ -855,11 +901,11 @@ msgstr "远程 DNS 服务器" msgid "Ring Buffer Capacity" msgstr "环形缓冲区容量" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:457 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:799 msgid "Rule Hit" msgstr "命中规则" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:11 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:9 msgid "Rule Settings" msgstr "自定义规则列表" @@ -875,7 +921,7 @@ msgstr "保存统计数据与查询日志在本地,并在下次启动时重新 msgid "Save the cache locally and reload the cache dump on the next startup" msgstr "保存缓存到本地文件,以供下次启动时重新载入使用" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:629 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1015 msgid "Search domain or client IP..." msgstr "搜索域名或客户端 IP..." @@ -887,7 +933,7 @@ msgstr "设置日志文件的最大容量(单位:MB)。" msgid "Starting update..." msgstr "正在启动更新..." -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:1094 #: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 msgid "Statistics" msgstr "统计" @@ -896,11 +942,11 @@ msgstr "统计" msgid "Stats Dump" msgstr "自动保存统计" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:530 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:896 msgid "Status" msgstr "状态" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:25 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:79 msgid "Streaming Media" msgstr "流媒体" @@ -916,6 +962,10 @@ msgstr "支持本地文件,例如:file:///var/mosdns/example.txt" msgid "TCP/DoT Connection Multiplexing" msgstr "TCP/DoT 连接复用" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:723 +msgid "Target file: %s (supports domain matching rules)" +msgstr "目标文件: %s (支持域名匹配规则)" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:210 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:258 msgid "Tencent Public DNS (119.28.28.28)" @@ -930,7 +980,7 @@ msgstr "腾讯公共 DNS(119.29.29.29)" msgid "Tencent Public DNS (DNS over HTTPS)" msgstr "腾讯公共 DNS(DNS over HTTPS)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:12 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:10 msgid "The list of rules only apply to 'Default Config' profiles." msgstr "规则列表仅适用于 “内置预设” 配置文件。" @@ -952,20 +1002,20 @@ msgstr "" "这是文件 “/etc/mosdns/config_custom.yaml” 的内容,您的 MosDNS 配置将从此文件" "生成。仅接受 yaml 格式的配置内容。" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:449 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:527 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:791 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:893 msgid "Time" msgstr "时间" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:386 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 msgid "Top Blocked Domains" msgstr "拦截域名 (Top 10)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:393 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:606 msgid "Top Clients" msgstr "活跃客户端 (Top 10)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:379 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:592 msgid "Top Queried Domains" msgstr "查询域名 (Top 10)" @@ -978,38 +1028,23 @@ msgid "TrafficRoute Public DNS (180.184.2.2)" msgstr "火山引擎公共 DNS(180.184.2.2)" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:494 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:43 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:48 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:67 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:72 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:91 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:96 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:115 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:120 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:139 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:144 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:163 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:168 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:187 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:192 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:211 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:216 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:17 msgid "Unable to save contents: %s" msgstr "无法保存内容:%s" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:114 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 msgid "Update Cycle" msgstr "更新周期" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:105 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:116 msgid "Update GeoIP & GeoSite databases" msgstr "更新广告规则、GeoIP & GeoSite 数据库" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:125 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:136 msgid "Update Time" msgstr "更新时间" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:142 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:153 msgid "" "Update data files with GitHub Proxy, leave blank to disable proxy downloads." msgstr "通过 GitHub 代理更新数据文件,留空则禁用代理下载。" @@ -1018,7 +1053,7 @@ msgstr "通过 GitHub 代理更新数据文件,留空则禁用代理下载。" msgid "Update failed" msgstr "更新失败" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:95 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 msgid "Update failed: %s" msgstr "更新失败: %s" @@ -1030,7 +1065,7 @@ msgstr "更新成功" msgid "Updating Database..." msgstr "更新数据库..." -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:453 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:795 msgid "Upstream" msgstr "上游服务器" @@ -1038,7 +1073,7 @@ msgstr "上游服务器" msgid "Warning" msgstr "警告" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:199 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:82 msgid "" "When enabling 'Custom Stream Media DNS', added domains will always use the " "'Streaming Media DNS server' for resolution (one domain per line, supports " @@ -1055,7 +1090,9 @@ msgstr "" "使用自定义规则来源时,请使用 MosDNS 支持的规则类型(域名列表或 AdGuardHome 规" "则)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:18 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:633 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:635 msgid "White Lists" msgstr "白名单" @@ -1069,18 +1106,28 @@ msgstr "信风公共 DNS(114.114.114.114)" msgid "Xinfeng Public DNS (114.114.115.115)" msgstr "信风公共 DNS(114.114.115.115)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:154 msgid "https://gh-proxy.com" msgstr "https://gh-proxy.com" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:303 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:427 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:540 msgid "● Live" msgstr "● 实时" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:148 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:223 msgid "● Live Auto-refresh" msgstr "● 实时刷新" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:152 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:227 msgid "❚❚ Paused (Page %d)" msgstr "❚❚ 暂停 (第 %d 页)" + +#~ msgid "Action" +#~ msgstr "操作" + +#~ msgid "Add %s to Blocklist" +#~ msgstr "将 %s 添加到黑名单" + +#~ msgid "Add %s to Whitelist" +#~ msgstr "将 %s 添加到白名单" diff --git a/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json b/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json index ecb5c7b3..6d75ac20 100644 --- a/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json +++ b/luci-app-mosdns/root/usr/share/rpcd/acl.d/luci-app-mosdns.json @@ -3,9 +3,13 @@ "description": "Grant UCI access for luci-app-mosdns", "read": { "file": { + "/etc/hosts": [ "read" ], "/etc/init.d/mosdns": [ "exec" ], "/etc/mosdns/config_custom.yaml": [ "read" ], "/etc/mosdns/rule/*": [ "read" ], + "/tmp/dhcp.leases": [ "read" ], + "/tmp/hosts/*": [ "read" ], + "/tmp/odhcpd.leases": [ "read" ], "/usr/bin/mosdns": [ "exec" ], "/usr/share/mosdns/mosdns.uc": [ "exec" ], "/var/log/*": [ "read" ], @@ -19,6 +23,7 @@ "clear_query_logs", "flush_cache", "get_history", + "get_leases", "get_logs", "get_stats", "get_top", diff --git a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns index 10d61532..0b32e836 100644 --- a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns +++ b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns @@ -88,6 +88,91 @@ function call_mosdns_api(endpoint, method) { } } +function get_client_leases() { + let leases = {}; + + let add_lease = function(ip, name) { + if (!ip || !name) return; + ip = replace(ip, /^\s+|\s+$/g, ''); + name = replace(name, /^\s+|\s+$/g, ''); + if (ip == "" || name == "" || name == "*" || name == "unknown") return; + + ip = replace(ip, /\/[0-9]+$/, ''); + ip = replace(ip, /^::ffff:/i, ''); + + leases[ip] = name; + try { + if (type(lc) == 'function') { + leases[lc(ip)] = name; + } + } catch (e) {} + }; + + let parse_hosts_file = function(filepath) { + try { + if (stat(filepath)) { + let content = readfile(filepath); + if (content) { + let lines = split(content, '\n'); + for (let i = 0; i < length(lines); i++) { + let line = replace(lines[i], /^\s+|\s+$/g, ''); + if (line == "" || match(line, /^#/)) continue; + let parts = split(line, /[ \t]+/); + if (length(parts) >= 2) { + add_lease(parts[0], parts[1]); + } + } + } + } + } catch (e) {} + }; + + try { + if (stat('/tmp/dhcp.leases')) { + let content = readfile('/tmp/dhcp.leases'); + if (content) { + let lines = split(content, '\n'); + for (let i = 0; i < length(lines); i++) { + let line = replace(lines[i], /^\s+|\s+$/g, ''); + if (line == "" || match(line, /^#/)) continue; + let parts = split(line, /[ \t]+/); + if (length(parts) >= 4) { + add_lease(parts[2], parts[3]); + } + } + } + } + } catch (e) {} + + parse_hosts_file('/tmp/hosts/odhcpd'); + + parse_hosts_file('/tmp/hosts/dhcp'); + + try { + if (stat('/tmp/odhcpd.leases')) { + let content = readfile('/tmp/odhcpd.leases'); + if (content) { + let lines = split(content, '\n'); + for (let i = 0; i < length(lines); i++) { + let line = replace(lines[i], /^\s+|\s+$/g, ''); + if (line == "" || match(line, /^#/)) continue; + let parts = split(line, /[ \t]+/); + if (length(parts) >= 6) { + let host = parts[3]; + for (let j = 5; j < length(parts); j++) { + add_lease(parts[j], host); + } + } + } + } + } + } catch (e) {} + + parse_hosts_file('/etc/hosts'); + + return leases; +} + const methods = { get_stats: { call: function() { @@ -107,7 +192,17 @@ const methods = { args: { limit: 10 }, call: function(request) { let limit = request?.args?.limit || 10; - return call_mosdns_api(`/api/v1/top?limit=${limit}`, 'GET'); + let res = call_mosdns_api(`/api/v1/top?limit=${limit}`, 'GET'); + if (type(res) == 'object' && !res.error) { + res.leases = get_client_leases(); + } + return res; + } + }, + + get_leases: { + call: function() { + return { leases: get_client_leases() }; } }, diff --git a/mosdns/Makefile b/mosdns/Makefile index 84ad6296..3d8374d3 100644 --- a/mosdns/Makefile +++ b/mosdns/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=mosdns PKG_VERSION:=5.3.4 -PKG_RELEASE:=20 +PKG_RELEASE:=21 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/IrineSistiana/mosdns/tar.gz/v$(PKG_VERSION)? diff --git a/mosdns/patches/223-feat-stats_api-support-real-time-QPS-in-stats-endpoi.patch b/mosdns/patches/223-feat-stats_api-support-real-time-QPS-in-stats-endpoi.patch new file mode 100644 index 00000000..881ad138 --- /dev/null +++ b/mosdns/patches/223-feat-stats_api-support-real-time-QPS-in-stats-endpoi.patch @@ -0,0 +1,136 @@ +From 6af46a12d9149422a9b9cac5ca3510c25ca7220f Mon Sep 17 00:00:00 2001 +From: sbwml +Date: Wed, 2 Sep 2026 23:08:38 +0800 +Subject: [PATCH 1/3] feat(stats_api): support real-time QPS in stats endpoint + +Signed-off-by: sbwml +--- + plugin/executable/stats_api/stats_api.go | 51 +++++++++++++++++++ + plugin/executable/stats_api/stats_api_test.go | 24 +++++++++ + 2 files changed, 75 insertions(+) + +--- a/plugin/executable/stats_api/stats_api.go ++++ b/plugin/executable/stats_api/stats_api.go +@@ -513,6 +513,7 @@ type StatsAPI struct { + blockedQueries atomic.Uint64 + cachedQueries atomic.Uint64 + totalLatencyUs atomic.Uint64 ++ currentQPS atomic.Uint64 + + updatedCount atomic.Uint64 + closeNotify chan struct{} +@@ -575,6 +576,7 @@ func NewStatsAPI(args *Args, logger *zap + s.logger.Error("failed to load stats dump", zap.Error(err)) + } + s.startDumpLoop() ++ s.startQPSLoop() + + if len(args.Listen) > 0 { + srv := &http.Server{ +@@ -641,6 +643,7 @@ func (s *StatsAPI) handleStats(w http.Re + blockedPct = math.Round(blockedPct*100) / 100 + cachedPct = math.Round(cachedPct*100) / 100 + avgLat = math.Round(avgLat*100) / 100 ++ qps := s.currentQPS.Load() + + resp := map[string]any{ + "total_queries": total, +@@ -649,6 +652,7 @@ func (s *StatsAPI) handleStats(w http.Re + "blocked_percentage": blockedPct, + "cached_percentage": cachedPct, + "avg_latency_ms": avgLat, ++ "qps": qps, + } + + w.Header().Set("Content-Type", "application/json") +@@ -878,6 +882,53 @@ func (s *StatsAPI) startDumpLoop() { + case <-s.closeNotify: + return + } ++ } ++ }() ++} ++ ++func (s *StatsAPI) startQPSLoop() { ++ go func() { ++ ticker := time.NewTicker(1 * time.Second) ++ defer ticker.Stop() ++ ++ const windowSize = 5 ++ history := make([]uint64, windowSize) ++ timestamps := make([]int64, windowSize) ++ lastTotal := s.totalQueries.Load() ++ ++ for { ++ select { ++ case now := <-ticker.C: ++ curTotal := s.totalQueries.Load() ++ var delta uint64 ++ if curTotal >= lastTotal { ++ delta = curTotal - lastTotal ++ } ++ lastTotal = curTotal ++ ++ nowSec := now.Unix() ++ idx := int(nowSec % windowSize) ++ history[idx] = delta ++ timestamps[idx] = nowSec ++ ++ var sum uint64 ++ validSecs := 0 ++ for i := 0; i < windowSize; i++ { ++ if timestamps[i] > 0 && nowSec-timestamps[i] < windowSize { ++ sum += history[i] ++ validSecs++ ++ } ++ } ++ ++ var qps uint64 ++ if validSecs > 0 { ++ qps = (sum + uint64(validSecs)/2) / uint64(validSecs) ++ } ++ s.currentQPS.Store(qps) ++ ++ case <-s.closeNotify: ++ return ++ } + } + }() + } +--- a/plugin/executable/stats_api/stats_api_test.go ++++ b/plugin/executable/stats_api/stats_api_test.go +@@ -281,6 +281,9 @@ func TestStatsAPIHTTPEndpoints(t *testin + if statsResp["total_queries"].(float64) != 1 { + t.Errorf("expected total_queries 1, got %v", statsResp["total_queries"]) + } ++ if _, ok := statsResp["qps"]; !ok { ++ t.Errorf("expected qps field in stats response") ++ } + + // Test GET /api/v1/history + reqHist := httptest.NewRequest(http.MethodGet, "/api/v1/history?points=24", nil) +@@ -546,3 +549,24 @@ func TestStatsAPICachedPercentageExclude + } + } + ++func TestStatsAPIQPS(t *testing.T) { ++ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop()) ++ defer s.Close() ++ ++ // Initially QPS is 0 ++ req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil) ++ w := httptest.NewRecorder() ++ s.Router().ServeHTTP(w, req) ++ ++ var statsResp map[string]any ++ if err := json.Unmarshal(w.Body.Bytes(), &statsResp); err != nil { ++ t.Fatalf("failed to unmarshal: %v", err) ++ } ++ if qps, ok := statsResp["qps"]; !ok { ++ t.Fatalf("expected qps field in stats response") ++ } else if qps.(float64) != 0 { ++ t.Errorf("expected initial qps 0, got %v", qps) ++ } ++} ++ ++ diff --git a/mosdns/patches/224-feat-stats_api-exclude-hosts-and-arbitrary-queries-f.patch b/mosdns/patches/224-feat-stats_api-exclude-hosts-and-arbitrary-queries-f.patch new file mode 100644 index 00000000..d982ed23 --- /dev/null +++ b/mosdns/patches/224-feat-stats_api-exclude-hosts-and-arbitrary-queries-f.patch @@ -0,0 +1,432 @@ +From aa3e90c717c234a3483716c81a349736b0d1b27c Mon Sep 17 00:00:00 2001 +From: sbwml +Date: Thu, 3 Sep 2026 22:34:10 +0800 +Subject: [PATCH 2/3] feat(stats_api): exclude hosts and arbitrary queries from + cache hit percentage + +Exclude queries resolved by hosts and arbitrary plugins from the cache +hit rate calculation (total - blocked - hosts - arbitrary), as locally +resolved queries bypass cache and should not skew the cache hit rate. + +Also track hosts_queries and arbitrary_queries in stats endpoint and +dump data. + +Signed-off-by: sbwml +--- + pkg/query_context/context.go | 22 +++ + plugin/executable/arbitrary/arbitrary.go | 1 + + plugin/executable/cache/cache.go | 2 + + plugin/executable/forward/forward.go | 2 + + plugin/executable/hosts/hosts.go | 1 + + plugin/executable/stats_api/stats_api.go | 81 +++++++--- + plugin/executable/stats_api/stats_api_test.go | 143 ++++++++++++++++++ + 7 files changed, 227 insertions(+), 25 deletions(-) + +--- a/pkg/query_context/context.go ++++ b/pkg/query_context/context.go +@@ -75,6 +75,8 @@ type Context struct { + UpstreamSelected *UpstreamLog + RuleHits []RuleHit + CacheState CacheLog ++ FromHosts bool ++ FromArbitrary bool + + // lazy init. + kv map[uint32]any +@@ -107,6 +109,16 @@ func (ctx *Context) SetCacheState(hit, l + } + } + ++func (ctx *Context) SetFromHosts() { ++ ctx.FromHosts = true ++ ctx.FromArbitrary = false ++} ++ ++func (ctx *Context) SetFromArbitrary() { ++ ctx.FromArbitrary = true ++ ctx.FromHosts = false ++} ++ + var contextUid atomic.Uint32 + + type ServerMeta = server.QueryMeta +@@ -241,6 +253,9 @@ func (ctx *Context) CopyTo(d *Context) * + } + d.upstreamOpt = ctx.upstreamOpt + ++ d.FromHosts = ctx.FromHosts ++ d.FromArbitrary = ctx.FromArbitrary ++ + d.kv = copyMap(ctx.kv) + d.marks = copyMap(ctx.marks) + return d +@@ -350,6 +365,13 @@ func (ctx *Context) MarshalLogObject(enc + return nil + })) + ++ if ctx.FromHosts { ++ encoder.AddBool("hosts", true) ++ } ++ if ctx.FromArbitrary { ++ encoder.AddBool("arbitrary", true) ++ } ++ + if r := ctx.resp; r != nil { + encoder.AddInt("rcode", r.Rcode) + encoder.AddInt("resp_size", r.Len()) +--- a/plugin/executable/arbitrary/arbitrary.go ++++ b/plugin/executable/arbitrary/arbitrary.go +@@ -99,6 +99,7 @@ func (a *Arbitrary) Exec(_ context.Conte + if inner := a.m.Load(); inner != nil { + if r := inner.Reply(qCtx.Q()); r != nil { + qCtx.SetResponse(r) ++ qCtx.SetFromArbitrary() + } + } + return nil +--- a/plugin/executable/cache/cache.go ++++ b/plugin/executable/cache/cache.go +@@ -234,6 +234,8 @@ func (c *Cache) Exec(ctx context.Context + c.hitTotal.Inc() + cachedResp.Id = q.Id // change msg id + qCtx.SetResponse(cachedResp) ++ qCtx.FromHosts = false ++ qCtx.FromArbitrary = false + if v, _, ok := c.backend.Get(key(msgKey)); ok && v != nil { + v.hitCount.Add(1) + ttl := int(v.expirationTime.Sub(v.storedTime).Seconds()) +--- a/plugin/executable/forward/forward.go ++++ b/plugin/executable/forward/forward.go +@@ -201,6 +201,8 @@ func (f *Forward) Exec(ctx context.Conte + return err + } + qCtx.SetResponse(r) ++ qCtx.FromHosts = false ++ qCtx.FromArbitrary = false + return nil + } + +--- a/plugin/executable/hosts/hosts.go ++++ b/plugin/executable/hosts/hosts.go +@@ -113,6 +113,7 @@ func (h *Hosts) Exec(_ context.Context, + r := inner.LookupMsg(qCtx.Q()) + if r != nil { + qCtx.SetResponse(r) ++ qCtx.SetFromHosts() + } + } + return nil +--- a/plugin/executable/stats_api/stats_api.go ++++ b/plugin/executable/stats_api/stats_api.go +@@ -487,18 +487,20 @@ func (h *HistoryStats) Import(points map + } + + type StatsDumpData struct { +- Version int `json:"version"` +- Timestamp int64 `json:"timestamp"` +- TotalQueries uint64 `json:"total_queries"` +- BlockedQueries uint64 `json:"blocked_queries"` +- CachedQueries uint64 `json:"cached_queries"` +- TotalLatencyUs uint64 `json:"total_latency_us"` +- TopDomains map[string]uint64 `json:"top_domains,omitempty"` +- TopClients map[string]uint64 `json:"top_clients,omitempty"` +- TopBlocked map[string]uint64 `json:"top_blocked,omitempty"` +- History map[int64]HistoryBucketData `json:"history,omitempty"` +- Logs []LogEntry `json:"logs,omitempty"` +- SeqID uint64 `json:"seq_id,omitempty"` ++ Version int `json:"version"` ++ Timestamp int64 `json:"timestamp"` ++ TotalQueries uint64 `json:"total_queries"` ++ BlockedQueries uint64 `json:"blocked_queries"` ++ CachedQueries uint64 `json:"cached_queries"` ++ HostsQueries uint64 `json:"hosts_queries,omitempty"` ++ ArbitraryQueries uint64 `json:"arbitrary_queries,omitempty"` ++ TotalLatencyUs uint64 `json:"total_latency_us"` ++ TopDomains map[string]uint64 `json:"top_domains,omitempty"` ++ TopClients map[string]uint64 `json:"top_clients,omitempty"` ++ TopBlocked map[string]uint64 `json:"top_blocked,omitempty"` ++ History map[int64]HistoryBucketData `json:"history,omitempty"` ++ Logs []LogEntry `json:"logs,omitempty"` ++ SeqID uint64 `json:"seq_id,omitempty"` + } + + type StatsAPI struct { +@@ -509,11 +511,13 @@ type StatsAPI struct { + topStats *TopStats + historyStats *HistoryStats + +- totalQueries atomic.Uint64 +- blockedQueries atomic.Uint64 +- cachedQueries atomic.Uint64 +- totalLatencyUs atomic.Uint64 +- currentQPS atomic.Uint64 ++ totalQueries atomic.Uint64 ++ blockedQueries atomic.Uint64 ++ cachedQueries atomic.Uint64 ++ hostsQueries atomic.Uint64 ++ arbitraryQueries atomic.Uint64 ++ totalLatencyUs atomic.Uint64 ++ currentQPS atomic.Uint64 + + updatedCount atomic.Uint64 + closeNotify chan struct{} +@@ -626,6 +630,8 @@ func (s *StatsAPI) handleStats(w http.Re + total := s.totalQueries.Load() + blocked := s.blockedQueries.Load() + cached := s.cachedQueries.Load() ++ hosts := s.hostsQueries.Load() ++ arbitrary := s.arbitraryQueries.Load() + latUs := s.totalLatencyUs.Load() + + var blockedPct, cachedPct, avgLat float64 +@@ -633,8 +639,9 @@ func (s *StatsAPI) handleStats(w http.Re + blockedPct = float64(blocked) / float64(total) * 100.0 + avgLat = (float64(latUs) / float64(total)) / 1000.0 + } +- if total > blocked { +- cachedPct = float64(cached) / float64(total-blocked) * 100.0 ++ excluded := blocked + hosts + arbitrary ++ if total > excluded { ++ cachedPct = float64(cached) / float64(total-excluded) * 100.0 + if cachedPct > 100.0 { + cachedPct = 100.0 + } +@@ -649,6 +656,8 @@ func (s *StatsAPI) handleStats(w http.Re + "total_queries": total, + "blocked_queries": blocked, + "cached_queries": cached, ++ "hosts_queries": hosts, ++ "arbitrary_queries": arbitrary, + "blocked_percentage": blockedPct, + "cached_percentage": cachedPct, + "avg_latency_ms": avgLat, +@@ -778,12 +787,14 @@ func (s *StatsAPI) writeDump(w io.Writer + gw.Name = statsDumpHeader + + data := StatsDumpData{ +- Version: 1, +- Timestamp: time.Now().Unix(), +- TotalQueries: s.totalQueries.Load(), +- BlockedQueries: s.blockedQueries.Load(), +- CachedQueries: s.cachedQueries.Load(), +- TotalLatencyUs: s.totalLatencyUs.Load(), ++ Version: 1, ++ Timestamp: time.Now().Unix(), ++ TotalQueries: s.totalQueries.Load(), ++ BlockedQueries: s.blockedQueries.Load(), ++ CachedQueries: s.cachedQueries.Load(), ++ HostsQueries: s.hostsQueries.Load(), ++ ArbitraryQueries: s.arbitraryQueries.Load(), ++ TotalLatencyUs: s.totalLatencyUs.Load(), + } + + data.TopDomains, data.TopClients, data.TopBlocked = s.topStats.Export() +@@ -817,6 +828,8 @@ func (s *StatsAPI) readDump(r io.Reader) + s.totalQueries.Store(data.TotalQueries) + s.blockedQueries.Store(data.BlockedQueries) + s.cachedQueries.Store(data.CachedQueries) ++ s.hostsQueries.Store(data.HostsQueries) ++ s.arbitraryQueries.Store(data.ArbitraryQueries) + s.totalLatencyUs.Store(data.TotalLatencyUs) + + s.topStats.Import(data.TopDomains, data.TopClients, data.TopBlocked) +@@ -1022,10 +1035,24 @@ func (s *StatsAPI) Exec(ctx context.Cont + s.blockedQueries.Add(1) + } + ++ isHosts := qCtx.FromHosts && !isBlocked && !isCached && qCtx.UpstreamSelected == nil ++ if isHosts { ++ s.hostsQueries.Add(1) ++ } ++ ++ isArbitrary := qCtx.FromArbitrary && !isBlocked && !isCached && qCtx.UpstreamSelected == nil ++ if isArbitrary { ++ s.arbitraryQueries.Add(1) ++ } ++ + // Extract Upstream information + var upstream string + if isCached { + upstream = "cache" ++ } else if isHosts { ++ upstream = "hosts" ++ } else if isArbitrary { ++ upstream = "arbitrary" + } else if u := qCtx.UpstreamSelected; u != nil { + if u.Addr != "" { + if !strings.Contains(u.Addr, "://") { +@@ -1083,6 +1110,10 @@ func (s *StatsAPI) Exec(ctx context.Cont + if rule == "" { + if isCached { + rule = "cache" ++ } else if isHosts { ++ rule = "hosts" ++ } else if isArbitrary { ++ rule = "arbitrary" + } else { + rule = "-" + } +--- a/plugin/executable/stats_api/stats_api_test.go ++++ b/plugin/executable/stats_api/stats_api_test.go +@@ -428,6 +428,12 @@ func TestStatsAPIPersistence(t *testing. + if i%3 == 0 { + s1.cachedQueries.Add(1) + } ++ if i%4 == 0 { ++ s1.hostsQueries.Add(1) ++ } ++ if i%5 == 0 { ++ s1.arbitraryQueries.Add(1) ++ } + s1.totalLatencyUs.Add(uint64(i * 5000)) + s1.topStats.Record(fmt.Sprintf("test%d.com.", i), "192.168.1.100", i%2 == 0) + s1.historyStats.Record(time.Now(), i%2 == 0, i%3 == 0) +@@ -479,6 +485,12 @@ func TestStatsAPIPersistence(t *testing. + if s2.cachedQueries.Load() != 3 { + t.Errorf("expected 3 cached queries after load, got %d", s2.cachedQueries.Load()) + } ++ if s2.hostsQueries.Load() != 2 { ++ t.Errorf("expected 2 hosts queries after load, got %d", s2.hostsQueries.Load()) ++ } ++ if s2.arbitraryQueries.Load() != 2 { ++ t.Errorf("expected 2 arbitrary queries after load, got %d", s2.arbitraryQueries.Load()) ++ } + + totalLogs, logs := s2.ringBuffer.QueryLogs(10, 0, "", "all") + if totalLogs != 10 || len(logs) != 10 { +@@ -569,4 +581,135 @@ func TestStatsAPIQPS(t *testing.T) { + } + } + ++func TestStatsAPICachedPercentageExcludesHostsAndArbitrary(t *testing.T) { ++ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop()) ++ router := s.Router() ++ ++ // 100 total queries: 20 blocked, 15 hosts, 15 arbitrary, 25 cached, 25 forwarded to upstream ++ // denominator = 100 - (20 + 15 + 15) = 50 ++ // cached_percentage = 25 / 50 = 50% ++ s.totalQueries.Store(100) ++ s.blockedQueries.Store(20) ++ s.hostsQueries.Store(15) ++ s.arbitraryQueries.Store(15) ++ s.cachedQueries.Store(25) ++ s.totalLatencyUs.Store(50000) ++ ++ req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil) ++ w := httptest.NewRecorder() ++ router.ServeHTTP(w, req) ++ ++ if w.Code != http.StatusOK { ++ t.Fatalf("expected HTTP 200, got %d", w.Code) ++ } ++ ++ var statsResp map[string]any ++ if err := json.Unmarshal(w.Body.Bytes(), &statsResp); err != nil { ++ t.Fatalf("failed to unmarshal stats response: %v", err) ++ } ++ ++ if hostsQueries := statsResp["hosts_queries"].(float64); hostsQueries != 15 { ++ t.Errorf("expected hosts_queries 15, got %v", hostsQueries) ++ } ++ if arbitraryQueries := statsResp["arbitrary_queries"].(float64); arbitraryQueries != 15 { ++ t.Errorf("expected arbitrary_queries 15, got %v", arbitraryQueries) ++ } ++ if blockedPct := statsResp["blocked_percentage"].(float64); blockedPct != 20.0 { ++ t.Errorf("expected blocked_percentage 20.0, got %v", blockedPct) ++ } ++ if cachedPct := statsResp["cached_percentage"].(float64); cachedPct != 50.0 { ++ t.Errorf("expected cached_percentage 50.0, got %v", cachedPct) ++ } ++} ++ ++func TestStatsAPIExecWithHostsAndArbitrary(t *testing.T) { ++ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop()) ++ ++ // Test 1: Normal hosts resolution ++ q1 := new(dns.Msg) ++ q1.SetQuestion("lan.local.", dns.TypeA) ++ qCtx1 := query_context.NewContext(q1) ++ ++ execHosts := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error { ++ resp := new(dns.Msg) ++ resp.SetReply(qCtx.Q()) ++ resp.Answer = append(resp.Answer, &dns.A{ ++ Hdr: dns.RR_Header{Name: "lan.local.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, ++ A: []byte{192, 168, 1, 10}, ++ }) ++ qCtx.SetResponse(resp) ++ qCtx.SetFromHosts() ++ return nil ++ }) ++ ++ walker1 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execHosts}}, nil) ++ if err := s.Exec(context.Background(), qCtx1, walker1); err != nil { ++ t.Fatalf("Exec failed: %v", err) ++ } ++ if s.hostsQueries.Load() != 1 { ++ t.Errorf("expected hostsQueries 1, got %d", s.hostsQueries.Load()) ++ } ++ _, logs1 := s.ringBuffer.QueryLogs(1, 0, "", "all") ++ if len(logs1) != 1 || logs1[0].Upstream != "hosts" { ++ t.Errorf("expected upstream 'hosts', got %+v", logs1) ++ } ++ ++ // Test 2: Hosts returning 0.0.0.0 (adblock) -> should be counted in blockedQueries, NOT hostsQueries ++ q2 := new(dns.Msg) ++ q2.SetQuestion("ad.example.com.", dns.TypeA) ++ qCtx2 := query_context.NewContext(q2) ++ ++ execHostsBlock := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error { ++ resp := new(dns.Msg) ++ resp.SetReply(qCtx.Q()) ++ resp.Answer = append(resp.Answer, &dns.A{ ++ Hdr: dns.RR_Header{Name: "ad.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, ++ A: []byte{0, 0, 0, 0}, ++ }) ++ qCtx.SetResponse(resp) ++ qCtx.SetFromHosts() ++ return nil ++ }) ++ ++ walker2 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execHostsBlock}}, nil) ++ if err := s.Exec(context.Background(), qCtx2, walker2); err != nil { ++ t.Fatalf("Exec failed: %v", err) ++ } ++ if s.blockedQueries.Load() != 1 { ++ t.Errorf("expected blockedQueries 1, got %d", s.blockedQueries.Load()) ++ } ++ if s.hostsQueries.Load() != 1 { // Should still be 1 (not incremented) ++ t.Errorf("expected hostsQueries to remain 1, got %d", s.hostsQueries.Load()) ++ } ++ ++ // Test 3: Arbitrary resolution ++ q3 := new(dns.Msg) ++ q3.SetQuestion("custom.zone.", dns.TypeA) ++ qCtx3 := query_context.NewContext(q3) ++ ++ execArbitrary := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error { ++ resp := new(dns.Msg) ++ resp.SetReply(qCtx.Q()) ++ resp.Answer = append(resp.Answer, &dns.A{ ++ Hdr: dns.RR_Header{Name: "custom.zone.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300}, ++ A: []byte{10, 0, 0, 1}, ++ }) ++ qCtx.SetResponse(resp) ++ qCtx.SetFromArbitrary() ++ return nil ++ }) ++ ++ walker3 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execArbitrary}}, nil) ++ if err := s.Exec(context.Background(), qCtx3, walker3); err != nil { ++ t.Fatalf("Exec failed: %v", err) ++ } ++ if s.arbitraryQueries.Load() != 1 { ++ t.Errorf("expected arbitraryQueries 1, got %d", s.arbitraryQueries.Load()) ++ } ++ _, logs3 := s.ringBuffer.QueryLogs(1, 0, "", "all") ++ if len(logs3) != 1 || logs3[0].Upstream != "arbitrary" { ++ t.Errorf("expected upstream 'arbitrary', got %+v", logs3) ++ } ++} ++ + diff --git a/mosdns/patches/225-fix-fallback-preserve-upstream-and-query-context-met.patch b/mosdns/patches/225-fix-fallback-preserve-upstream-and-query-context-met.patch new file mode 100644 index 00000000..5c7f3f1b --- /dev/null +++ b/mosdns/patches/225-fix-fallback-preserve-upstream-and-query-context-met.patch @@ -0,0 +1,123 @@ +From f178e9542a7effb74e8feb8b535197b3131ff05a Mon Sep 17 00:00:00 2001 +From: sbwml +Date: Thu, 3 Sep 2026 22:42:53 +0800 +Subject: [PATCH 3/3] fix(fallback): preserve upstream and query context + metadata from winning branch + +Signed-off-by: sbwml +--- + pkg/query_context/context.go | 41 +++++++++++++++++++ + .../executable/sequence/fallback/fallback.go | 22 +++++++--- + 2 files changed, 57 insertions(+), 6 deletions(-) + +--- a/pkg/query_context/context.go ++++ b/pkg/query_context/context.go +@@ -253,6 +253,15 @@ func (ctx *Context) CopyTo(d *Context) * + } + d.upstreamOpt = ctx.upstreamOpt + ++ if ctx.UpstreamSelected != nil { ++ u := *ctx.UpstreamSelected ++ d.UpstreamSelected = &u ++ } ++ if len(ctx.RuleHits) > 0 { ++ d.RuleHits = make([]RuleHit, len(ctx.RuleHits)) ++ copy(d.RuleHits, ctx.RuleHits) ++ } ++ d.CacheState = ctx.CacheState + d.FromHosts = ctx.FromHosts + d.FromArbitrary = ctx.FromArbitrary + +@@ -261,6 +270,38 @@ func (ctx *Context) CopyTo(d *Context) * + return d + } + ++// CopyMetadataFrom copies execution metadata and log details from src Context. ++func (ctx *Context) CopyMetadataFrom(src *Context) { ++ if src == nil { ++ return ++ } ++ ctx.upstreamOpt = src.upstreamOpt ++ ctx.UpstreamSelected = src.UpstreamSelected ++ if len(src.RuleHits) > 0 { ++ ctx.RuleHits = src.RuleHits ++ } ++ ctx.CacheState = src.CacheState ++ ctx.FromHosts = src.FromHosts ++ ctx.FromArbitrary = src.FromArbitrary ++ ++ if src.marks != nil { ++ if ctx.marks == nil { ++ ctx.marks = make(map[uint32]struct{}) ++ } ++ for k, v := range src.marks { ++ ctx.marks[k] = v ++ } ++ } ++ if src.kv != nil { ++ if ctx.kv == nil { ++ ctx.kv = make(map[uint32]any) ++ } ++ for k, v := range src.kv { ++ ctx.kv[k] = v ++ } ++ } ++} ++ + // StoreValue stores any v in to this Context + // k MUST from RegKey. + func (ctx *Context) StoreValue(k uint32, v any) { +--- a/plugin/executable/sequence/fallback/fallback.go ++++ b/plugin/executable/sequence/fallback/fallback.go +@@ -107,8 +107,13 @@ func (f *fallback) Exec(ctx context.Cont + return f.doFallback(ctx, qCtx) + } + ++type fallbackResult struct { ++ r *dns.Msg ++ qCtx *query_context.Context ++} ++ + func (f *fallback) doFallback(ctx context.Context, qCtx *query_context.Context) error { +- respChan := make(chan *dns.Msg, 2) // resp could be nil. ++ respChan := make(chan *fallbackResult, 2) // resp could be nil. + primFailed := make(chan struct{}) + primDone := make(chan struct{}) + +@@ -129,7 +134,7 @@ func (f *fallback) doFallback(ctx contex + respChan <- nil + } else { + close(primDone) +- respChan <- r ++ respChan <- &fallbackResult{r: r, qCtx: qCtx} + } + }() + +@@ -167,18 +172,23 @@ func (f *fallback) doFallback(ctx contex + case <-timer.C: // or timed out. + } + } +- respChan <- r ++ if r == nil { ++ respChan <- nil ++ } else { ++ respChan <- &fallbackResult{r: r, qCtx: qCtx} ++ } + }() + + for i := 0; i < 2; i++ { + select { + case <-ctx.Done(): + return context.Cause(ctx) +- case r := <-respChan: +- if r == nil { // One of goroutines finished but failed. ++ case res := <-respChan: ++ if res == nil || res.r == nil { // One of goroutines finished but failed. + continue + } +- qCtx.SetResponse(r) ++ qCtx.SetResponse(res.r) ++ qCtx.CopyMetadataFrom(res.qCtx) + return nil + } + }