diff --git a/doc/lucky1.png b/doc/lucky1.png deleted file mode 100644 index a5b41b6e..00000000 Binary files a/doc/lucky1.png and /dev/null differ diff --git a/doc/lucky2.png b/doc/lucky2.png deleted file mode 100644 index ba4c026f..00000000 Binary files a/doc/lucky2.png and /dev/null differ diff --git a/doc/lucky3.png b/doc/lucky3.png deleted file mode 100644 index 6fa7982f..00000000 Binary files a/doc/lucky3.png and /dev/null differ diff --git a/doc/taskplan1.png b/doc/taskplan1.png new file mode 100644 index 00000000..1f8db7be Binary files /dev/null and b/doc/taskplan1.png differ diff --git a/doc/taskplan2.png b/doc/taskplan2.png new file mode 100644 index 00000000..edb45d6d Binary files /dev/null and b/doc/taskplan2.png differ diff --git a/doc/taskplan3.png b/doc/taskplan3.png new file mode 100644 index 00000000..d4794702 Binary files /dev/null and b/doc/taskplan3.png differ diff --git a/doc/view.png b/doc/view.png new file mode 100644 index 00000000..169e112f Binary files /dev/null and b/doc/view.png differ diff --git a/doc/view2.png b/doc/view2.png new file mode 100644 index 00000000..9058c632 Binary files /dev/null and b/doc/view2.png differ diff --git a/gn/Makefile b/gn/Makefile index 7434589e..4922bad3 100644 --- a/gn/Makefile +++ b/gn/Makefile @@ -9,9 +9,9 @@ PKG_RELEASE:=1 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git -PKG_SOURCE_DATE:=2026-08-19 -PKG_SOURCE_VERSION:=58933a7cdbc90f70f2381f0c72e76d29be1d43a9 -PKG_MIRROR_HASH:=b4fd70abf2fe8e3017ac3c4e025fd25cd4364dbe2470035b3daf58a129f9f54f +PKG_SOURCE_DATE:=2026-08-28 +PKG_SOURCE_VERSION:=c1b663788ed1ecf2ffd53781c98ae2291ee002cf +PKG_MIRROR_HASH:=aa635f1ce4b0cf2b7776dc44ad28671eb0fb79276097addca503ce4708ccdc5d PKG_LICENSE:=BSD 3-Clause PKG_LICENSE_FILES:=LICENSE diff --git a/gn/src/out/last_commit_position.h b/gn/src/out/last_commit_position.h index 90717455..39f96381 100644 --- a/gn/src/out/last_commit_position.h +++ b/gn/src/out/last_commit_position.h @@ -3,7 +3,7 @@ #ifndef OUT_LAST_COMMIT_POSITION_H_ #define OUT_LAST_COMMIT_POSITION_H_ -#define LAST_COMMIT_POSITION_NUM 2528 -#define LAST_COMMIT_POSITION "2528 (58933a7cdbc9)" +#define LAST_COMMIT_POSITION_NUM 2539 +#define LAST_COMMIT_POSITION "2539 (c1b663788ed1)" #endif // OUT_LAST_COMMIT_POSITION_H_ diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile index 10ac3d18..61ec9b6c 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -1,12 +1,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns -PKG_VERSION:=1.7.9 +PKG_VERSION:=1.7.10 PKG_RELEASE:=1 LUCI_TITLE:=LuCI Support for mosdns LUCI_PKGARCH:=all -LUCI_DEPENDS:=+mosdns +curl +v2ray-geoip +v2ray-geosite +geo2txt +ucode +LUCI_DEPENDS:=+mosdns +uclient-fetch +v2ray-geoip +v2ray-geosite +geo2txt +ucode PKG_MAINTAINER:=sbwml @@ -15,6 +15,7 @@ define Package/$(PKG_NAME)/conffiles /etc/mosdns/cache.dump /etc/mosdns/config_custom.yaml /etc/mosdns/rule +/etc/mosdns/stats.dump endef include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js index 430b8e89..443ed170 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js @@ -163,7 +163,7 @@ return view.extend({ o.value('info', _('Info')); o.value('warn', _('Warning')); o.value('error', _('Error')); - o.default = 'info'; + o.default = 'error'; o.depends('configfile', '/var/etc/mosdns.json'); o = s.taboption('basic', form.Value, 'log_file', _('Log File')); @@ -426,6 +426,29 @@ return view.extend({ o.default = 52001; o.depends('configfile', '/var/etc/mosdns.json'); + o = s.taboption('api', form.Flag, 'stats_collector', _('Enable Stats Collector')); + o.rmempty = false; + o.default = o.enabled; + o.depends('configfile', '/var/etc/mosdns.json'); + + o = s.taboption('api', form.Value, 'stats_capacity', _('Ring Buffer Capacity'), + _('Query log ring buffer capacity (FIFO overwrite, default 2000, larger values consume more memory)')); + o.datatype = 'and(uinteger,min(1))'; + o.default = 2000; + o.depends('stats_collector', '1'); + + o = s.taboption('api', form.Flag, 'stats_dump_file', _('Stats Dump'), + _('Save query statistics and logs locally and reload on next startup.')); + o.rmempty = false; + o.default = false; + o.depends('stats_collector', '1'); + + o = s.taboption('api', form.Value, 'stats_dump_interval', + _('Auto Save Stats Interval')); + o.datatype = 'and(uinteger,min(1))'; + o.default = 600; + o.depends('stats_dump_file', '1'); + o = s.taboption('api', form.Button, '_flush_cache', null, _('Flushing DNS Cache will clear any IP addresses or DNS records from MosDNS cache.')); o.title = ' '; 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 new file mode 100644 index 00000000..9b398094 --- /dev/null +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js @@ -0,0 +1,722 @@ +'use strict'; +'require dom'; +'require poll'; +'require rpc'; +'require ui'; +'require view'; + +const callGetStats = rpc.declare({ + object: 'luci.mosdns', + method: 'get_stats', + expect: { '': {} } +}); + +const callGetHistory = rpc.declare({ + object: 'luci.mosdns', + method: 'get_history', + params: ['points'], + expect: { '': {} } +}); + +const callGetTop = rpc.declare({ + object: 'luci.mosdns', + method: 'get_top', + params: ['limit'], + expect: { '': {} } +}); + +const callGetLogs = rpc.declare({ + object: 'luci.mosdns', + method: 'get_logs', + params: ['limit', 'offset', 'search', 'filter'], + expect: { '': {} } +}); + +const callClearQueryLogs = rpc.declare({ + object: 'luci.mosdns', + method: 'clear_query_logs', + expect: { '': {} } +}); + +let filterVal = 'all'; +let searchVal = ''; +let pageIdx = 0; +const PAGE_SIZE = 20; +let isUserPaused = false; + +let nodeStats; +let nodeTop; +let nodeLogs; +let autoStatusBadge; + +const cleanIP = ip => { + if (!ip) return '-'; + return ip.replace(/^::ffff:/i, ''); +}; + +const injectStyles = () => { + if (document.getElementById('mosdns-statistics-styles')) + return; + + /* HTML Styles provided by DeepSeek Chat */ + const css = [ + '.mosdns-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem; margin-bottom: 1.25rem; }', + '.mosdns-rankings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; }', + '.mosdns-stat-card { background: var(--cbi-section-bg, #fff); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 1rem 1.2rem; box-shadow: 0 2px 6px rgba(0,0,0,0.03); display: flex; flex-direction: column; justify-content: space-between; position: relative; overflow: hidden; }', + '.mosdns-stat-card .title-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; font-size: 0.85rem; opacity: 0.75; font-weight: 500; }', + '.mosdns-stat-card .metric-val { font-size: 1.85rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; }', + '.mosdns-stat-card .subtext { font-size: 0.8rem; opacity: 0.6; margin-top: 0.4rem; }', + '.mosdns-sparkline-wrap { margin-top: 0.5rem; height: 44px; position: relative; overflow: visible; display: flex; align-items: flex-end; }', + '.mosdns-sparkline { width: 100%; height: 100%; display: block; overflow: visible; }', + '.mosdns-sparkline-tooltip { position: absolute; pointer-events: none; z-index: 20; padding: 0.25rem 0.5rem; border-radius: 5px; background: var(--cbi-section-bg, #fff); border: 1px solid rgba(0,0,0,0.12); box-shadow: 0 3px 10px rgba(0,0,0,0.12); line-height: 1.25; text-align: center; white-space: nowrap; transition: opacity 0.15s ease; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }', + '.mosdns-rank-panel { background: var(--cbi-section-bg, #fff); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 1rem 1.1rem; box-shadow: 0 2px 6px rgba(0,0,0,0.03); }', + '.mosdns-rank-panel h4 { margin: 0 0 0.85rem 0; font-size: 0.95rem; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }', + '.mosdns-rank-item { position: relative; overflow: hidden; border-radius: 6px; padding: 0.35rem 0.65rem; display: flex; justify-content: space-between; align-items: center; background: rgba(125,125,125,0.03); border: 1px solid rgba(125,125,125,0.08); margin-bottom: 0.35rem; }', + '.mosdns-rank-bar { position: absolute; left: 0; top: 0; bottom: 0; opacity: 0.15; pointer-events: none; transition: width .3s ease; }', + '.mosdns-badge { display: inline-block; padding: 0.15em 0.55em; font-size: 0.75rem; font-weight: 600; border-radius: 4px; line-height: 1.25; text-align: center; white-space: nowrap; box-sizing: border-box; }', + '.mosdns-status-badge { min-width: 68px; }', + '.badge-danger { background: rgba(239, 68, 68, 0.12); color: #dc2626; border: 1px solid rgba(239, 68, 68, 0.25); }', + '.badge-teal { background: rgba(16, 185, 129, 0.12); color: #059669; border: 1px solid rgba(16, 185, 129, 0.25); }', + '.badge-primary { background: rgba(59, 130, 246, 0.12); color: #2563eb; border: 1px solid rgba(59, 130, 246, 0.25); }', + '.badge-neutral { background: rgba(107, 114, 128, 0.12); color: #4b5563; border: 1px solid rgba(107, 114, 128, 0.25); }', + '.badge-qtype { font-family: monospace; font-size: 0.72rem; padding: 0.1em 0.4em; background: rgba(125,125,125,0.1); border-radius: 3px; opacity: 0.8; margin-left: 0.4rem; }', + '.badge-pulse { animation: pulse 2s infinite; }', + '.dns-latency-fastest { color: #10b981; font-weight: 600; }', + '.dns-latency-fast { color: #059669; font-weight: 600; }', + '.dns-latency-normal { color: #3b82f6; font-weight: 600; }', + '.dns-latency-slow { color: #d97706; font-weight: 600; }', + '.dns-latency-slower { color: #ea580c; font-weight: 600; }', + '.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-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-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-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; }', + '@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); }', + ' .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); }', + ' .dns-latency-fastest { color: #34d399; }', + ' .dns-latency-normal { color: #60a5fa; }', + ' .dns-latency-timeout { color: #f87171; }', + '}' + ].join('\n'); + + document.head.appendChild(E('style', { id: 'mosdns-statistics-styles' }, css)); +}; + +const debounce = (fn, delay = 300) => { + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +}; + +const formatTimestamp = iso => { + if (!iso) return '-'; + const d = new Date(iso); + return isNaN(d) ? iso : d.toTimeString().slice(0, 8); +}; + +const getLatencyClass = elapsedMs => { + const val = parseFloat(elapsedMs) || 0; + if (val < 5) return 'dns-latency-fastest'; + if (val < 20) return 'dns-latency-fast'; + if (val < 50) return 'dns-latency-normal'; + if (val < 100) return 'dns-latency-slow'; + if (val < 300) return 'dns-latency-slower'; + return 'dns-latency-timeout'; +}; + +const updateLiveStatusBadge = () => { + if (!autoStatusBadge) return; + if (pageIdx === 0 && !searchVal && !isUserPaused) { + dom.content(autoStatusBadge, [ + E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live Auto-refresh')) + ]); + } else { + dom.content(autoStatusBadge, [ + E('span', { class: 'mosdns-badge badge-neutral' }, _('❚❚ Paused (Page %d)').format(pageIdx + 1)) + ]); + } +}; + +const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => { + const width = 300; + const height = 44; + const padTop = 4; + const padBottom = 2; + const drawHeight = height - padTop - padBottom; + + let items = (dataItems && dataItems.length > 0) ? dataItems : []; + if (!items.length) { + items = new Array(24).fill(0).map(() => ({ time: '', val: 0 })); + } + if (items.length < 2) { + items = [items[0] || { time: '', val: 0 }, items[0] || { time: '', val: 0 }]; + } + + const vals = items.map(i => i.val || 0); + const maxVal = maxScale || Math.max(...vals, 1); + const len = items.length; + + const coords = items.map((item, idx) => { + const x = (idx / (len - 1)) * width; + const y = height - padBottom - ((item.val || 0) / maxVal) * drawHeight; + return { x, y, time: item.time, val: item.val || 0 }; + }); + + let pathD = 'M ' + coords[0].x.toFixed(1) + ',' + coords[0].y.toFixed(1); + for (let i = 0; i < coords.length - 1; i++) { + const p0 = coords[i === 0 ? 0 : i - 1]; + const p1 = coords[i]; + const p2 = coords[i + 1]; + const p3 = coords[i + 2 < coords.length ? i + 2 : i + 1]; + + const cp1x = p1.x + (p2.x - p0.x) / 6; + const cp1y = p1.y + (p2.y - p0.y) / 6; + const cp2x = p2.x - (p3.x - p1.x) / 6; + const cp2y = p2.y - (p3.y - p1.y) / 6; + + pathD += ' C ' + cp1x.toFixed(1) + ',' + cp1y.toFixed(1) + ' ' + cp2x.toFixed(1) + ',' + cp2y.toFixed(1) + ' ' + p2.x.toFixed(1) + ',' + p2.y.toFixed(1); + } + + const areaD = pathD + ' L ' + width + ',' + height + ' L 0,' + height + ' Z'; + + const svgContainer = E('div', { class: 'mosdns-sparkline-wrap' }); + svgContainer.innerHTML = + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''; + + const hoverGroup = svgContainer.querySelector('.spark-hover-group'); + const hoverLine = svgContainer.querySelector('.spark-hover-line'); + const hoverDot = svgContainer.querySelector('.spark-hover-dot'); + const hitbox = svgContainer.querySelector('.spark-hover-hitbox'); + const tooltip = svgContainer.querySelector('.mosdns-sparkline-tooltip'); + + const onMove = e => { + const rect = svgContainer.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const ratio = Math.max(0, Math.min(1, mouseX / rect.width)); + const idx = Math.round(ratio * (len - 1)); + const coord = coords[idx]; + if (!coord) return; + + hoverLine.setAttribute('x1', coord.x); + hoverLine.setAttribute('x2', coord.x); + hoverDot.setAttribute('cx', coord.x); + hoverDot.setAttribute('cy', coord.y); + hoverGroup.style.display = 'block'; + + let timeStr = '-'; + if (coord.time) { + const d = new Date(coord.time); + if (!isNaN(d)) { + const hh = String(d.getHours()).padStart(2, '0'); + timeStr = hh + ':00'; + } else { + timeStr = coord.time.slice(11, 16) || coord.time; + } + } + + tooltip.innerHTML = + '
' + coord.val.toLocaleString() + '
' + + '
' + timeStr + '
'; + tooltip.style.display = 'block'; + + const tooltipX = (coord.x / width) * rect.width; + if (ratio > 0.65) { + tooltip.style.left = 'auto'; + tooltip.style.right = (rect.width - tooltipX + 8) + 'px'; + } else { + tooltip.style.left = (tooltipX + 8) + 'px'; + tooltip.style.right = 'auto'; + } + tooltip.style.top = '-6px'; + }; + + const onLeave = () => { + hoverGroup.style.display = 'none'; + tooltip.style.display = 'none'; + }; + + hitbox.addEventListener('mousemove', onMove); + hitbox.addEventListener('mouseleave', onLeave); + + return svgContainer; +}; + +const renderOverviewStats = (stats, historyData) => { + if (!stats || stats.error) { + return E('div', { class: 'alert-message warning' }, + _('MosDNS API is unreachable. Please ensure MosDNS is running and stats_api plugin is enabled.')); + } + + const { + total_queries: total = 0, + blocked_queries: blocked = 0, + cached_queries: cached = 0, + blocked_percentage: blocked_pct = 0, + cached_percentage: cached_pct = 0, + avg_latency_ms: avg_ms = 0 + } = stats; + + const points = historyData?.points || []; + const totalItems = points.map(p => ({ time: p.time, val: Number(p.total) || 0 })); + const blockedItems = points.map(p => ({ time: p.time, val: Number(p.blocked) || 0 })); + const cachedItems = points.map(p => ({ time: p.time, val: Number(p.cached) || 0 })); + + const baseMax = Math.max(...totalItems.map(i => i.val), 1); + + return E('div', { class: 'mosdns-grid' }, [ + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('DNS Queries Total')), + E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live')) + ]), + E('div', { class: 'metric-val' }, total.toLocaleString()), + E('div', { class: 'subtext' }, _('Avg Processing') + ': ' + avg_ms + ' ms') + ]), + createSparklineSVG(totalItems, '#3b82f6', 'spark-grad-total', baseMax) + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Blocked by Filters')), + E('span', { class: 'mosdns-badge badge-danger' }, blocked_pct + '%') + ]), + E('div', { class: 'metric-val', style: 'color: #dc2626;' }, blocked.toLocaleString()) + ]), + createSparklineSVG(blockedItems, '#dc2626', 'spark-grad-blocked', baseMax) + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Cached Queries')), + E('span', { class: 'mosdns-badge badge-teal' }, cached_pct + '%') + ]), + E('div', { class: 'metric-val', style: 'color: #059669;' }, cached.toLocaleString()) + ]), + createSparklineSVG(cachedItems, '#059669', 'spark-grad-cached', baseMax) + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Average Processing Time')), + E('span', { class: 'mosdns-badge badge-primary' }, _('Latency')) + ]), + E('div', { class: 'metric-val', style: 'color: #2563eb;' }, avg_ms + ' ms'), + E('div', { class: 'subtext' }, _('Per-query speed')) + ]) + ]) + ]); +}; + +const renderTopRankings = topData => { + if (!topData || topData.error) return E('div', {}); + + const { top_blocked = [], top_domains = [], top_clients = [] } = topData; + + const renderList = (items, key, color, isClient = false) => { + if (!items || !items.length) { + return E('div', { style: 'padding: 1.5rem; text-align: center; opacity: 0.5; font-size: 0.85rem;' }, _('No data available')); + } + 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); + const cnt = item.count || 0; + const pct = Math.round((cnt / maxCount) * 100); + + return E('div', { class: 'mosdns-rank-item' }, [ + E('div', { class: 'mosdns-rank-bar', style: 'width: ' + pct + '%; background-color: ' + color + ';' }), + 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), + E('span', { class: 'mosdns-badge badge-neutral mosdns-mono', style: 'z-index: 1;' }, cnt.toLocaleString()) + ]); + }) + ); + }; + + return E('div', { class: 'mosdns-rankings-grid' }, [ + E('div', { class: 'mosdns-rank-panel' }, [ + E('h4', { style: 'color: #2563eb;' }, [ + E('span', {}, _('Top Queried Domains')), + E('span', { class: 'mosdns-badge badge-primary' }, top_domains.length) + ]), + renderList(top_domains, 'domain', '#2563eb') + ]), + E('div', { class: 'mosdns-rank-panel' }, [ + E('h4', { style: 'color: #dc2626;' }, [ + E('span', {}, _('Top Blocked Domains')), + E('span', { class: 'mosdns-badge badge-danger' }, top_blocked.length) + ]), + renderList(top_blocked, 'domain', '#dc2626') + ]), + E('div', { class: 'mosdns-rank-panel' }, [ + E('h4', { style: 'color: #059669;' }, [ + E('span', {}, _('Top Clients')), + E('span', { class: 'mosdns-badge badge-teal' }, top_clients.length) + ]), + renderList(top_clients, 'client_ip', '#059669', true) + ]) + ]); +}; + +const showLogDetailsModal = item => { + let statusBadge; + if (item.is_blocked) { + statusBadge = E('span', { class: 'mosdns-badge badge-danger mosdns-status-badge' }, 'BLOCKED'); + } else if (item.is_cached) { + statusBadge = E('span', { class: 'mosdns-badge badge-teal mosdns-status-badge' }, 'CACHED'); + } else if (item.status === 'NOERROR') { + statusBadge = E('span', { class: 'mosdns-badge badge-primary mosdns-status-badge' }, 'NOERROR'); + } else { + statusBadge = E('span', { class: 'mosdns-badge badge-neutral mosdns-status-badge' }, item.status || 'NOERROR'); + } + + const answersCount = (item.answers && item.answers.length) || 0; + let answersContent; + if (answersCount > 0) { + answersContent = E('div', { class: 'mosdns-answers-list' }, + item.answers.map(a => E('div', { class: 'mosdns-answer-row' }, [ + E('div', { style: 'display: flex; align-items: center; gap: 0.5rem; overflow: hidden; flex: 1;' }, [ + E('span', { class: 'badge-qtype', style: 'margin: 0;' }, a.type || 'A'), + E('span', { class: 'mosdns-mono mosdns-answer-data', title: a.data }, a.data) + ]), + E('span', { class: 'mosdns-badge badge-neutral mosdns-mono mosdns-answer-ttl' }, 'TTL ' + a.ttl + 's') + ])) + ); + } else { + answersContent = E('div', { + style: 'text-align: center; padding: 1.25rem; background: rgba(125,125,125,0.03); border: 1px dashed rgba(125,125,125,0.15); border-radius: 6px; opacity: 0.6; font-size: 0.85rem;' + }, _('No DNS answer records returned.')); + } + + const body = E('div', { style: 'padding: 0.25rem 0;' }, [ + E('div', { class: 'mosdns-modal-header' }, [ + E('div', { class: 'mosdns-modal-domain' }, [ + E('span', { class: 'mosdns-mono' }, item.domain || '-'), + E('span', { class: 'badge-qtype' }, item.qtype || 'A') + ]), + E('div', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + statusBadge, + E('span', { class: 'mosdns-mono ' + getLatencyClass(item.elapsed_ms), style: 'font-size: 0.85rem;' }, item.elapsed_ms + ' ms') + ]) + ]), + + 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: 'mosdns-modal-meta-item' }, [ + E('div', { class: 'meta-label' }, _('Time')), + E('div', { class: 'meta-val mosdns-mono' }, formatTimestamp(item.timestamp) + (item.timestamp ? ' (' + item.timestamp.slice(0, 10) + ')' : '')) + ]), + E('div', { class: 'mosdns-modal-meta-item' }, [ + E('div', { class: 'meta-label' }, _('Upstream')), + E('div', { class: 'meta-val mosdns-mono', style: 'word-break: break-all;' }, item.upstream || '-') + ]), + E('div', { class: 'mosdns-modal-meta-item' }, [ + E('div', { class: 'meta-label' }, _('Rule Hit')), + E('div', { class: 'meta-val mosdns-mono', style: 'word-break: break-all;' }, item.rule || '-') + ]) + ]), + + E('div', { class: 'mosdns-modal-section-title' }, [ + E('span', {}, _('Answers')), + E('span', { class: 'mosdns-badge badge-neutral' }, answersCount) + ]), + answersContent + ]); + + ui.showModal(_('Query Log Details'), [ + body, + E('div', { class: 'right', style: 'margin-top: 1.25rem;' }, [ + E('button', { + class: 'btn cbi-button cbi-button-action', + click: ui.hideModal + }, _('Close')) + ]) + ]); +}; + +const renderLogsTable = logsData => { + const { total = 0, items = [] } = logsData || {}; + const totalPages = Math.ceil(total / PAGE_SIZE) || 1; + + const rows = items.map(item => { + let statusBadge; + if (item.is_blocked) { + statusBadge = E('span', { class: 'mosdns-badge badge-danger mosdns-status-badge' }, 'BLOCKED'); + } else if (item.is_cached) { + statusBadge = E('span', { class: 'mosdns-badge badge-teal mosdns-status-badge' }, 'CACHED'); + } else if (item.status === 'NOERROR') { + statusBadge = E('span', { class: 'mosdns-badge badge-primary mosdns-status-badge' }, 'NOERROR'); + } else { + statusBadge = E('span', { class: 'mosdns-badge badge-neutral mosdns-status-badge' }, item.status || 'NOERROR'); + } + + const answersText = (item.answers && item.answers.length > 0) + ? 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 || '-' }, [ + 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 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) + }, answersText), + E('td', { class: 'td 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' }, [ + E('td', { class: 'td', colspan: 6, style: 'text-align: center; opacity: 0.5; padding: 2rem;' }, _('No query log entries found.')) + ])); + } + + 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')) + ]), + ...rows + ]), + + E('div', { style: 'display: flex; justify-content: space-between; align-items: center; margin-top: 0.75rem;' }, [ + E('span', { style: 'font-size: 0.85rem; opacity: 0.7;' }, _('Page %d / %d (%d entries)').format(pageIdx + 1, totalPages, total)), + E('div', { style: 'display: flex; gap: 0.5rem;' }, [ + E('button', { + class: 'btn cbi-button cbi-button-action', + disabled: pageIdx === 0 ? 'disabled' : null, + click: () => { + if (pageIdx > 0) { + pageIdx--; + updateLiveStatusBadge(); + refreshLogs(); + } + } + }, _('Previous')), + E('button', { + class: 'btn cbi-button cbi-button-action', + disabled: (pageIdx + 1) >= totalPages ? 'disabled' : null, + click: () => { + if ((pageIdx + 1) < totalPages) { + pageIdx++; + updateLiveStatusBadge(); + refreshLogs(); + } + } + }, _('Next')) + ]) + ]) + ]); +}; + +const pollScheduler = async () => { + try { + const promises = [ + callGetStats(), + callGetTop(10), + callGetHistory(24) + ]; + + const shouldRefreshLogs = (pageIdx === 0 && !searchVal && !isUserPaused); + if (shouldRefreshLogs) { + promises.push(callGetLogs(PAGE_SIZE, 0, '', filterVal)); + } + + const results = await Promise.all(promises); + + dom.content(nodeStats, renderOverviewStats(results[0], results[2])); + dom.content(nodeTop, renderTopRankings(results[1])); + + if (shouldRefreshLogs && results[3]) { + dom.content(nodeLogs, renderLogsTable(results[3])); + } + updateLiveStatusBadge(); + } catch (e) { + } +}; + +const refreshLogs = async () => { + try { + const logs = await callGetLogs(PAGE_SIZE, pageIdx * PAGE_SIZE, searchVal, filterVal); + dom.content(nodeLogs, renderLogsTable(logs)); + updateLiveStatusBadge(); + } catch (e) { + ui.addNotification(null, E('p', [_('Failed to update query logs: '), e.message]), 'error'); + } +}; + +return view.extend({ + async load() { + return Promise.all([ + L.resolveDefault(callGetStats(), {}), + L.resolveDefault(callGetTop(10), {}), + L.resolveDefault(callGetLogs(PAGE_SIZE, 0, searchVal, filterVal), {}), + L.resolveDefault(callGetHistory(24), {}) + ]); + }, + + render(data) { + injectStyles(); + + nodeStats = E('div', { id: 'overview-stats' }); + nodeTop = E('div', { id: 'top-rankings' }); + nodeLogs = E('div', { id: 'logs-table' }); + autoStatusBadge = E('div', { style: 'display: inline-block;' }); + + dom.content(nodeStats, renderOverviewStats(data[0], data[3])); + dom.content(nodeTop, renderTopRankings(data[1])); + dom.content(nodeLogs, renderLogsTable(data[2])); + updateLiveStatusBadge(); + + const searchInput = E('input', { + type: 'text', + class: 'cbi-input-text', + placeholder: _('Search domain or client IP...'), + style: 'min-width: 220px;' + }); + searchInput.addEventListener('input', debounce(() => { + searchVal = searchInput.value.trim(); + pageIdx = 0; + updateLiveStatusBadge(); + refreshLogs(); + }, 300)); + + const filterSelect = E('select', { class: 'cbi-input-select' }, [ + E('option', { value: 'all', selected: filterVal === 'all' ? 'selected' : null }, _('All Queries')), + E('option', { value: 'blocked', selected: filterVal === 'blocked' ? 'selected' : null }, _('Blocked Only')), + E('option', { value: 'cached', selected: filterVal === 'cached' ? 'selected' : null }, _('Cached Only')) + ]); + filterSelect.addEventListener('change', () => { + filterVal = filterSelect.value; + pageIdx = 0; + updateLiveStatusBadge(); + refreshLogs(); + }); + + const resetPageBtn = E('button', { + class: 'cbi-button cbi-button-apply', + click: () => { + pageIdx = 0; + updateLiveStatusBadge(); + refreshLogs(); + } + }, _('First Page / Resume')); + + const clearBtn = E('button', { + class: 'btn cbi-button cbi-button-remove', + style: 'margin-left: auto;' + }, _('Clear query logs')); + + clearBtn.addEventListener('click', () => { + ui.showModal(_('Clear query logs'), [ + E('p', {}, _('Are you sure you want to clear all real-time query logs and top rankings?')), + E('div', { class: 'right', style: 'margin-top: 1rem;' }, [ + E('button', { + class: 'btn cbi-button cbi-button-neutral', + click: ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + class: 'btn cbi-button cbi-button-remove', + click: async () => { + ui.hideModal(); + try { + const res = await callClearQueryLogs(); + if (res?.success) { + ui.addNotification(null, E('p', _('Query logs cleared successfully.')), 'info'); + pageIdx = 0; + await pollScheduler(); + await refreshLogs(); + } else { + ui.addNotification(null, E('p', [_('Failed to clear query logs: '), res?.error || '']), 'error'); + } + } catch (e) { + ui.addNotification(null, E('p', e.message), 'error'); + } + } + }, _('Clear')) + ]) + ]); + }); + + const controlBar = E('div', { style: 'display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 0.75rem;' }, [ + searchInput, + filterSelect, + resetPageBtn, + autoStatusBadge, + clearBtn + ]); + + poll.add(pollScheduler); + + return E('div', { class: 'cbi-map' }, [ + E('h2', { name: 'content' }, '%s - %s'.format(_('MosDNS'), _('Statistics'))), + nodeStats, + nodeTop, + E('div', { class: 'cbi-section' }, [ + E('h3', {}, _('Real-time Query Logs')), + controlBar, + nodeLogs + ]) + ]); + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/luci-app-mosdns/po/templates/mosdns.pot b/luci-app-mosdns/po/templates/mosdns.pot index 605e12ab..a136fad4 100644 --- a/luci-app-mosdns/po/templates/mosdns.pot +++ b/luci-app-mosdns/po/templates/mosdns.pot @@ -74,24 +74,50 @@ msgstr "" msgid "Aliyun Public DNS (DNS over QUIC)" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:640 +msgid "All Queries" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 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 +msgid "Answers" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:203 msgid "Apple domains optimization" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:667 +msgid "" +"Are you sure you want to clear all real-time query logs and top rankings?" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:359 msgid "Auto Save Cache Interval" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:447 +msgid "Auto Save Stats Interval" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 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 +msgid "Average Processing Time" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:306 +msgid "Avg Processing" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:217 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:263 msgid "Baidu Public DNS (180.76.76.76)" @@ -119,6 +145,14 @@ msgstr "" msgid "Block PTR" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:641 +msgid "Blocked Only" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:314 +msgid "Blocked by Filters" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:255 msgid "Bootstrap DNS servers" msgstr "" @@ -137,6 +171,18 @@ msgstr "" msgid "Cache Prefetching" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:642 +msgid "Cached Only" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:325 +msgid "Cached Queries" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:672 +msgid "Cancel" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 msgid "Check And Update" msgstr "" @@ -163,10 +209,29 @@ msgstr "" msgid "Cisco Public DNS (208.67.222.222)" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:692 +msgid "Clear" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:65 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 +msgid "Clear query logs" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:511 +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 +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/update.js:35 msgid "Close" msgstr "" @@ -202,7 +267,7 @@ msgstr "" msgid "Config File" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:455 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:478 msgid "Configuration Editor" msgstr "" @@ -240,6 +305,10 @@ msgstr "" msgid "DNS Forward" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:302 +msgid "DNS Queries Total" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:271 msgid "" "DNS query request concurrency, The number of upstream DNS servers that are " @@ -280,6 +349,14 @@ 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 +msgid "Domain & Record" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:532 +msgid "Elapsed" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 msgid "Enable Auto Database Update" msgstr "" @@ -296,6 +373,10 @@ msgstr "" msgid "Enable EDNS client subnet" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:429 +msgid "Enable Stats Collector" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:283 msgid "" "Enable TCP/DoT RFC 7766 new Query Pipelining connection multiplexing mode" @@ -310,11 +391,11 @@ msgstr "" msgid "Enabled" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:482 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:505 msgid "Enter the GeoIP.dat category to be exported, Allow add multiple tags" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:477 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:500 msgid "Enter the GeoSite.dat category to be exported, Allow add multiple tags" msgstr "" @@ -354,8 +435,8 @@ msgstr "" msgid "Every Wednesday" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:478 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:483 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:501 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:506 msgid "Export directory: /var/mosdns" msgstr "" @@ -363,11 +444,23 @@ msgstr "" msgid "Failed to clean logs." msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:686 +msgid "Failed to clear query logs:" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 msgid "Failed to start update." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:432 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 +msgid "Failed to update query logs:" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:658 +msgid "First Page / Resume" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:455 msgid "Flush DNS Cache" msgstr "" @@ -379,7 +472,7 @@ msgstr "" msgid "Flushing DNS Cache Success." msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:430 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:453 msgid "" "Flushing DNS Cache will clear any IP addresses or DNS records from MosDNS " "cache." @@ -415,7 +508,7 @@ msgstr "" msgid "GeoData Export" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:481 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:504 msgid "GeoIP Tags" msgstr "" @@ -423,7 +516,7 @@ msgstr "" msgid "GeoIP Type" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:476 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:499 msgid "GeoSite Tags" msgstr "" @@ -496,6 +589,10 @@ msgid "" "seconds)." msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:337 +msgid "Latency" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:322 msgid "Lazy Cache TTL" msgstr "" @@ -537,7 +634,7 @@ msgstr "" msgid "Log Level" msgstr "" -#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 +#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:46 msgid "Logs" msgstr "" @@ -577,10 +674,17 @@ 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/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 +msgid "" +"MosDNS API is unreachable. Please ensure MosDNS is running and stats_api " +"plugin is enabled." +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:107 msgid "MosDNS is a plugin-based DNS forwarder/traffic splitter." msgstr "" @@ -593,10 +697,34 @@ 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 +msgid "Next" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:428 +msgid "No DNS answer records returned." +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:353 +msgid "No data available" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:27 msgid "No log data." msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:520 +msgid "No query log entries found." +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 +msgid "Page %d / %d (%d entries)" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:340 +msgid "Per-query speed" +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 " @@ -628,6 +756,10 @@ msgstr "" msgid "Prevent DNS Leaks" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:550 +msgid "Previous" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:329 msgid "" "Proactively refresh hot cache entries in the background before they expire." @@ -643,10 +775,28 @@ msgstr "" msgid "Quad9 Public DNS (9.9.9.9)" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:469 +msgid "Query Log Details" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:435 +msgid "" +"Query log ring buffer capacity (FIFO overwrite, default 2000, larger values " +"consume more memory)" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:681 +msgid "Query logs cleared successfully." +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:31 msgid "RUNNING" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:712 +msgid "Real-time Query Logs" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 msgid "Redirect" msgstr "" @@ -669,6 +819,14 @@ msgstr "" msgid "Remote DNS server" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:434 +msgid "Ring Buffer Capacity" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:457 +msgid "Rule Hit" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:11 msgid "Rule Settings" msgstr "" @@ -677,10 +835,18 @@ msgstr "" msgid "Rules" msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:441 +msgid "Save query statistics and logs locally and reload on next startup." +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:353 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 +msgid "Search domain or client IP..." +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:175 msgid "Set the maximum size of the log file (in MB)." msgstr "" @@ -689,6 +855,19 @@ msgstr "" msgid "Starting update..." msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 +msgid "Statistics" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:440 +msgid "Stats Dump" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:530 +msgid "Status" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:25 msgid "Streaming Media" msgstr "" @@ -730,13 +909,30 @@ msgid "" "server)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:456 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:479 msgid "" "This is the content of the file '/etc/mosdns/config_custom.yaml' from which " "your MosDNS configuration will be generated. Only accepts configuration " "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 +msgid "Time" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:386 +msgid "Top Blocked Domains" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:393 +msgid "Top Clients" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:379 +msgid "Top Queried Domains" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:213 msgid "TrafficRoute Public DNS (180.184.1.1)" msgstr "" @@ -745,7 +941,7 @@ msgstr "" msgid "TrafficRoute Public DNS (180.184.2.2)" msgstr "" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:471 +#: 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 @@ -798,6 +994,10 @@ msgstr "" msgid "Updating Database..." msgstr "" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:453 +msgid "Upstream" +msgstr "" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:164 msgid "Warning" msgstr "" @@ -832,3 +1032,15 @@ msgstr "" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "https://gh-proxy.com" msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:303 +msgid "● Live" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:148 +msgid "● Live Auto-refresh" +msgstr "" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:152 +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 aa99da22..da0ee102 100644 --- a/luci-app-mosdns/po/zh_Hans/mosdns.po +++ b/luci-app-mosdns/po/zh_Hans/mosdns.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-03-20 09:23+0800\n" -"PO-Revision-Date: 2026-05-15 14:09+0800\n" +"PO-Revision-Date: 2026-08-30 08:26+0800\n" "Last-Translator: sbwml \n" "Language-Team: Chinese\n" "Language: zh_Hans\n" @@ -89,24 +89,50 @@ 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 +msgid "All Queries" +msgstr "所有查询" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:68 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 +msgid "Answers" +msgstr "应答结果" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:203 msgid "Apple domains optimization" msgstr "Apple 域名解析优化" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:667 +msgid "" +"Are you sure you want to clear all real-time query logs and top rankings?" +msgstr "确定要清空所有实时查询日志和排行数据吗?" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:359 msgid "Auto Save Cache Interval" msgstr "自动保存缓存间隔(秒)" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:447 +msgid "Auto Save Stats Interval" +msgstr "自动保存统计间隔" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:106 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 +msgid "Average Processing Time" +msgstr "平均延迟" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:306 +msgid "Avg Processing" +msgstr "平均处理" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:217 #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:263 msgid "Baidu Public DNS (180.76.76.76)" @@ -136,6 +162,14 @@ msgstr "黑名单" msgid "Block PTR" msgstr "PTR 黑名单" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:641 +msgid "Blocked Only" +msgstr "仅拦截" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:314 +msgid "Blocked by Filters" +msgstr "拦截总数" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:255 msgid "Bootstrap DNS servers" msgstr "Bootstrap DNS 服务器" @@ -154,6 +188,18 @@ msgstr "自动保存缓存" msgid "Cache Prefetching" msgstr "缓存预取" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:642 +msgid "Cached Only" +msgstr "仅缓存" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:325 +msgid "Cached Queries" +msgstr "缓存命中" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:672 +msgid "Cancel" +msgstr "取消" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:150 msgid "Check And Update" msgstr "检查并更新" @@ -180,10 +226,29 @@ 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 +msgid "Clear" +msgstr "清空" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:65 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 +msgid "Clear query logs" +msgstr "清除查询日志" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:511 +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 +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/update.js:35 msgid "Close" msgstr "关闭" @@ -219,7 +284,7 @@ msgstr "DNS 服务器并发数(默认 2)" msgid "Config File" msgstr "配置文件" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:455 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:478 msgid "Configuration Editor" msgstr "配置编辑器" @@ -258,6 +323,10 @@ msgstr "DNS 缓存大小" msgid "DNS Forward" msgstr "DNS 转发" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:302 +msgid "DNS Queries Total" +msgstr "DNS 查询总数" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:271 msgid "" "DNS query request concurrency, The number of upstream DNS servers that are " @@ -299,6 +368,14 @@ 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 +msgid "Domain & Record" +msgstr "域名与类型" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:532 +msgid "Elapsed" +msgstr "耗时" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:111 msgid "Enable Auto Database Update" msgstr "启用自动更新" @@ -315,6 +392,10 @@ msgstr "启用 DNS 缓存" msgid "Enable EDNS client subnet" msgstr "启用 EDNS 客户端子网" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:429 +msgid "Enable Stats Collector" +msgstr "启用统计收集器" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:283 msgid "" "Enable TCP/DoT RFC 7766 new Query Pipelining connection multiplexing mode" @@ -329,11 +410,11 @@ msgstr "启用此选项 fallback 策略会强制转发到远程 DNS" msgid "Enabled" msgstr "启用" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:482 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:505 msgid "Enter the GeoIP.dat category to be exported, Allow add multiple tags" msgstr "输入需要导出的 GeoIP.dat 类别条目,允许添加多个标签" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:477 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:500 msgid "Enter the GeoSite.dat category to be exported, Allow add multiple tags" msgstr "填写需要导出的 GeoSite.dat 类别条目,允许添加多个标签" @@ -373,8 +454,8 @@ msgstr "每周二" msgid "Every Wednesday" msgstr "每周三" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:478 -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:483 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:501 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:506 msgid "Export directory: /var/mosdns" msgstr "导出目录:/var/mosdns" @@ -382,11 +463,23 @@ msgstr "导出目录:/var/mosdns" msgid "Failed to clean logs." msgstr "清理日志失败:%s" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:686 +msgid "Failed to clear query logs:" +msgstr "清除查询日志失败:" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:89 msgid "Failed to start update." msgstr "启动更新失败" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:432 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:599 +msgid "Failed to update query logs:" +msgstr "更新查询日志失败:" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:658 +msgid "First Page / Resume" +msgstr "重置并恢复" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:455 msgid "Flush DNS Cache" msgstr "刷新 DNS 缓存" @@ -398,7 +491,7 @@ msgstr "刷新 DNS 缓存失败,请检查 MosDNS 状态是否在运行中。" msgid "Flushing DNS Cache Success." msgstr "刷新 DNS 缓存成功" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:430 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:453 msgid "" "Flushing DNS Cache will clear any IP addresses or DNS records from MosDNS " "cache." @@ -434,7 +527,7 @@ msgstr "全量:包含所有国家和私有 IP 地址。" msgid "GeoData Export" msgstr "GeoData 导出" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:481 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:504 msgid "GeoIP Tags" msgstr "GeoIP 标签" @@ -442,7 +535,7 @@ msgstr "GeoIP 标签" msgid "GeoIP Type" msgstr "GeoIP 类型" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:476 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:499 msgid "GeoSite Tags" msgstr "GeoSite 标签" @@ -475,7 +568,7 @@ msgstr "灰名单" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:22 msgid "Hosts" -msgstr "" +msgstr "Hosts 列表" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:300 msgid "IP Address" @@ -488,6 +581,10 @@ msgid "" "www.cloudflare.com/ips-v6\" target=\"_blank\">https://www.cloudflare.com/ips-" "v6" msgstr "" +"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 "" @@ -515,6 +612,10 @@ msgid "" "seconds)." msgstr "后台线程扫描缓存以进行预取的时间间隔(单位:秒)" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:337 +msgid "Latency" +msgstr "延迟" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:322 msgid "Lazy Cache TTL" msgstr "乐观缓存 TTL" @@ -556,7 +657,7 @@ msgstr "日志文件大小" msgid "Log Level" msgstr "日志等级" -#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 +#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:46 msgid "Logs" msgstr "日志" @@ -599,10 +700,19 @@ 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/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 +msgid "" +"MosDNS API is unreachable. Please ensure MosDNS is running and stats_api " +"plugin is enabled." +msgstr "" +"MosDNS API 无法连接。请确保 MosDNS 正在运行且已启用统计收集器(stats_api)插" +"件。" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:107 msgid "MosDNS is a plugin-based DNS forwarder/traffic splitter." msgstr "MosDNS 是一个插件化的 DNS 转发/分流器。" @@ -615,10 +725,34 @@ 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 +msgid "Next" +msgstr "下一页" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:428 +msgid "No DNS answer records returned." +msgstr "无 DNS 应答记录" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:353 +msgid "No data available" +msgstr "暂无数据" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/logs.js:27 msgid "No log data." msgstr "无日志数据。" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:520 +msgid "No query log entries found." +msgstr "未找到查询日志记录。" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:538 +msgid "Page %d / %d (%d entries)" +msgstr "第 %d / %d 页(%d 条记录)" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:340 +msgid "Per-query speed" +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 " @@ -652,6 +786,10 @@ msgstr "当剩余 TTL 小于此值(秒)时触发预取" msgid "Prevent DNS Leaks" msgstr "防止 DNS 泄漏" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:550 +msgid "Previous" +msgstr "上一页" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:329 msgid "" "Proactively refresh hot cache entries in the background before they expire." @@ -667,10 +805,28 @@ 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 +msgid "Query Log Details" +msgstr "查询日志详情" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:435 +msgid "" +"Query log ring buffer capacity (FIFO overwrite, default 2000, larger values " +"consume more memory)" +msgstr "查询日志环形缓冲区容量(FIFO 覆盖,默认 2000,数值越大占用内存越多)" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:681 +msgid "Query logs cleared successfully." +msgstr "查询日志清除成功。" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:31 msgid "RUNNING" msgstr "运行中" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:712 +msgid "Real-time Query Logs" +msgstr "实时查询日志" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:23 msgid "Redirect" msgstr "重定向" @@ -695,6 +851,14 @@ msgstr "远程 DNS 首选 IPv4" msgid "Remote DNS server" msgstr "远程 DNS 服务器" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:434 +msgid "Ring Buffer Capacity" +msgstr "环形缓冲区容量" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:457 +msgid "Rule Hit" +msgstr "命中规则" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:11 msgid "Rule Settings" msgstr "自定义规则列表" @@ -703,17 +867,38 @@ msgstr "自定义规则列表" msgid "Rules" msgstr "规则列表" +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:441 +msgid "Save query statistics and logs locally and reload on next startup." +msgstr "保存统计数据与查询日志在本地,并在下次启动时重新加载" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:353 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 +msgid "Search domain or client IP..." +msgstr "搜索域名或客户端 IP..." + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:175 msgid "Set the maximum size of the log file (in MB)." msgstr "设置日志文件的最大容量(单位:MB)。" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:28 msgid "Starting update..." -msgstr "" +msgstr "正在启动更新..." + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:708 +#: luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json:38 +msgid "Statistics" +msgstr "统计" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:440 +msgid "Stats Dump" +msgstr "自动保存统计" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:530 +msgid "Status" +msgstr "状态" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/rules.js:25 msgid "Streaming Media" @@ -758,7 +943,7 @@ msgstr "" "此功能通常在使用自建 DNS 服务器作为 远程 / 流媒体 DNS 上游时使用(需要上游服" "务器的支持)" -#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:456 +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:479 msgid "" "This is the content of the file '/etc/mosdns/config_custom.yaml' from which " "your MosDNS configuration will be generated. Only accepts configuration " @@ -767,6 +952,23 @@ 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 +msgid "Time" +msgstr "时间" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:386 +msgid "Top Blocked Domains" +msgstr "拦截域名 (Top 10)" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:393 +msgid "Top Clients" +msgstr "活跃客户端 (Top 10)" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:379 +msgid "Top Queried Domains" +msgstr "查询域名 (Top 10)" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:213 msgid "TrafficRoute Public DNS (180.184.1.1)" msgstr "火山引擎公共 DNS(180.184.1.1)" @@ -775,7 +977,7 @@ msgstr "火山引擎公共 DNS(180.184.1.1)" 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:471 +#: 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 @@ -828,6 +1030,10 @@ msgstr "更新成功" msgid "Updating Database..." msgstr "更新数据库..." +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:453 +msgid "Upstream" +msgstr "上游服务器" + #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/basic.js:164 msgid "Warning" msgstr "警告" @@ -865,4 +1071,16 @@ msgstr "信风公共 DNS(114.114.115.115)" #: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/update.js:143 msgid "https://gh-proxy.com" -msgstr "" +msgstr "https://gh-proxy.com" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:303 +msgid "● Live" +msgstr "● 实时" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:148 +msgid "● Live Auto-refresh" +msgstr "● 实时刷新" + +#: luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js:152 +msgid "❚❚ Paused (Page %d)" +msgstr "❚❚ 暂停 (第 %d 页)" diff --git a/luci-app-mosdns/root/etc/config/mosdns b/luci-app-mosdns/root/etc/config/mosdns index 6736cd7f..a56d1b91 100644 --- a/luci-app-mosdns/root/etc/config/mosdns +++ b/luci-app-mosdns/root/etc/config/mosdns @@ -8,7 +8,7 @@ config mosdns 'config' option geo_update_day_time '2' option geoip_type 'geoip-only-cn-private' option configfile '/var/etc/mosdns.json' - option log_level 'info' + option log_level 'error' option log_file '/var/log/mosdns.log' option log_size '1' option cache '1' @@ -30,4 +30,9 @@ config mosdns 'config' option lazy_cache_ttl '86400' option dump_file '0' option reject_type65 '1' + option stats_collector '1' + option stats_capacity '2000' + option stats_dump_file '0' + option stats_dump_interval '600' + diff --git a/luci-app-mosdns/root/etc/init.d/mosdns b/luci-app-mosdns/root/etc/init.d/mosdns index cf4a3bd2..441cf336 100755 --- a/luci-app-mosdns/root/etc/init.d/mosdns +++ b/luci-app-mosdns/root/etc/init.d/mosdns @@ -34,7 +34,7 @@ get_config() { config_get listen_address $1 listen_address "0.0.0.0" config_get log_file $1 log_file "/var/log/mosdns.log" config_get log_size $1 log_size "1" - config_get log_level $1 log_level "info" + config_get log_level $1 log_level "error" config_get minimal_ttl $1 minimal_ttl 0 config_get maximum_ttl $1 maximum_ttl 0 config_get redirect $1 redirect 0 @@ -57,6 +57,10 @@ get_config() { config_get cloudflare $1 cloudflare 0 config_get cloudflare_ip $1 cloudflare_ip "" config_get reject_type65 $1 reject_type65 "0" + config_get stats_collector $1 stats_collector 1 + config_get stats_capacity $1 stats_capacity 2000 + config_get stats_dump_file $1 stats_dump_file 0 + config_get stats_dump_interval $1 stats_dump_interval 600 } generate_config() { @@ -79,6 +83,20 @@ generate_config() { json_close_array # plugins json_add_array "plugins" + # plugin: stats_collector + [ "$stats_collector" -eq 1 ] && { + json_add_object + json_add_string "tag" "stats_collector" + json_add_string "type" "stats_api" + json_add_object "args" + json_add_int "capacity" "$stats_capacity" + [ "$stats_dump_file" -eq 1 ] && { + json_add_string "dump_file" "/etc/mosdns/stats.dump" + json_add_int "dump_interval" "$stats_dump_interval" + } + json_close_object + json_close_object + } # plugin: geosite_cn json_add_object json_add_string "tag" "geosite_cn" @@ -589,6 +607,11 @@ generate_config() { json_add_string "tag" "main_sequence" json_add_string "type" "sequence" json_add_array "args" + [ "$stats_collector" -eq 1 ] && { + json_add_object + json_add_string "exec" "\$stats_collector" + json_close_object + } json_add_object json_add_string "exec" "\$hosts" json_close_object diff --git a/luci-app-mosdns/root/etc/mosdns/config_custom.yaml b/luci-app-mosdns/root/etc/mosdns/config_custom.yaml index 5e521e96..32330e5d 100644 --- a/luci-app-mosdns/root/etc/mosdns/config_custom.yaml +++ b/luci-app-mosdns/root/etc/mosdns/config_custom.yaml @@ -9,6 +9,14 @@ api: include: [] plugins: + # 统计收集器 + - tag: stats_collector + type: stats_api + args: + capacity: 2000 # 查询日志环缓冲区容量 (FIFO 覆盖) + dump_file: "/etc/mosdns/stats.dump" # 数据持久化转存文件路径,不设置则不转存 + dump_interval: 600 # 周期性转储间隔(秒) + # 国内域名 - tag: geosite_cn type: domain_set @@ -120,6 +128,7 @@ plugins: - tag: main_sequence type: sequence args: + - exec: $stats_collector # 执行全局统计控制器 - exec: $lazy_cache - exec: jump has_resp_sequence - exec: $query_is_local_domain diff --git a/luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json b/luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json index e866a8f1..2c58026d 100644 --- a/luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json +++ b/luci-app-mosdns/root/usr/share/luci/menu.d/luci-app-mosdns.json @@ -34,9 +34,17 @@ "path": "mosdns/update" } }, + "admin/services/mosdns/statistics": { + "title": "Statistics", + "order": 25, + "action": { + "type": "view", + "path": "mosdns/statistics" + } + }, "admin/services/mosdns/logs": { "title": "Logs", - "order": 25, + "order": 30, "action": { "type": "view", "path": "mosdns/logs" diff --git a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc index 423c141f..c041d1f8 100755 --- a/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc +++ b/luci-app-mosdns/root/usr/share/mosdns/mosdns.uc @@ -149,8 +149,8 @@ function update_adlist() { print(`Downloading ${mirror}${url}\n`); stdout.flush(); - let curl_res = exec_sys(`curl --connect-timeout 5 -m 90 --ipv4 -kfSLo "${ad_tmpdir}/${filename}" "${mirror}${url}"`); - if (curl_res.code !== 0) download_failed = true; + let dl_res = exec_sys(`wget -4 -q --no-check-certificate -T 90 -t 2 -O "${ad_tmpdir}/${filename}" "${mirror}${url}"`); + if (dl_res.code !== 0) download_failed = true; } } @@ -192,7 +192,7 @@ function update_geodat() { print(`Downloading ${geoip_url}.sha256sum\n`); stdout.flush(); - if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geoip.dat.sha256sum" "${geoip_url}.sha256sum"`).code !== 0) { + if (exec_sys(`wget -4 -q --no-check-certificate -T 20 -t 2 -O "${tmpdir}/geoip.dat.sha256sum" "${geoip_url}.sha256sum"`).code !== 0) { exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geoip.dat.sha256sum"); } @@ -209,7 +209,7 @@ function update_geodat() { } else { print(`Downloading ${geoip_url}\n`); stdout.flush(); - if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geoip.dat" "${geoip_url}"`).code !== 0) { + if (exec_sys(`wget -4 -q --no-check-certificate -T 120 -t 2 -O "${tmpdir}/geoip.dat" "${geoip_url}"`).code !== 0) { exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geoip.dat"); } @@ -227,7 +227,7 @@ function update_geodat() { print(`Downloading ${geosite_url}.sha256sum\n`); stdout.flush(); - if (exec_sys(`curl --connect-timeout 5 -m 20 --ipv4 -kfSLo "${tmpdir}/geosite.dat.sha256sum" "${geosite_url}.sha256sum"`).code !== 0) { + if (exec_sys(`wget -4 -q --no-check-certificate -T 20 -t 2 -O "${tmpdir}/geosite.dat.sha256sum" "${geosite_url}.sha256sum"`).code !== 0) { exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geosite.dat.sha256sum"); } @@ -244,7 +244,7 @@ function update_geodat() { } else { print(`Downloading ${geosite_url}\n`); stdout.flush(); - if (exec_sys(`curl --connect-timeout 5 -m 120 --ipv4 -kfSLo "${tmpdir}/geosite.dat" "${geosite_url}"`).code !== 0) { + if (exec_sys(`wget -4 -q --no-check-certificate -T 120 -t 2 -O "${tmpdir}/geosite.dat" "${geosite_url}"`).code !== 0) { exec_sys(`rm -rf "${tmpdir}"`); die("Failed to download geosite.dat"); } 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 9c7d23ad..ecb5c7b3 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 @@ -16,10 +16,15 @@ "service": [ "list" ], "luci.mosdns": [ "clean_log", + "clear_query_logs", "flush_cache", + "get_history", + "get_logs", + "get_stats", + "get_top", + "get_update_log", "get_version", "print_log", - "get_update_log", "start_update" ] }, @@ -35,7 +40,8 @@ "/var/mosdns/*": [ "write" ] }, "ubus": { - "file": [ "write" ] + "file": [ "write" ], + "luci.mosdns": [ "clear_query_logs" ] }, "uci": [ "mosdns" ] } 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 9760722f..10d61532 100644 --- a/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns +++ b/luci-app-mosdns/root/usr/share/rpcd/ucode/luci.mosdns @@ -58,16 +58,85 @@ function get_logfile_path_internal() { } } +function parse_json_safe(str) { + if (!str || str == "") + return null; + + try { + return json(str); + } catch(e) { + return null; + } +} + +function call_mosdns_api(endpoint, method) { + try { + let uci_cursor = cursor(); + uci_cursor.load('mosdns'); + let port = uci_cursor.get('mosdns', 'config', 'listen_port_api') || '9091'; + let http_method = method ? method : 'GET'; + let post_flag = (http_method === 'POST') ? '--post-data=""' : ''; + let cmd = `wget -q -O - ${post_flag} "http://127.0.0.1:${port}/plugins/stats_collector${endpoint}"`; + let res = exec_sys(cmd); + if (res.code === 0 && res.stdout != "") { + let json_val = parse_json_safe(res.stdout); + if (json_val) return json_val; + } + return { error: "MosDNS API unreachable" }; + } catch (e) { + return { error: String(e) }; + } +} + const methods = { + get_stats: { + call: function() { + return call_mosdns_api('/api/v1/stats', 'GET'); + } + }, + + get_history: { + args: { points: 24 }, + call: function(request) { + let points = request?.args?.points || 24; + return call_mosdns_api(`/api/v1/history?points=${points}`, 'GET'); + } + }, + + get_top: { + args: { limit: 10 }, + call: function(request) { + let limit = request?.args?.limit || 10; + return call_mosdns_api(`/api/v1/top?limit=${limit}`, 'GET'); + } + }, + + get_logs: { + args: { limit: 50, offset: 0, search: '', filter: 'all' }, + call: function(request) { + let limit = request?.args?.limit || 50; + let offset = request?.args?.offset || 0; + let search = request?.args?.search || ''; + let filter = request?.args?.filter || 'all'; + return call_mosdns_api(`/api/v1/logs?limit=${limit}&offset=${offset}&search=${search}&filter=${filter}`, 'GET'); + } + }, + + clear_query_logs: { + call: function() { + return call_mosdns_api('/api/v1/logs/clear', 'POST'); + } + }, + flush_cache: { call: function() { try { let uci_cursor = cursor(); uci_cursor.load('mosdns'); - let port = uci_cursor.get('mosdns', 'config', 'listen_port_api'); + let port = uci_cursor.get('mosdns', 'config', 'listen_port_api') || '9091'; if (!port) return { error: "API listen port not configured." }; - let res = exec_sys(`curl -s 127.0.0.1:${port}/plugins/lazy_cache/flush`); + let res = exec_sys(`wget -q -O - "http://127.0.0.1:${port}/plugins/lazy_cache/flush"`); if (res.code === 0) { return { success: true }; } else { diff --git a/luci-app-syncthing/Makefile b/luci-app-syncthing/Makefile new file mode 100644 index 00000000..2219fcb2 --- /dev/null +++ b/luci-app-syncthing/Makefile @@ -0,0 +1,22 @@ +# Copyright (C) 2020 Gyj1109 +# 适配 OpenWrt 25.12 修改版 + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-syncthing +PKG_VERSION:=1.0 +PKG_RELEASE:=3 + +LUCI_TITLE:=LuCI support for Syncthing +LUCI_PKGARCH:=all +# 适配 25.12:确保安装了 syncthing 后,LuCI 界面能正确拉起 +LUCI_DEPENDS:=+syncthing + +include $(TOPDIR)/feeds/luci/luci.mk + +# 明确定义配置文件的归属,这对 25.12 的 apk 管理器非常重要 +define Package/$(PKG_NAME)/conffiles +/etc/config/syncthing +endef + +$(eval $(call BuildPackage,$(PKG_NAME))) diff --git a/luci-app-syncthing/README.md b/luci-app-syncthing/README.md new file mode 100644 index 00000000..b07298c1 --- /dev/null +++ b/luci-app-syncthing/README.md @@ -0,0 +1,2 @@ +# luci-app-syncthing +在Potat0000源码基础上进行了汉化优化,官方openwrt23.05.3编译后可使用 diff --git a/luci-app-syncthing/luasrc/controller/syncthing.lua b/luci-app-syncthing/luasrc/controller/syncthing.lua new file mode 100644 index 00000000..ee2ca7ef --- /dev/null +++ b/luci-app-syncthing/luasrc/controller/syncthing.lua @@ -0,0 +1,17 @@ +module("luci.controller.syncthing", package.seeall) + +function index() + if not nixio.fs.access("/etc/config/syncthing") then + return + end + + entry({"admin", "services", "syncthing"}, cbi("syncthing"), _("文件同步"), 10).dependent = true + entry({"admin", "services", "syncthing", "status"}, call("act_status")).leaf = true +end + +function act_status() + local e = {} + e.running = luci.sys.call("pgrep syncthing >/dev/null") == 0 + luci.http.prepare_content("application/json") + luci.http.write_json(e) +end diff --git a/luci-app-syncthing/luasrc/model/cbi/syncthing.lua b/luci-app-syncthing/luasrc/model/cbi/syncthing.lua new file mode 100644 index 00000000..324ea054 --- /dev/null +++ b/luci-app-syncthing/luasrc/model/cbi/syncthing.lua @@ -0,0 +1,48 @@ +require("nixio.fs") + +m = Map("syncthing", translate("Syncthing同步工具")) + +m:section(SimpleSection).template = "syncthing/syncthing_status" + +s = m:section(TypedSection, "syncthing") + +s.anonymous = true + +o = s:option(Flag, "enabled", translate("启用")) +o.default = 0 +o.rmempty = false + +gui_address = s:option(Value, "gui_address", translate("GUI访问地址")) +gui_address.description = translate("使用0.0.0.0以监控所有访问。") +gui_address.default = "http://0.0.0.0:8384" +gui_address.placeholder = "http://0.0.0.0:8384" +gui_address.rmempty = false + +home = s:option(Value, "home", translate("配置文件目录")) +home.description = translate("只有保存在/etc/syncthing中的配置会自动备份!") +home.default = "/etc/syncthing" +home.placeholder = "/etc/syncthing" +home.rmempty = false + +user = s:option(ListValue, "user", translate("用户")) +user.description = translate("默认是syncthing,但这可能会导致权限被拒绝。Syncthing官方不建议以root身份运行。") +user:value("", translate("syncthing")) +for u in luci.util.execi("cat /etc/passwd | cut -d ':' -f1") do + user:value(u) +end + +macprocs = s:option(Value, "macprocs", translate("线程限制")) +macprocs.description = translate("0表示匹配CPU数量(默认),>0表示显式指定并发数。") +macprocs.default = "0" +macprocs.placeholder = "0" +macprocs.datatype = "range(0,32)" +macprocs.rmempty = false + +nice = s:option(Value, "nice", translate("优先级")) +nice.description = translate("显式指定优先级值。0是最高,19是最低。(暂时不允许设置负值)") +nice.default = "19" +nice.placeholder = "19" +nice.datatype = "range(0,19)" +nice.rmempty = false + +return m diff --git a/luci-app-syncthing/luasrc/view/syncthing/syncthing_status.htm b/luci-app-syncthing/luasrc/view/syncthing/syncthing_status.htm new file mode 100644 index 00000000..566cc020 --- /dev/null +++ b/luci-app-syncthing/luasrc/view/syncthing/syncthing_status.htm @@ -0,0 +1,27 @@ + + +
+

+ <%:正在收集数据...%> +

+
diff --git a/luci-app-syncthing/root/etc/uci-defaults/luci-syncthing b/luci-app-syncthing/root/etc/uci-defaults/luci-syncthing new file mode 100644 index 00000000..3adb7997 --- /dev/null +++ b/luci-app-syncthing/root/etc/uci-defaults/luci-syncthing @@ -0,0 +1,12 @@ +#!/bin/sh +touch /etc/config/syncthing + +uci -q batch <<-EOF >/dev/null + delete ucitrack.@syncthing[-1] + add ucitrack syncthing + set ucitrack.@syncthing[-1].exec='/etc/init.d/syncthing stop && /etc/init.d/syncthing start' + commit ucitrack +EOF +# remove LuCI cache +rm -f /tmp/luci* +exit 0 diff --git a/luci-app-syncthing/root/usr/share/rpcd/acl.d/luci-app-syncthing.json b/luci-app-syncthing/root/usr/share/rpcd/acl.d/luci-app-syncthing.json new file mode 100644 index 00000000..9fc9cc54 --- /dev/null +++ b/luci-app-syncthing/root/usr/share/rpcd/acl.d/luci-app-syncthing.json @@ -0,0 +1,11 @@ +{ + "luci-app-syncthing": { + "description": "Grant UCI access for luci-app-syncthing", + "read": { + "uci": [ "syncthing" ] + }, + "write": { + "uci": [ "syncthing" ] + } + } +}