diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile
index fe39983f..ae52b719 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.11
-PKG_RELEASE:=19
+PKG_VERSION:=1.7.12
+PKG_RELEASE:=20
LUCI_TITLE:=LuCI Support for mosdns
LUCI_PKGARCH:=all
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 9b398094..c08bbbda 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
@@ -48,6 +48,10 @@ let nodeStats;
let nodeTop;
let nodeLogs;
let autoStatusBadge;
+let statsElements = null;
+let currentBadgeState = null;
+let lastTopJson = '';
+let lastLogsJson = '';
const cleanIP = ip => {
if (!ip) return '-';
@@ -66,9 +70,10 @@ const injectStyles = () => {
'.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-wrap { margin-top: 0.5rem; height: 44px; position: relative; overflow: visible; display: flex; align-items: flex-end; touch-action: none; -webkit-user-select: none; user-select: none; }',
'.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; }',
+ '.spark-hover-hitbox { touch-action: none; -webkit-user-select: none; user-select: none; cursor: crosshair; }',
'.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; }',
@@ -143,7 +148,15 @@ const getLatencyClass = elapsedMs => {
const updateLiveStatusBadge = () => {
if (!autoStatusBadge) return;
- if (pageIdx === 0 && !searchVal && !isUserPaused) {
+ const isLive = (pageIdx === 0 && !searchVal && !isUserPaused);
+ const targetState = isLive ? 'live' : ('paused:' + pageIdx);
+
+ if (currentBadgeState === targetState)
+ return;
+
+ currentBadgeState = targetState;
+
+ if (isLive) {
dom.content(autoStatusBadge, [
E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live Auto-refresh'))
]);
@@ -154,47 +167,17 @@ const updateLiveStatusBadge = () => {
}
};
-const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => {
+const createSparklineSVG = (strokeColor, fillGradId) => {
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';
+ let coords = [];
+ let isHovered = false;
+ let currentIdx = -1;
+ let hideTimer = null;
const svgContainer = E('div', { class: 'mosdns-sparkline-wrap' });
svgContainer.innerHTML =
@@ -205,27 +188,26 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => {
'' +
'' +
'' +
- '' +
- '' +
+ '' +
+ '' +
'' +
'' +
'' +
'' +
- '' +
+ '' +
'' +
'
';
+ const areaPath = svgContainer.querySelector('.spark-area-path');
+ const linePath = svgContainer.querySelector('.spark-line-path');
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 renderTooltipAndHover = idx => {
+ if (idx < 0 || idx >= coords.length) return;
const coord = coords[idx];
if (!coord) return;
@@ -251,10 +233,14 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => {
'' + timeStr + '
';
tooltip.style.display = 'block';
- const tooltipX = (coord.x / width) * rect.width;
+ const rect = svgContainer.getBoundingClientRect();
+ const containerWidth = rect.width || width;
+ const tooltipX = (coord.x / width) * containerWidth;
+ const ratio = coord.x / width;
+
if (ratio > 0.65) {
tooltip.style.left = 'auto';
- tooltip.style.right = (rect.width - tooltipX + 8) + 'px';
+ tooltip.style.right = (containerWidth - tooltipX + 8) + 'px';
} else {
tooltip.style.left = (tooltipX + 8) + 'px';
tooltip.style.right = 'auto';
@@ -262,21 +248,203 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => {
tooltip.style.top = '-6px';
};
- const onLeave = () => {
- hoverGroup.style.display = 'none';
- tooltip.style.display = 'none';
+ const update = (dataItems, maxScale) => {
+ 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;
+
+ 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';
+
+ areaPath.setAttribute('d', areaD);
+ linePath.setAttribute('d', pathD);
+
+ if (isHovered && currentIdx >= 0) {
+ renderTooltipAndHover(currentIdx);
+ }
};
- hitbox.addEventListener('mousemove', onMove);
- hitbox.addEventListener('mouseleave', onLeave);
+ const onPositionMove = clientX => {
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ const rect = svgContainer.getBoundingClientRect();
+ if (!rect.width || !coords.length) return;
+ const mouseX = clientX - rect.left;
+ const ratio = Math.max(0, Math.min(1, mouseX / rect.width));
+ const idx = Math.round(ratio * (coords.length - 1));
+ currentIdx = idx;
+ isHovered = true;
+ renderTooltipAndHover(idx);
+ };
+ const onLeave = (delay = 0) => {
+ if (delay > 0) {
+ if (hideTimer) clearTimeout(hideTimer);
+ hideTimer = setTimeout(() => {
+ isHovered = false;
+ currentIdx = -1;
+ hoverGroup.style.display = 'none';
+ tooltip.style.display = 'none';
+ hideTimer = null;
+ }, delay);
+ } else {
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ isHovered = false;
+ currentIdx = -1;
+ hoverGroup.style.display = 'none';
+ tooltip.style.display = 'none';
+ }
+ };
+
+ hitbox.addEventListener('mousemove', e => onPositionMove(e.clientX));
+ hitbox.addEventListener('mouseenter', e => onPositionMove(e.clientX));
+ hitbox.addEventListener('mouseleave', () => onLeave(0));
+
+ hitbox.addEventListener('touchstart', e => {
+ if (e.touches && e.touches.length > 0) {
+ onPositionMove(e.touches[0].clientX);
+ }
+ }, { passive: true });
+
+ hitbox.addEventListener('touchmove', e => {
+ if (e.touches && e.touches.length > 0) {
+ onPositionMove(e.touches[0].clientX);
+ }
+ }, { passive: true });
+
+ hitbox.addEventListener('touchend', () => onLeave(1500), { passive: true });
+ hitbox.addEventListener('touchcancel', () => onLeave(0), { passive: true });
+
+ svgContainer.update = update;
return svgContainer;
};
-const renderOverviewStats = (stats, historyData) => {
+const createOverviewStatsDOM = () => {
+ const metricTotal = E('div', { class: 'metric-val' }, '-');
+ const subtextTotal = E('div', { class: 'subtext' }, '-');
+ const sparklineTotal = createSparklineSVG('#3b82f6', 'spark-grad-total');
+
+ const badgeBlocked = E('span', { class: 'mosdns-badge badge-danger' }, '-');
+ const metricBlocked = E('div', { class: 'metric-val', style: 'color: #dc2626;' }, '-');
+ const sparklineBlocked = createSparklineSVG('#dc2626', 'spark-grad-blocked');
+
+ const badgeCached = E('span', { class: 'mosdns-badge badge-teal' }, '-');
+ const metricCached = E('div', { class: 'metric-val', style: 'color: #059669;' }, '-');
+ const sparklineCached = createSparklineSVG('#059669', 'spark-grad-cached');
+
+ const metricLatency = E('div', { class: 'metric-val', style: 'color: #2563eb;' }, '-');
+ const subtextLatency = E('div', { class: 'subtext' }, _('Per-query speed'));
+
+ const grid = 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'))
+ ]),
+ metricTotal,
+ subtextTotal
+ ]),
+ sparklineTotal
+ ]),
+
+ E('div', { class: 'mosdns-stat-card' }, [
+ E('div', {}, [
+ E('div', { class: 'title-row' }, [
+ E('span', {}, _('Blocked by Filters')),
+ badgeBlocked
+ ]),
+ metricBlocked
+ ]),
+ sparklineBlocked
+ ]),
+
+ E('div', { class: 'mosdns-stat-card' }, [
+ E('div', {}, [
+ E('div', { class: 'title-row' }, [
+ E('span', {}, _('Cached Queries')),
+ badgeCached
+ ]),
+ metricCached
+ ]),
+ sparklineCached
+ ]),
+
+ 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'))
+ ]),
+ metricLatency,
+ subtextLatency
+ ])
+ ])
+ ]);
+
+ statsElements = {
+ grid,
+ metricTotal,
+ subtextTotal,
+ sparklineTotal,
+ badgeBlocked,
+ metricBlocked,
+ sparklineBlocked,
+ badgeCached,
+ metricCached,
+ sparklineCached,
+ metricLatency,
+ subtextLatency
+ };
+
+ return grid;
+};
+
+const updateOverviewStats = (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.'));
+ if (nodeStats) {
+ dom.content(nodeStats, E('div', { class: 'alert-message warning' },
+ _('MosDNS API is unreachable. Please ensure MosDNS is running and stats_api plugin is enabled.')));
+ statsElements = null;
+ }
+ return;
+ }
+
+ if (!statsElements || !nodeStats.contains(statsElements.grid)) {
+ dom.content(nodeStats, createOverviewStatsDOM());
}
const {
@@ -295,52 +463,19 @@ const renderOverviewStats = (stats, historyData) => {
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)
- ]),
+ statsElements.metricTotal.textContent = total.toLocaleString();
+ statsElements.subtextTotal.textContent = _('Avg Processing') + ': ' + avg_ms + ' ms';
+ statsElements.sparklineTotal.update(totalItems, 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)
- ]),
+ statsElements.badgeBlocked.textContent = blocked_pct + '%';
+ statsElements.metricBlocked.textContent = blocked.toLocaleString();
+ statsElements.sparklineBlocked.update(blockedItems, 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)
- ]),
+ statsElements.badgeCached.textContent = cached_pct + '%';
+ statsElements.metricCached.textContent = cached.toLocaleString();
+ statsElements.sparklineCached.update(cachedItems, 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'))
- ])
- ])
- ]);
+ statsElements.metricLatency.textContent = avg_ms + ' ms';
};
const renderTopRankings = topData => {
@@ -398,6 +533,13 @@ const renderTopRankings = topData => {
]);
};
+const updateTopRankings = topData => {
+ const json = JSON.stringify(topData || {});
+ if (json === lastTopJson) return;
+ lastTopJson = json;
+ dom.content(nodeTop, renderTopRankings(topData));
+};
+
const showLogDetailsModal = item => {
let statusBadge;
if (item.is_blocked) {
@@ -579,11 +721,15 @@ const pollScheduler = async () => {
const results = await Promise.all(promises);
- dom.content(nodeStats, renderOverviewStats(results[0], results[2]));
- dom.content(nodeTop, renderTopRankings(results[1]));
+ updateOverviewStats(results[0], results[2]);
+ updateTopRankings(results[1]);
if (shouldRefreshLogs && results[3]) {
- dom.content(nodeLogs, renderLogsTable(results[3]));
+ const json = JSON.stringify(results[3]);
+ if (json !== lastLogsJson) {
+ lastLogsJson = json;
+ dom.content(nodeLogs, renderLogsTable(results[3]));
+ }
}
updateLiveStatusBadge();
} catch (e) {
@@ -593,6 +739,7 @@ const pollScheduler = async () => {
const refreshLogs = async () => {
try {
const logs = await callGetLogs(PAGE_SIZE, pageIdx * PAGE_SIZE, searchVal, filterVal);
+ lastLogsJson = JSON.stringify(logs || {});
dom.content(nodeLogs, renderLogsTable(logs));
updateLiveStatusBadge();
} catch (e) {
@@ -613,13 +760,19 @@ return view.extend({
render(data) {
injectStyles();
+ statsElements = null;
+ currentBadgeState = null;
+ lastTopJson = '';
+ lastLogsJson = '';
+
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]));
+ updateOverviewStats(data[0], data[3]);
+ updateTopRankings(data[1]);
+ lastLogsJson = JSON.stringify(data[2] || {});
dom.content(nodeLogs, renderLogsTable(data[2]));
updateLiveStatusBadge();
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index 0ff01de1..51ad0225 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.26
-PKG_RELEASE:=254
+PKG_RELEASE:=255
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua
index ad55a1da..9cecc8b4 100644
--- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua
+++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua
@@ -1810,18 +1810,7 @@ function gen_config(var)
end
end
- table.insert(route.rules, {
- action = "route",
- ip_is_private = true,
- outbound = "direct"
- })
-
if COMMON.default_outbound_tag then
- table.insert(route.rules, {
- action = "route",
- port_range = { "0:65535" },
- outbound = COMMON.default_outbound_tag
- })
route.final = COMMON.default_outbound_tag
end
diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua
index 8162ade4..0bc35e4f 100644
--- a/luci-app-passwall/luasrc/passwall/util_xray.lua
+++ b/luci-app-passwall/luasrc/passwall/util_xray.lua
@@ -1540,11 +1540,6 @@ function gen_config(var)
})
end
- table.insert(rules, {
- outboundTag = "direct",
- ip = { "geoip:private" }
- })
-
if default_outboundTag then
local rule = {
_flag = "default",
diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index ddcbfd8c..a4c2eb4b 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.27
-PKG_RELEASE:=106
+PKG_RELEASE:=107
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
index 7edf3221..d1a32027 100644
--- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
+++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua
@@ -1797,11 +1797,6 @@ function gen_config(var)
end
if COMMON.default_outbound_tag then
- table.insert(route.rules, {
- action = "route",
- port_range = { "0:65535" },
- outbound = COMMON.default_outbound_tag
- })
route.final = COMMON.default_outbound_tag
end
diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile
index cdfeabce..81b895ec 100644
--- a/luci-theme-footstrap/Makefile
+++ b/luci-theme-footstrap/Makefile
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
-PKG_RELEASE:=48
+PKG_RELEASE:=49
endif
LUCI_TITLE:=Footstrap Theme
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
index 9c8a7f87..8910b20e 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js
@@ -130,6 +130,18 @@ function naturalHeight(el) {
const last = el.lastElementChild;
if (!last) return 0;
const box = el.getBoundingClientRect();
+ /* A BOX WITH NO HEIGHT OF ITS OWN HOLDS NOTHING UP, and asking its content how tall it would be
+ * gets an answer about a page that is not on screen. `visibility: hidden` leaves children in the
+ * layout with rects of their own, so an inactive tab pane — collapsed to height 0 by
+ * theme/30-tables.css — measures its full content here and the floor then pins that collapse
+ * open: on Network -> Interfaces the hidden `device` pane held 893px, and the active pane's
+ * content sat that much further down the page (issue #41, reported against a third-party page
+ * and reproduced on the stock one). The clear-and-remeasure shape this replaced read the
+ * collapsed height and wrote nothing, which is the behaviour restored here.
+ *
+ * A container a tick has just emptied also measures 0, and that is the same answer for the same
+ * reason: the floor it already wears is what holds it up, and holdFloor() leaves it alone. */
+ if (!box.height) return 0;
const end = last.getBoundingClientRect();
if (!end.height && !end.width) return 0; /* a last child out of the flow says nothing */
const cs = window.getComputedStyle(el);
diff --git a/mise/Makefile b/mise/Makefile
index 226031c4..b14a38a3 100644
--- a/mise/Makefile
+++ b/mise/Makefile
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise
-PKG_VERSION:=2026.8.15
-PKG_RELEASE:=2
+PKG_VERSION:=2026.8.16
+PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip
diff --git a/mosdns/Makefile b/mosdns/Makefile
index bab8a1b7..84ad6296 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:=19
+PKG_RELEASE:=20
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/100-mosdns-update-dependencies.patch b/mosdns/patches/100-mosdns-update-dependencies.patch
index 15fbbdee..449ba9f1 100644
--- a/mosdns/patches/100-mosdns-update-dependencies.patch
+++ b/mosdns/patches/100-mosdns-update-dependencies.patch
@@ -1,16 +1,16 @@
-From 201408484de552cb927f6a50b76a34d65c8ac8c6 Mon Sep 17 00:00:00 2001
+From 4b76512586cd404855b717d6b8ac3b4d12ef7a54 Mon Sep 17 00:00:00 2001
From: sbwml
Date: Fri, 20 Mar 2026 10:39:22 +0800
Subject: [PATCH] mosdns: update dependencies
Signed-off-by: sbwml
---
- .github/workflows/release.yml | 2 +-
- .github/workflows/test.yml | 2 +-
- go.mod | 36 +++++++++----------
- go.sum | 68 +++++++++++++++++------------------
- pkg/query_context/context.go | 1 -
- 5 files changed, 54 insertions(+), 55 deletions(-)
+ .github/workflows/release.yml | 2 +-
+ .github/workflows/test.yml | 2 +-
+ go.mod | 60 ++++++++---------
+ go.sum | 121 ++++++++++++++++------------------
+ pkg/query_context/context.go | 1 -
+ 5 files changed, 85 insertions(+), 101 deletions(-)
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,107 +36,132 @@ Signed-off-by: sbwml
--- a/go.mod
+++ b/go.mod
-@@ -1,30 +1,30 @@
+@@ -1,31 +1,31 @@
module github.com/IrineSistiana/mosdns/v5
-go 1.24.9
-+go 1.25.0
++go 1.26.0
require (
github.com/IrineSistiana/go-bytes-pool v0.0.0-20230918115058-c72bd9761c57
- github.com/go-chi/chi/v5 v5.2.3
- github.com/go-viper/mapstructure/v2 v2.4.0
-+ github.com/go-chi/chi/v5 v5.2.5
++ github.com/go-chi/chi/v5 v5.3.2
+ github.com/go-viper/mapstructure/v2 v2.5.0
github.com/google/nftables v0.3.0
- github.com/kardianos/service v1.2.4
+- github.com/kardianos/service v1.2.4
- github.com/klauspost/compress v1.18.2
- github.com/miekg/dns v1.1.70
-+ github.com/klauspost/compress v1.18.4
-+ github.com/miekg/dns v1.1.72
++ github.com/kardianos/service v1.3.0
++ github.com/klauspost/compress v1.19.2
++ github.com/miekg/dns v1.1.73
github.com/mitchellh/mapstructure v1.5.0
github.com/nadoo/ipset v0.5.0
- github.com/prometheus/client_golang v1.23.2
+- github.com/prometheus/client_golang v1.23.2
- github.com/quic-go/quic-go v0.58.1
-+ github.com/quic-go/quic-go v0.59.0
++ github.com/prometheus/client_golang v1.24.1
++ github.com/quic-go/quic-go v0.62.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
- github.com/stretchr/testify v1.11.1
+- github.com/stretchr/testify v1.11.1
++ github.com/stretchr/testify v1.12.1
github.com/vishvananda/netlink v1.3.0
- go.uber.org/zap v1.27.1
- go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
+- go.uber.org/zap v1.27.1
+- go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
- golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
- golang.org/x/net v0.48.0
- golang.org/x/sync v0.19.0
- golang.org/x/sys v0.40.0
- golang.org/x/time v0.14.0
-+ golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
-+ golang.org/x/net v0.52.0
-+ golang.org/x/sync v0.20.0
-+ golang.org/x/sys v0.42.0
+- google.golang.org/protobuf v1.36.11
++ go.uber.org/zap v1.28.0
++ go4.org/netipx v0.0.0-20260823151212-3075585bcbeb
++ golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa
++ golang.org/x/net v0.58.0
++ golang.org/x/sync v0.22.0
++ golang.org/x/sys v0.47.0
+ golang.org/x/time v0.15.0
- google.golang.org/protobuf v1.36.11
++ google.golang.org/protobuf v1.36.12
)
-@@ -37,14 +37,14 @@ require (
- github.com/fsnotify/fsnotify v1.9.0 // indirect
+ replace github.com/nadoo/ipset v0.5.0 => github.com/IrineSistiana/ipset v0.5.1-0.20220703061533-6e0fc3b04c0a
+@@ -33,18 +33,16 @@ replace github.com/nadoo/ipset v0.5.0 =>
+ require (
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+- github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+- github.com/fsnotify/fsnotify v1.9.0 // indirect
++ github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/mdlayher/netlink v1.8.0 // indirect
-+ github.com/mdlayher/netlink v1.9.0 // indirect
- github.com/mdlayher/socket v0.5.1 // indirect
+- github.com/mdlayher/socket v0.5.1 // indirect
++ github.com/mdlayher/netlink v1.11.2 // indirect
++ github.com/mdlayher/socket v0.6.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
- github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.67.5 // indirect
+- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+- github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+- github.com/prometheus/client_model v0.6.2 // indirect
+- github.com/prometheus/common v0.67.5 // indirect
- github.com/prometheus/procfs v0.19.2 // indirect
-+ github.com/prometheus/procfs v0.20.1 // indirect
++ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
++ github.com/prometheus/client_model v0.6.3 // indirect
++ github.com/prometheus/common v0.71.0 // indirect
++ github.com/prometheus/procfs v0.22.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
-@@ -55,11 +55,11 @@ require (
+@@ -55,11 +53,7 @@ require (
github.com/vishvananda/netns v0.0.4 // indirect
go.uber.org/mock v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- go.yaml.in/yaml/v2 v2.4.3 // indirect
-+ go.yaml.in/yaml/v2 v2.4.4 // indirect
- go.yaml.in/yaml/v3 v3.0.4 // indirect
+- go.yaml.in/yaml/v3 v3.0.4 // indirect
- golang.org/x/crypto v0.46.0 // indirect
- golang.org/x/mod v0.32.0 // indirect
- golang.org/x/text v0.33.0 // indirect
- golang.org/x/tools v0.40.0 // indirect
-+ golang.org/x/crypto v0.49.0 // indirect
-+ golang.org/x/mod v0.34.0 // indirect
-+ golang.org/x/text v0.35.0 // indirect
-+ golang.org/x/tools v0.43.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
+- gopkg.in/yaml.v3 v3.0.1 // indirect
++ go.yaml.in/yaml/v3 v3.0.5 // indirect
++ golang.org/x/crypto v0.55.0 // indirect
++ golang.org/x/text v0.41.0 // indirect
)
--- a/go.sum
+++ b/go.sum
-@@ -13,10 +13,10 @@ github.com/frankban/quicktest v1.14.6 h1
+@@ -7,58 +7,56 @@ github.com/beorn7/perks v1.0.1/go.mod h1
+ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
- github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
- github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+-github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+-github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
-github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
-github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
-github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
-github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
-+github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
-+github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
++github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
++github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
++github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
++github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
+github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
+github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
-@@ -25,20 +25,20 @@ github.com/inconshreveable/mousetrap v1.
+ github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
- github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
- github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
+-github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk=
+-github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
-github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
-github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
-+github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
-+github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
++github.com/kardianos/service v1.3.0 h1:/LGy+xPP2TM+GLTiCZ2di7cy0Jd/qrawlTUfqKYFdTI=
++github.com/kardianos/service v1.3.0/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc=
++github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
++github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -145,46 +170,80 @@ Signed-off-by: sbwml
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
-github.com/mdlayher/netlink v1.8.0/go.mod h1:UhgKXUlDQhzb09DrCl2GuRNEglHmhYoWAHid9HK3594=
-+github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
-+github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
- github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
- github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
+-github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
+-github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
-github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
-github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
-+github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
-+github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
++github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
++github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
++github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
++github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
++github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
++github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
-@@ -53,12 +53,12 @@ github.com/prometheus/client_model v0.6.
- github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
- github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
- github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+-github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+-github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+-github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
+-github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
+-github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
+-github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+-github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
-github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
-github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
-+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
-+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
++github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
++github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
++github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
++github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
++github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
++github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
++github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
++github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
++github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
++github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
++github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
++github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
-github.com/quic-go/quic-go v0.58.1 h1:J0s55TVauDbnCEY5tU2B8e7nYb3gnKMSez5Fwi1dl2s=
-github.com/quic-go/quic-go v0.58.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
-+github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
-+github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
++github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8=
++github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-@@ -91,32 +91,32 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSx
+@@ -75,8 +73,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2K
+ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
+-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
++github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
++github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
+ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+ github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
+@@ -89,38 +87,31 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5E
+ go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
- go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
- go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+-go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
+-go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
-go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
++go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
++go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
- go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
- go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
- go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
+-go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
+-go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
@@ -195,16 +254,18 @@ Signed-off-by: sbwml
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-+golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-+golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-+golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
-+golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
-+golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
-+golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
-+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
-+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
++go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
++go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
++go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs=
++go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
++golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
++golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
++golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
++golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
++golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
++golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
++golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
++golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
@@ -215,17 +276,21 @@ Signed-off-by: sbwml
-golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
-golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
-golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
-+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+-google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+-google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
++golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
++golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
++golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
++golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
+golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
+golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
-+golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
-+golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
- google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
- google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
++google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
++google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
--- a/pkg/query_context/context.go
+++ b/pkg/query_context/context.go
@@ -108,7 +108,6 @@ func (ctx *Context) QQuestion() dns.Ques
diff --git a/mosdns/patches/222-feat-stats_api-exclude-blocked-queries-from-cache-hi.patch b/mosdns/patches/222-feat-stats_api-exclude-blocked-queries-from-cache-hi.patch
new file mode 100644
index 00000000..9d115391
--- /dev/null
+++ b/mosdns/patches/222-feat-stats_api-exclude-blocked-queries-from-cache-hi.patch
@@ -0,0 +1,74 @@
+From 68799cf43340bf2e9e0b7145ea961d69ca45d6fe Mon Sep 17 00:00:00 2001
+From: sbwml
+Date: Mon, 31 Aug 2026 22:49:01 +0800
+Subject: [PATCH] feat(stats_api): exclude blocked queries from cache hit
+ percentage
+
+Calculate cache hit rate based on unblocked queries (total - blocked)
+instead of total queries to prevent ad blocking from skewing the stats.
+
+Signed-off-by: sbwml
+---
+ plugin/executable/stats_api/stats_api.go | 7 +++-
+ plugin/executable/stats_api/stats_api_test.go | 35 +++++++++++++++++++
+ 2 files changed, 41 insertions(+), 1 deletion(-)
+
+--- a/plugin/executable/stats_api/stats_api.go
++++ b/plugin/executable/stats_api/stats_api.go
+@@ -629,9 +629,14 @@ func (s *StatsAPI) handleStats(w http.Re
+ var blockedPct, cachedPct, avgLat float64
+ if total > 0 {
+ blockedPct = float64(blocked) / float64(total) * 100.0
+- cachedPct = float64(cached) / float64(total) * 100.0
+ avgLat = (float64(latUs) / float64(total)) / 1000.0
+ }
++ if total > blocked {
++ cachedPct = float64(cached) / float64(total-blocked) * 100.0
++ if cachedPct > 100.0 {
++ cachedPct = 100.0
++ }
++ }
+
+ blockedPct = math.Round(blockedPct*100) / 100
+ cachedPct = math.Round(cachedPct*100) / 100
+--- a/plugin/executable/stats_api/stats_api_test.go
++++ b/plugin/executable/stats_api/stats_api_test.go
+@@ -511,3 +511,38 @@ func TestStatsAPINonExistentDumpFile(t *
+ t.Errorf("expected 0 total queries, got %d", s.totalQueries.Load())
+ }
+ }
++
++func TestStatsAPICachedPercentageExcludesBlocked(t *testing.T) {
++ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop())
++ router := s.Router()
++
++ // 100 total queries: 50 blocked, 25 cached, 25 forwarded to upstream
++ s.totalQueries.Store(100)
++ s.blockedQueries.Store(50)
++ 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)
++ }
++
++ // blocked_percentage = 50 / 100 = 50%
++ if blockedPct := statsResp["blocked_percentage"].(float64); blockedPct != 50.0 {
++ t.Errorf("expected blocked_percentage 50.0, got %v", blockedPct)
++ }
++
++ // cached_percentage = 25 / (100 - 50) = 50% (previously 25%)
++ if cachedPct := statsResp["cached_percentage"].(float64); cachedPct != 50.0 {
++ t.Errorf("expected cached_percentage 50.0, got %v", cachedPct)
++ }
++}
++