diff --git a/firewall4/Makefile b/firewall4/Makefile index 3da0721bc..910deeb6b 100644 --- a/firewall4/Makefile +++ b/firewall4/Makefile @@ -5,7 +5,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=firewall4 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=$(PROJECT_GIT)/project/firewall4.git diff --git a/firewall4/patches/003-add-tcpcca-default-and-apply.patch b/firewall4/patches/003-add-tcpcca-default-and-apply.patch new file mode 100644 index 000000000..db3c3d476 --- /dev/null +++ b/firewall4/patches/003-add-tcpcca-default-and-apply.patch @@ -0,0 +1,77 @@ +From: coolsnowwolf +Subject: [PATCH] firewall4: apply the configured TCP congestion control + +Apply the LuCI tcpcca setting on service start, restart and reload, and +recognize it as a firewall defaults option. Keep the cubic default used +by the UI and only apply settings after a successful firewall operation. + +Signed-off-by: coolsnowwolf + +--- a/root/etc/config/firewall ++++ b/root/etc/config/firewall +@@ -5,6 +5,7 @@ + option forward REJECT + # Uncomment this line to disable ipv6 rules + # option disable_ipv6 1 ++ option tcpcca cubic + option flow_offloading 1 + option flow_offloading_hw 1 + option fullcone 1 +--- a/root/etc/init.d/firewall ++++ b/root/etc/init.d/firewall +@@ -4,17 +4,34 @@ + USE_PROCD=1 + QUIET="" + ++apply_tcpcca_section() { ++ local tcpcca ++ ++ config_get tcpcca "$1" tcpcca cubic ++ ++ case "$tcpcca" in ++ bbr|cubic|reno) ++ sysctl -w net.ipv4.tcp_congestion_control="$tcpcca" >/dev/null ++ ;; ++ esac ++} ++ ++apply_tcpcca() { ++ config_load firewall ++ config_foreach apply_tcpcca_section defaults ++} ++ + service_triggers() { + procd_add_reload_trigger firewall + procd_add_reload_data_trigger firewall + } + + restart() { +- fw4 restart ++ fw4 restart && apply_tcpcca + } + + start_service() { +- fw4 ${QUIET} start ++ fw4 ${QUIET} start && apply_tcpcca + } + + stop_service() { +@@ -22,7 +39,7 @@ + } + + reload_service() { +- fw4 reload ++ fw4 reload && apply_tcpcca + } + + boot() { +--- a/root/usr/share/ucode/fw4.uc ++++ b/root/usr/share/ucode/fw4.uc +@@ -2038,6 +2038,7 @@ + flow_offloading: [ "bool", "0" ], + flow_offloading_hw: [ "bool", "0" ], + ++ tcpcca: [ "string", "cubic" ], + auto_includes: [ "bool", "1" ] + }); + diff --git a/honk/Makefile b/honk/Makefile index 9f6b1ae92..d0c8e3697 100644 --- a/honk/Makefile +++ b/honk/Makefile @@ -5,11 +5,11 @@ include $(TOPDIR)/rules.mk PKG_NAME:=honk -PKG_VERSION:=2026.11.03~77176ff -PKG_RELEASE:=28 +PKG_VERSION:=2026.11.03~88d470c +PKG_RELEASE:=29 HONK_SRC_URL:=https://github.com/daeuniverse/honk.git -HONK_SRC_VERSION:=77176ff8e8cc2b75385a7030dc376536c382574a +HONK_SRC_VERSION:=88d470c662e0ee7a3de3e67afc6129f67e66c1bf PKG_LICENSE:=AGPL-3.0-only PKG_LICENSE_FILES:=LICENSE diff --git a/luci-app-honk/Makefile b/luci-app-honk/Makefile index ad8d338f0..36b7aa2de 100644 --- a/luci-app-honk/Makefile +++ b/luci-app-honk/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-honk PKG_VERSION:=1.1.4 -PKG_RELEASE:=32 +PKG_RELEASE:=33 LUCI_TITLE:=LuCI support for honk LUCI_PKGARCH:=all diff --git a/luci-app-honk/po/zh_Hans/honk.po b/luci-app-honk/po/zh_Hans/honk.po index e388c5c9c..a7a312c1c 100644 --- a/luci-app-honk/po/zh_Hans/honk.po +++ b/luci-app-honk/po/zh_Hans/honk.po @@ -362,3 +362,13 @@ msgstr "Zashboard" msgid "Saved, but the panel files are not installed yet — download the panel first." msgstr "配置已保存,但面板文件尚未安装,请先下载面板。" + +msgid "This page belongs to the advanced mode. Enable Advanced Settings in the global settings to use it." +msgstr "此页面属于高级模式,请先在全局设置中启用「高级设置」。" + +msgid "This page belongs to a web dashboard. Select one in the global settings to use it." +msgstr "此页面属于 Web 管理面板,请先在全局设置中选择面板类型。" + +msgid "Open Global Settings" +msgstr "前往全局设置" + diff --git a/luci-app-honk/root/www/luci-static/resources/honk/common.js b/luci-app-honk/root/www/luci-static/resources/honk/common.js index 4f931e932..31fbb8ed5 100644 --- a/luci-app-honk/root/www/luci-static/resources/honk/common.js +++ b/luci-app-honk/root/www/luci-static/resources/honk/common.js @@ -8,6 +8,7 @@ 'require dom'; 'require view'; 'require form'; +'require session'; function showNotification(title, children, type, timeout) { timeout = (timeout != null) ? timeout : 3000; @@ -332,27 +333,57 @@ function ensureEditorStyles() { '.honk-status-field { display: inline-flex !important; align-items: center !important; justify-content: flex-start !important; gap: 16px !important; flex-wrap: wrap !important; min-height: 32px !important; }', '.honk-editor-toolbar { margin-bottom: 6px !important; margin-top: 0 !important; display: flex !important; align-items: center !important; justify-content: flex-start !important; }', '.cm-format-btn { margin: 0 !important; cursor: pointer !important; }', - '.cbi-value:has(.CodeMirror) { align-items: flex-start !important; }', - '.cbi-value:has(.CodeMirror) > .cbi-value-title { padding-top: 5px !important; flex-grow: 0 !important; }', - // 26rem basis: when the row is narrower, the editor wraps under its title instead of being squeezed beside it - '.cbi-value:has(.CodeMirror) .cbi-value-field { flex: 1 1 26rem !important; min-width: 0 !important; width: auto !important; }', + // The editor is the content of its row: label on top, editor across the + // full width, toolbar above it instead of beside it. + '.honk-code-row { display: block !important; }', + '.honk-code-row > .cbi-value-title { display: block !important; width: auto !important; flex: none !important; padding: 0 0 6px 0 !important; }', + '.honk-code-row > .cbi-value-field { display: block !important; width: auto !important; flex: none !important; }', + '.honk-editor { display: block !important; width: 100% !important; min-width: 0 !important; }', + '.honk-editor > .CodeMirror { width: 100% !important; }', + // Colours: a theme variable when the theme has one, otherwise a palette that + // follows the color-scheme every LuCI theme sets. Without light-dark() + // support the plain declarations above each line are used, and without + // either CodeMirror keeps its own light palette. '.CodeMirror {', - ' border: 1px solid var(--border, var(--hairline, var(--border-color-medium, #ccc))) !important;', + ' border: 1px solid var(--border, #d5d9de) !important;', + ' border: 1px solid var(--border, light-dark(#d5d9de, #2a2f37)) !important;', ' border-radius: var(--radius-base, 4px);', ' height: auto;', ' min-height: 480px;', ' font-family: var(--font-mono, monospace);', ' font-size: 13px;', - ' background: var(--background, var(--control-bg, var(--background-color-high, #ffffff))) !important;', - ' color: var(--foreground, var(--text, var(--text-color-highest, inherit))) !important;', + ' background: var(--background, var(--background-color-high, #ffffff)) !important;', + ' background: var(--background, var(--background-color-high, light-dark(#ffffff, #16181c))) !important;', + ' color: var(--foreground, var(--text, var(--text-color-highest, #1f2328))) !important;', + ' color: var(--foreground, var(--text, var(--text-color-highest, light-dark(#1f2328, #e6e8ea)))) !important;', ' box-shadow: none;', '}', '.CodeMirror-gutters {', - ' border-right: 1px solid var(--border, var(--hairline, var(--border-color-medium, #ccc))) !important;', + ' border-right: 1px solid var(--border, light-dark(#e3e6ea, #262b33)) !important;', ' background: var(--surface-raised, var(--surface-sunken, var(--background-color-low, #f7f7f7))) !important;', + ' background: var(--surface-raised, var(--surface-sunken, var(--background-color-low, light-dark(#f7f7f7, #111317)))) !important;', '}', - '.CodeMirror-linenumber { color: var(--muted-foreground, var(--text-muted, var(--text-color-low, #888888))) !important; }', - '.CodeMirror-cursor { border-left: 1px solid var(--foreground, var(--text, currentColor)) !important; }' + '.CodeMirror-linenumber { color: var(--muted-foreground, var(--text-muted, #8a919b)) !important; }', + '.CodeMirror-linenumber { color: var(--muted-foreground, var(--text-muted, light-dark(#8a919b, #6b7686))) !important; }', + '.CodeMirror-cursor { border-left: 1px solid var(--foreground, var(--text, light-dark(#1f2328, #e6e8ea))) !important; }', + '.CodeMirror-activeline-background { background: light-dark(#f3f6f9, #1b2027) !important; }', + 'div.CodeMirror-selected { background: light-dark(#d9e2ec, #2b3440) !important; }', + '.CodeMirror-focused div.CodeMirror-selected { background: light-dark(#b9cfe7, #33414f) !important; }', + '.cm-s-default .cm-comment { color: light-dark(#a50, #7b8794) !important; }', + '.cm-s-default .cm-keyword { color: light-dark(#708, #c792ea) !important; }', + '.cm-s-default .cm-string { color: light-dark(#a11, #7ec699) !important; }', + '.cm-s-default .cm-number { color: light-dark(#164, #f78c6c) !important; }', + '.cm-s-default .cm-def { color: light-dark(#00f, #82aaff) !important; }', + '.cm-s-default .cm-variable { color: light-dark(#1f2328, #e6e8ea) !important; }', + '.cm-s-default .cm-variable-2 { color: light-dark(#05a, #82aaff) !important; }', + '.cm-s-default .cm-variable-3, .cm-s-default .cm-type { color: light-dark(#085, #7fdbca) !important; }', + '.cm-s-default .cm-operator { color: light-dark(#1f2328, #a8b3c2) !important; }', + '.cm-s-default .cm-bracket { color: light-dark(#997, #8b98a9) !important; }', + '.cm-s-default .cm-meta, .cm-s-default .cm-qualifier { color: light-dark(#555, #8b98a9) !important; }', + '.cm-s-default .cm-builtin { color: light-dark(#30a, #c792ea) !important; }', + '.cm-s-default .cm-tag { color: light-dark(#170, #7ec699) !important; }', + '.cm-s-default .cm-attribute { color: light-dark(#00c, #82aaff) !important; }', + '.CodeMirror-foldmarker { color: light-dark(#1f2328, #e6e8ea) !important; background: light-dark(#eef2f6, #262b33) !important; }' ].join('\n'); document.head.appendChild(style); } @@ -599,10 +630,21 @@ function initCodeMirror(textarea, onSaveCallback) { var toolbar = E('div', { 'class': 'honk-editor-toolbar' }, [ formatBtn ]); + // Toolbar and editor share one full width box: some themes lay the form + // field out as a flex row, which would otherwise squeeze the editor into + // the space left of the button instead of using the width of the row. var wrapper = editor.getWrapperElement(); - if (!wrapper.previousElementSibling || !wrapper.previousElementSibling.classList.contains('honk-editor-toolbar')) { - wrapper.parentNode.insertBefore(toolbar, wrapper); - } + var holder = E('div', { 'class': 'honk-editor' }); + + wrapper.parentNode.insertBefore(holder, wrapper); + holder.appendChild(toolbar); + holder.appendChild(wrapper); + + var row = textarea.closest('.cbi-value'); + if (row) + row.classList.add('honk-code-row'); + + editor.refresh(); if (window.IntersectionObserver) { var observer = new IntersectionObserver(function(entries) { @@ -627,6 +669,26 @@ function initCodeMirror(textarea, onSaveCallback) { }); } +// A page that is filtered out of the menu can still be opened by its URL; say +// why it is unavailable instead of rendering an editor that does not apply. +function renderHiddenPage(title, reason) { + var hint = (reason === 'advanced') + ? _('This page belongs to the advanced mode. Enable Advanced Settings in the global settings to use it.') + : _('This page belongs to a web dashboard. Select one in the global settings to use it.'); + + return E('div', { 'class': 'cbi-map' }, [ + E('style', {}, '.cbi-page-actions { display: none !important; }'), + E('h2', {}, title), + E('div', { 'class': 'cbi-section' }, [ + E('p', {}, hint), + E('p', {}, E('a', { + 'class': 'btn cbi-button cbi-button-action', + 'href': L.url('admin', 'services', 'honk', 'global') + }, _('Open Global Settings'))) + ]) + ]); +} + function createConfigFileView(filePath, mapTitle, mapDesc, fieldTitle, successMsg, needRestart) { return view.extend({ load: function() { @@ -634,6 +696,11 @@ function createConfigFileView(filePath, mapTitle, mapDesc, fieldTitle, successMs }, render: function() { + var reason = hiddenTabReason(currentTab()); + + if (reason) + return renderHiddenPage(mapTitle, reason); + var m = new form.Map('honk', mapTitle, mapDesc); var s = m.section(form.NamedSection, '_status'); @@ -700,73 +767,174 @@ function createConfigFileView(filePath, mapTitle, mapDesc, fieldTitle, successMs } -function updateTabVisibilityFromSections(sections) { - var s = (sections && sections[0]) ? sections[0] : (uci.get_first('honk', 'honk') || {}); - var isAdvanced = (s.advanced === '1'); - var dashType = s.dashboard || ''; +// --------------------------------------------------------------------------- +// Pages that do not apply to the current settings +// +// The DNS, node and routing editors belong to advanced mode, the API editor to +// a selected dashboard. menu.d cannot express that: its depends.uci matches +// option existence only, and the evaluated menu is cached in +// /tmp/luci-indexcache. The entries are filtered out here instead - in the menu +// tree every theme builds its navigation and page tabs from, plus the links of +// a bar a theme has already painted, matched by their target instead of by +// theme specific classes. +// --------------------------------------------------------------------------- - var hiddenTabs = []; - if (!isAdvanced) { - hiddenTabs.push('dns', 'node', 'route'); - } - if (dashType === 'none') { - hiddenTabs.push('api'); - } +var ADVANCED_TABS = [ 'dns', 'node', 'route' ]; +var MENU_BASE_KEY = 'honk-menu-base'; +var MENU_STATE_KEY = 'honk-menu-state'; - applyTabCss(hiddenTabs); - - var currentTab = (window.L && L.env && Array.isArray(L.env.dispatchpath)) ? L.env.dispatchpath[3] : ''; - if (!currentTab) { - var m = window.location.pathname.match(/\/honk\/([a-z0-9_-]+)/); - if (m) currentTab = m[1]; - } - if (currentTab && hiddenTabs.indexOf(currentTab) !== -1) { - window.location.href = L.url('admin/services/honk/global'); - } +function honkSettings() { + return uci.get_first('honk', 'honk') || {}; } -// Tab visibility control: -// - dns, node, route are hidden when advanced=0 -// - api is hidden when dashboard=none -function applyTabVisibility() { - var sections = uci.sections('honk', 'honk'); - if (sections && sections.length) { - updateTabVisibilityFromSections(sections); - return Promise.resolve(); - } +function getHiddenTabs() { + var settings = honkSettings(); + var hidden = []; + if (settings.advanced !== '1') + hidden = hidden.concat(ADVANCED_TABS); + + if (settings.dashboard === 'none') + hidden.push('api'); + + return hidden; +} + +// The setting that keeps a page hidden, or null when the page applies. +function hiddenTabReason(tab) { + var settings = honkSettings(); + + if (tab && ADVANCED_TABS.indexOf(tab) !== -1 && settings.advanced !== '1') + return 'advanced'; + + if (tab === 'api' && settings.dashboard === 'none') + return 'dashboard'; + + return null; +} + +function currentTab() { + var dispatch = (window.L && L.env && Array.isArray(L.env.dispatchpath)) ? L.env.dispatchpath : []; + var fromUrl = window.location.pathname.match(/\/honk\/([a-z0-9_-]+)/); + + return (dispatch.length > 3) ? dispatch[3] : (fromUrl ? fromUrl[1] : ''); +} + +function honkMenuNode(tree) { + var node = tree; + + [ 'admin', 'services', 'honk' ].forEach(function(name) { + node = (node && node.children) ? node.children[name] : null; + }); + + return node || null; +} + +// Filter the shared menu tree, and keep the filtered copy in LuCI's session +// cache so that the following page loads paint the same navigation. +function hideMenuEntries(hidden) { + var state = hidden.join(','); + + return ui.menu.load().then(function(tree) { + if (!L.isObject(tree) || !L.isObject(tree.children)) + return; + + var cached = session.getLocalData('menu'); + var base = session.getLocalData(MENU_BASE_KEY); + + // Keep the unfiltered tree, so that entries come back when the setting + // that hid them is enabled again. LuCI drops its own copy when packages + // change, and the tree fetched then is the new reference. + if (!L.isObject(cached) || !L.isObject(base)) { + base = JSON.parse(JSON.stringify(tree)); + session.setLocalData(MENU_BASE_KEY, base); + } + + if (L.isObject(cached) && session.getLocalData(MENU_STATE_KEY) === state) + return; + + var filtered = JSON.parse(JSON.stringify(base)); + var honk = honkMenuNode(filtered); + + if (honk && L.isObject(honk.children)) { + hidden.forEach(function(name) { + if (honk.children[name]) + honk.children[name].satisfied = false; + }); + } + + ui.menu.menu = filtered; + + session.setLocalData('menu', filtered); + session.setLocalData(MENU_STATE_KEY, state); + }).catch(function() {}); +} + +function hideTabLinks(hidden) { + if (!hidden.length) + return; + + var targets = hidden.map(function(name) { + return L.url('admin', 'services', 'honk', name); + }); + + document.querySelectorAll('a[href]').forEach(function(link) { + if (link.closest('#view') || targets.indexOf(link.pathname) === -1) + return; + + (link.closest('li') || link).style.setProperty('display', 'none', 'important'); + }); +} + +var lateLinks = null; + +// Themes paint their navigation once the menu promise settles - after this +// module may have been evaluated - and some of them rebuild parts of it when it +// is opened (the vantage flyout does), so keep watching for those links. +function watchLateLinks() { + if (lateLinks || !window.MutationObserver) + return; + + var queued = false; + + lateLinks = new MutationObserver(function() { + if (queued) + return; + + queued = true; + + window.setTimeout(function() { + queued = false; + hideTabLinks(getHiddenTabs()); + }, 100); + }); + + lateLinks.observe(document.body, { childList: true, subtree: true }); +} + +// dns, node and route are hidden when advanced=0, api when dashboard=none. +function applyTabVisibility() { return uci.load('honk').then(function() { - updateTabVisibilityFromSections(uci.sections('honk', 'honk')); + var hidden = getHiddenTabs(); + + hideTabLinks(hidden); + watchLateLinks(); + + return hideMenuEntries(hidden); }).catch(function() { }); } -function applyTabCss(hiddenTabs) { - var styleId = 'honk-tab-visibility-style'; - var existing = document.getElementById(styleId); - if (!existing) { - existing = document.createElement('style'); - existing.id = styleId; - document.head.appendChild(existing); - } - if (!hiddenTabs || hiddenTabs.length === 0) { - existing.textContent = ''; - } else { - existing.textContent = hiddenTabs.map(function(tab) { - return '#tabmenu .tabmenu-item-' + tab + ',\n' + - '.tabmenu-item-' + tab + ',\n' + - '#tabmenu a[href$="/honk/' + tab + '"]'; - }).join(',\n') + ' { display: none !important; }'; - } +// The service changes the stored settings on its own when the dashboard is +// switched, so re-read them before the page picks the menu it paints from. +function refreshTabVisibility() { + uci.unload('honk'); + + return applyTabVisibility(); } -// Early synchronous check if UCI data is already loaded in memory -try { - var _earlySections = uci.sections('honk', 'honk'); - if (_earlySections && _earlySections.length) { - updateTabVisibilityFromSections(_earlySections); - } -} catch (e) {} +// Start right away: the theme's navigation may still be on its way. +applyTabVisibility(); function renderStatusHeader() { var statusEl = E('span', { 'id': 'honk_status', 'style': 'font-weight: 500;' }, [ @@ -815,8 +983,6 @@ function renderStatusHeader() { addElementPoll(pollStatusFn, statusEl, 5); - applyTabVisibility(); - return section; } @@ -824,6 +990,7 @@ preloadCodeMirror(); return baseclass.extend({ applyTabVisibility: applyTabVisibility, + refreshTabVisibility: refreshTabVisibility, callHonkStatus: callHonkStatus, callHonkReload: callHonkReload, callHonkRestart: callHonkRestart, diff --git a/luci-app-honk/root/www/luci-static/resources/view/honk/dashboard.js b/luci-app-honk/root/www/luci-static/resources/view/honk/dashboard.js index 95240a443..45858dcc3 100644 --- a/luci-app-honk/root/www/luci-static/resources/view/honk/dashboard.js +++ b/luci-app-honk/root/www/luci-static/resources/view/honk/dashboard.js @@ -17,9 +17,6 @@ return view.extend({ }, render: function() { - if (honk && honk.applyTabVisibility) { - honk.applyTabVisibility(); - } var sec = (uci.sections('honk', 'honk')[0] || {}); var sid = sec['.name'] || 'config'; var dashType = uci.get('honk', sid, 'dashboard') || 'none'; @@ -43,7 +40,11 @@ return view.extend({ return; } - window.location.reload(); + // The switch changes the stored settings, and the reloaded page has + // to paint the tabs that apply to the new dashboard. + honk.refreshTabVisibility().then(function() { + window.location.reload(); + }); }).catch(function(err) { sel.disabled = false; honk.showNotification(null, E('p', _('Failed to switch dashboard:') + ' ' + (err.message || err)), 'error'); diff --git a/luci-app-honk/root/www/luci-static/resources/view/honk/global.js b/luci-app-honk/root/www/luci-static/resources/view/honk/global.js index d907184cf..25a76a0e4 100644 --- a/luci-app-honk/root/www/luci-static/resources/view/honk/global.js +++ b/luci-app-honk/root/www/luci-static/resources/view/honk/global.js @@ -63,6 +63,10 @@ return view.extend({ return this.handleSave(ev).then(function() { var newDash = uci.get('honk', sid, 'dashboard') || 'none'; + // The menu the reloaded page paints from has to match the new settings, + // and LuCI reloads before the view renders again. + honk.applyTabVisibility(); + var pre = (newDash !== oldDash) ? honk.switchDashboardAndWait(newDash).then(function(res) { if (res && res.pending) honk.showNotification(null, E('p', _('Saved, but the panel files are not installed yet — download the panel first.')), 'warning'); diff --git a/luci-app-honk/root/www/luci-static/resources/view/honk/log.js b/luci-app-honk/root/www/luci-static/resources/view/honk/log.js index 7f1af5358..ca1b9052e 100644 --- a/luci-app-honk/root/www/luci-static/resources/view/honk/log.js +++ b/luci-app-honk/root/www/luci-static/resources/view/honk/log.js @@ -14,10 +14,6 @@ return view.extend({ handleReset: null, render: function() { - if (honk && honk.applyTabVisibility) { - honk.applyTabVisibility(); - } - var scrolled = false; var logTextarea = E('textarea', { diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 4f8b0a0e5..df2db27a7 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=26.10.29 -PKG_RELEASE:=364 +PKG_VERSION:=26.11.3 +PKG_RELEASE:=365 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 715530521..054d5269d 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.10.29 -PKG_RELEASE:=165 +PKG_VERSION:=26.11.3 +PKG_RELEASE:=166 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile index 47c5bd140..48f37fd35 100644 --- a/luci-app-trafficctl/Makefile +++ b/luci-app-trafficctl/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-trafficctl -PKG_VERSION:=1.25.4 -PKG_RELEASE:=19 +PKG_VERSION:=1.26.1 +PKG_RELEASE:=20 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Denis Iusupov diff --git a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/history.js b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/history.js index 79c68b559..91695f8b1 100644 --- a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/history.js +++ b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/history.js @@ -32,6 +32,13 @@ var callHistoryRead = rpc.declare({ expect: { result: [] } }); +var callHistoryReadIfaces = rpc.declare({ + object: 'luci.trafficctl', + method: 'history_read_ifaces', + params: ['period', 'to'], + expect: { result: [] } +}); + var callHistoryFlush = rpc.declare({ object: 'luci.trafficctl', method: 'history_flush', @@ -86,6 +93,35 @@ var ACCURACY = { } }; +// Deliberately separate from ACCURACY. The interface series is not a view of +// the device series and its accuracy does not follow it: these bytes come from +// the kernel's own interface counters via /proc/net/dev, so flow offloading +// does not hide them. Under offload this table is the MORE trustworthy of the +// two, which is the inverse of the device table's verdict — inheriting it +// would be wrong in the direction that matters. +var IF_ACCURACY = { + 'exact': { + cls: 'tc-c-ok', + icon: '●', + text: _('Exact. These are the router\'s own interface counters, so flow offloading does not undercount them — with offloading on, this table is more trustworthy than the device table above. A link that goes down restarts its counter: the traffic it carried between the last sample and the restart is lost.') + } +}; + +function ifAccuracyOf(verdict) { + return IF_ACCURACY[verdict] || { + cls: 'tc-c-warn', + icon: '⚠', + text: _('The accuracy of these interface figures could not be determined.') + }; +} + +// '-' and '?' are the store's placeholders for an absent label and an unknown +// role (the file is tab-separated, so an empty field could not be empty). +function ifText(v) { + if (!v || v === '-' || v === '?') return ''; + return v; +} + function accuracyOf(verdict) { return ACCURACY[verdict] || { cls: 'tc-c-warn', @@ -102,6 +138,10 @@ return view.extend({ selTo: '', sortKey: 'total', tbody: null, + ifRows: [], + ifBody: null, + ifNoteEl: null, + ifWrapEl: null, statusEl: null, bannerEl: null, @@ -227,16 +267,100 @@ return view.extend({ ])); }, + // Fixed order rather than a sortable header: there are a handful of rows + // and the useful reading is "which WAN carried what", so WAN first. + ifSorted: function() { + var rank = { wan: 0, vpn: 1, lan: 2, other: 3 }; + return this.ifRows.slice().sort(function(a, b) { + var ra = rank[a.role], rb = rank[b.role]; + if (ra === undefined) ra = 4; + if (rb === undefined) rb = 4; + if (ra !== rb) return ra - rb; + return ((b.bytes_in || 0) + (b.bytes_out || 0)) - + ((a.bytes_in || 0) + (a.bytes_out || 0)); + }); + }, + + renderIfRow: function(r) { + var label = ifText(r.label) || r.dev; + var role = ifText(r.role); + var name = [E('span', { 'class': 'tc-fw-bold' }, label)]; + // The device name is what appears in logs and in `ip link`, so it is + // worth showing even when a friendlier label exists. + if (label !== r.dev) { + name.push(E('span', { 'class': 'tc-mono tc-sm tc-c-faint', + 'style': 'margin-left:6px' }, r.dev)); + } + if (role) { + name.push(E('span', { 'class': 'tc-c-muted tc-sm', + 'style': 'margin-left:6px' }, role)); + } + var tot = (r.bytes_in || 0) + (r.bytes_out || 0); + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, name), + E('td', { 'class': 'td tc-right tc-mono tc-sm' }, fmtBytes(r.bytes_in || 0)), + E('td', { 'class': 'td tc-right tc-mono tc-sm' }, fmtBytes(r.bytes_out || 0)), + E('td', { 'class': 'td tc-right tc-mono tc-fw-bold' }, fmtBytes(tot)), + E('td', { 'class': 'td tc-center tc-sm tc-c-muted' }, fmtAgo(r.last_seen)) + ]); + }, + + // No total row, unlike the device table. These rows overlap by design: a + // VPN tunnel's bytes also travelled over its WAN, and the LAN bridge sees + // the same traffic from the other side. A sum would be a number with no + // meaning, presented as if it were the period's total. + renderIfTable: function() { + var self = this; + while (this.ifBody.firstChild) this.ifBody.removeChild(this.ifBody.firstChild); + if (!this.ifRows.length) { + this.ifBody.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td tc-c-faint', 'colspan': 5 }, + _('No interface traffic recorded for this period yet.')) + ])); + return; + } + this.ifSorted().forEach(function(r) { + self.ifBody.appendChild(self.renderIfRow(r)); + }); + }, + + // Hidden entirely when the series is off or history is: an empty table + // with a note about accuracy would be worse than no table. + renderIfSection: function() { + if (!this.ifWrapEl) return; + var on = !!this.st.enabled && this.st.interfaces !== false; + this.ifWrapEl.classList.toggle('tc-hidden', !on); + if (!on) return; + var a = ifAccuracyOf(this.st.iface_accuracy); + while (this.ifNoteEl.firstChild) this.ifNoteEl.removeChild(this.ifNoteEl.firstChild); + this.ifNoteEl.appendChild(E('div', { 'class': a.cls }, [ + E('span', { 'class': 'tc-fw-bold' }, a.icon + ' '), + document.createTextNode(a.text) + ])); + }, + loadPeriod: function() { var self = this; self.statusEl.textContent = _('Loading…'); - return callHistoryRead(self.sel || '', self.selTo || '').then(function(rows) { - self.rows = rows || []; + // Each series catches its own failure: one of them being unreadable + // must not blank the other, since they come from different files. + var wantIf = self.st.interfaces !== false; + return Promise.all([ + callHistoryRead(self.sel || '', self.selTo || '') + .then(function(rows) { self.rows = rows || []; }) + .catch(function(e) { + self.rows = []; + ui.addNotification(null, E('p', e.message), 'error'); + }), + wantIf ? callHistoryReadIfaces(self.sel || '', self.selTo || '') + .then(function(rows) { self.ifRows = rows || []; }) + .catch(function() { self.ifRows = []; }) + : Promise.resolve() + ]).then(function() { self.statusEl.textContent = ''; self.renderTable(); - }).catch(function(e) { - self.statusEl.textContent = ''; - ui.addNotification(null, E('p', e.message), 'error'); + self.renderIfTable(); + self.renderIfSection(); }); }, @@ -247,6 +371,7 @@ return view.extend({ self.st = res[0] || {}; self.periods = res[1] || []; self.renderBanner(); + self.renderIfSection(); }).catch(function() {}); }, @@ -405,8 +530,36 @@ return view.extend({ var table = E('table', { 'class': 'table tc-table' }, [thead, this.tbody]); + // ── interfaces: a second series, below the first ──────────────────── + this.ifBody = E('tbody', {}); + this.ifNoteEl = E('div', { 'style': 'margin-bottom:6px' }); + this.ifWrapEl = E('div', { 'class': 'tc-hidden', 'style': 'margin-top:18px' }, [ + E('h3', {}, _('Per-interface totals')), + E('div', { 'class': 'cbi-map-descr' }, + _('Which link carried the traffic — the question the device table cannot answer. This is a separate series, not a breakdown of it: the rows include the router\'s own traffic and broadcast, and they overlap each other (a VPN tunnel\'s bytes also travelled over its WAN), so the two tables are not meant to add up. Received and Sent are relative to the interface, so on the LAN bridge Received is what your devices sent.')), + this.ifNoteEl, + E('table', { 'class': 'table tc-table' }, [ + E('thead', {}, [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th' }, _('Interface')), + E('th', { 'class': 'th tc-right', + 'title': _('Bytes the interface received. On a WAN that is download; on the LAN bridge it is what your devices uploaded.') }, + _('Received')), + E('th', { 'class': 'th tc-right', + 'title': _('Bytes the interface sent. On a WAN that is upload; on the LAN bridge it is what your devices downloaded.') }, + _('Sent')), + E('th', { 'class': 'th tc-right' }, _('Total')), + E('th', { 'class': 'th tc-center' }, _('Last active')) + ]) + ]), + this.ifBody + ]) + ]); + this.renderBanner(); this.renderTable(); + this.renderIfTable(); + this.renderIfSection(); if (this.st.enabled) this.loadPeriod(); // ── controls ──────────────────────────────────────────────────────── @@ -425,7 +578,10 @@ return view.extend({ enableBtn.textContent = self.st.enabled ? _('Switch off') : _('Switch on'); if (self.st.enabled) return self.loadPeriod(); self.rows = []; + self.ifRows = []; self.renderTable(); + self.renderIfTable(); + self.renderIfSection(); }); }).catch(function(e) { enableBtn.disabled = false; @@ -468,7 +624,8 @@ return view.extend({ this.bannerEl, E('div', { 'style': 'margin-bottom:8px' }, [enableBtn, flushBtn, csvBtn]), picker, - table + table, + this.ifWrapEl ]); } }); diff --git a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js index 983bb21f4..e85d87fce 100644 --- a/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js +++ b/luci-app-trafficctl/htdocs/luci-static/resources/view/trafficctl/status.js @@ -1113,6 +1113,18 @@ function buildGroupedTable(groups, sortCol, sortDir) { return E('div', { 'class': 'table tc-table' }, [titleRow].concat(rows)); } +// 'oif' is the uci interface name when the router has one, 'oif_dev' the +// kernel device it resolved to. Same layout as the overview rows: the device +// is only worth showing when it differs from the name (#10). +function oifCell(r) { + if (!r.oif) return E('div', { 'class': 'td tc-mono' }, E('span', { 'class': 'tc-c-faint' }, '—')); + var cell = E('div', { 'class': 'td tc-mono' }, escHtml(r.oif)); + if (r.oif_dev && r.oif_dev !== r.oif) { + cell.appendChild(E('span', { 'class': 'tc-c-faint tc-ov-ifdev' }, ' ' + escHtml(r.oif_dev))); + } + return cell; +} + function buildTable(conns, sortCol, sortDir, rdnsMode, hiddenCols) { var allCols = [ { key:'proto', label: _('Proto'), num:false }, @@ -1166,7 +1178,7 @@ function buildTable(conns, sortCol, sortDir, rdnsMode, hiddenCols) { service: E('div', { 'class': 'td tc-c-speed' }, escHtml(r.service || (SERVICE_PORTS[r.port]||''))), bytes: E('div', { 'class': 'td tc-right tc-mono tc-fw-bold' }, fmtBytes(r.bytes)), state: E('div', { 'class': 'td tc-fw-bold' + scCls }, state), - oif: E('div', { 'class': 'td tc-mono' }, r.oif ? escHtml(r.oif) : E('span', { 'class': 'tc-c-faint' }, '—')) + oif: oifCell(r) }; var cells = cols.map(function(c) { return cellMap[c.key]; }); return E('div', { 'class': 'tr' }, cells); diff --git a/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot b/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot index c480f7c8b..71b6d798d 100644 --- a/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot +++ b/luci-app-trafficctl/po/templates/luci-app-trafficctl.pot @@ -181,6 +181,12 @@ msgstr "" msgid "Bytes sent back to remote clients" msgstr "" +msgid "Bytes the interface received. On a WAN that is download; on the LAN bridge it is what your devices uploaded." +msgstr "" + +msgid "Bytes the interface sent. On a WAN that is upload; on the LAN bridge it is what your devices downloaded." +msgstr "" + msgid "CPU load (1 min)" msgstr "" @@ -352,6 +358,9 @@ msgstr "" msgid "Exact. Byte counts come from cumulative nftables counters, so they do not depend on how often the sampler runs. The TCP/UDP split is unavailable from this source." msgstr "" +msgid "Exact. These are the router's own interface counters, so flow offloading does not undercount them — with offloading on, this table is more trustworthy than the device table above. A link that goes down restarts its counter: the traffic it carried between the last sample and the restart is lost." +msgstr "" + msgid "Export CSV" msgstr "" @@ -484,6 +493,9 @@ msgstr "" msgid "Inline keyboard preview:" msgstr "" +msgid "Interface" +msgstr "" + msgid "Interfaces" msgstr "" @@ -508,6 +520,9 @@ msgstr "" msgid "LAN keeps working" msgstr "" +msgid "Last active" +msgstr "" + msgid "Last seen" msgstr "" @@ -622,6 +637,9 @@ msgstr "" msgid "No extended stats available." msgstr "" +msgid "No interface traffic recorded for this period yet." +msgstr "" + msgid "No offload" msgstr "" @@ -721,6 +739,9 @@ msgstr "" msgid "Per-device totals accumulated per accounting period, including devices that are not connected now. Figures survive reboots; the live view on the Devices tab does not." msgstr "" +msgid "Per-interface totals" +msgstr "" + msgid "Period" msgstr "" @@ -781,6 +802,9 @@ msgstr "" msgid "Ratelimits" msgstr "" +msgid "Received" +msgstr "" + msgid "Received by this interface" msgstr "" @@ -850,6 +874,9 @@ msgstr "" msgid "Sending…" msgstr "" +msgid "Sent" +msgstr "" + msgid "Sent by this interface" msgstr "" @@ -994,6 +1021,9 @@ msgstr "" msgid "The accuracy of these figures could not be determined." msgstr "" +msgid "The accuracy of these interface figures could not be determined." +msgstr "" + msgid "The auto-revert helper is not running; this page re-checks the deadline while it is open." msgstr "" @@ -1132,6 +1162,9 @@ msgstr "" msgid "When this device last moved any traffic, not when it was last connected" msgstr "" +msgid "Which link carried the traffic — the question the device table cannot answer. This is a separate series, not a breakdown of it: the rows include the router's own traffic and broadcast, and they overlap each other (a VPN tunnel's bytes also travelled over its WAN), so the two tables are not meant to add up. Received and Sent are relative to the interface, so on the LAN bridge Received is what your devices sent." +msgstr "" + msgid "While this total was being accumulated, flow offloading froze the counters it is built from. The figure is a floor, not a measurement." msgstr "" diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-device.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-device.sh index c8acbafca..1b4af29ee 100755 --- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-device.sh +++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-device.sh @@ -279,11 +279,29 @@ if command -v ip >/dev/null 2>&1; then done fi -CONNS_OUT=$(echo "$CONNTRACK_DATA" | awk -v ip="$IP" -v pf="$PROTO_FILTER" -v oifmap="$OIF_MAP" ' +# The kernel device is not what the user named the interface: mwan3 egress is +# "awg3", the network is "Finland" (#10). The uci name comes from the role map +# trafficctl-ifaces.sh already memoizes in tmpfs — the overview polls it, so it +# is normally warm. Built here only when it is missing and there is something +# to label, never per connection. +IFROLE_CACHE="${TCTL_IFROLE_CACHE:-/tmp/trafficctl_ifroles}" +if [ -s "$OIF_MAP" ] && [ ! -s "$IFROLE_CACHE" ]; then + /usr/local/bin/trafficctl-ifaces.sh >/dev/null 2>&1 +fi + +CONNS_OUT=$(echo "$CONNTRACK_DATA" | awk -v ip="$IP" -v pf="$PROTO_FILTER" -v oifmap="$OIF_MAP" -v ifroles="$IFROLE_CACHE" ' BEGIN { n=0 while ((getline line < oifmap) > 0) { split(line, p, " "); oifdev[p[1]] = p[2] } close(oifmap) + # "dev role tunnel primary defroute enslaved label", label "-" when the + # device has no uci interface. Header lines start with "#". + while ((getline line < ifroles) > 0) { + if (substr(line, 1, 1) == "#") continue + if (split(line, f, " ") < 7 || f[7] == "-") continue + oiflbl[f[1]] = f[7] + } + close(ifroles) } { proto="" @@ -325,10 +343,11 @@ BEGIN { else if (dport == "993") svc="imaps" else if (dport == "8080") svc="http-alt" - oif = "" - if (dst in oifdev) oif = oifdev[dst] + odev = ""; oif = "" + if (dst in oifdev) { odev = oifdev[dst]; oif = odev } + if (odev in oiflbl) oif = oiflbl[odev] if (n > 0) printf "," - printf "{\"proto\":\"%s\",\"dst\":\"%s\",\"host\":\"\",\"port\":%s,\"service\":\"%s\",\"bytes\":%.0f,\"state\":\"%s\",\"oif\":\"%s\"}", proto, dst, dport, svc, bytes, state, oif + printf "{\"proto\":\"%s\",\"dst\":\"%s\",\"host\":\"\",\"port\":%s,\"service\":\"%s\",\"bytes\":%.0f,\"state\":\"%s\",\"oif\":\"%s\",\"oif_dev\":\"%s\"}", proto, dst, dport, svc, bytes, state, oif, odev n++ } ') diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh index 269774178..7288d015f 100755 --- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh +++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh @@ -129,7 +129,7 @@ tctl_ratelimit_add() { local rate_kbyte_up=$((rate_kbit_up / 8)) [ "$rate_kbyte_up" -lt 1 ] && rate_kbyte_up=1 - local slug dl_expr ul_expr ul6_expr mac + local slug dl_expr ul_expr ul6_expr dl6_expr feed6_expr v6set mac slug=$(tctl_target_slug "$ip") # The two directions were already separate rules on separate hooks — they # were symmetric only because both took the same number. @@ -141,12 +141,9 @@ tctl_ratelimit_add() { ul_expr="ip saddr $ip limit rate over ${rate_kbyte_up} kbytes/second" fi - # Upload over IPv6, keyed on the MAC. This direction and only this one: - # at LAN ingress the frame is still the client's own, so "ether saddr" - # identifies it whatever address family it is using. Download over IPv6 is - # policed at LAN egress, where the destination MAC is the next hop's — it - # needs the device's current v6 address set and is deliberately not - # attempted here. + # Upload over IPv6, keyed on the MAC: at LAN ingress the frame is still + # the client's own, so "ether saddr" identifies it whatever address family + # it is using. # # Scoped to IPv6 on purpose: IPv4 keeps being policed by exactly the rule it # always was, so nothing about existing behaviour depends on whether this @@ -161,13 +158,41 @@ tctl_ratelimit_add() { # against nftables 1.1.1 / kernel 6.6). nfproto is what tctl_block_add uses, # because that rule lives in inet fw4. Same intent, different family, and # the wrong one does not silently under-match — it fails to load. - ul6_expr="" + # + # Download over IPv6 cannot be keyed on the MAC: at LAN egress the L2 + # header is not built yet. It is matched on the device's v6 ADDRESSES, + # which is only sound if the set of them can never go stale — SLAAC + # privacy extensions rotate them, and a set that misses the current one + # is a silent bypass. So the set is not written from a neighbour-table + # snapshot that something would have to keep refreshing; the data path + # fills it. Every v6 packet the client sends crosses LAN ingress with its + # own MAC, and the feed rule there adds its source address with a timeout + # ("update" re-arms it on every packet, so an address in use never + # expires). A new temporary address is in the set from its first packet, + # one that stopped being used drops out after the timeout. Download that + # no packet from the client ever preceded is the only thing that is not + # covered, and for TCP there is none. Link-local is left out: it never + # crosses the router. + # + # Verified on kernel 6.12 / nftables 1.0.9 with a client in its own netns + # behind a bridge: one ping from a privacy address put that address in the + # set, and a 5 Mbit/s limit held an iperf3 download to it at ~6 Mbit/s. + ul6_expr=""; dl6_expr=""; feed6_expr="" + v6set="tctl_v6_$slug" if mac=$(tctl_target_mac "$ip"); then ul6_expr="meta protocol ip6 ether saddr $mac limit rate over ${rate_kbyte_up} kbytes/second" + feed6_expr="meta protocol ip6 ether saddr $mac ip6 saddr != fe80::/10 update @$v6set { ip6 saddr }" + dl6_expr="meta protocol ip6 ip6 daddr @$v6set limit rate over ${rate_kbyte} kbytes/second" fi if [ "$TCTL_FW" = "nft" ]; then nft add table netdev tm_ratelimit 2>/dev/null + # The set has to exist before any rule that names it. Without it the + # v6 download half is skipped and reported as such, never half-built. + if [ -n "$dl6_expr" ] && ! nft add set netdev tm_ratelimit "$v6set" \ + "{ type ipv6_addr; flags dynamic,timeout; timeout ${TCTL_V6SET_TIMEOUT:-1h}; }" 2>/dev/null; then + feed6_expr=""; dl6_expr="" + fi # Download is policed at LAN EGRESS, not WAN ingress. # @@ -183,7 +208,7 @@ tctl_ratelimit_add() { # the IP stack, and the port-level egress hook never fires for it. # Verified on kernel 6.12: br-lan egress counted every packet, the # ports counted none. - local dev chain dl_ok=0 wan_dev + local dev chain dl_ok=0 dl6_ok=0 wan_dev for dev in $(tctl_get_lan_devices); do chain=$(tctl_egress_chain "$dev") nft add chain netdev tm_ratelimit "$chain" \ @@ -191,15 +216,23 @@ tctl_ratelimit_add() { nft add rule netdev tm_ratelimit "$chain" \ "$dl_expr counter drop comment \"$comment\"" 2>/dev/null \ && dl_ok=1 + [ -n "$dl6_expr" ] || continue + nft add rule netdev tm_ratelimit "$chain" \ + "$dl6_expr counter drop comment \"${comment}_dl6\"" 2>/dev/null \ + && dl6_ok=1 done # Kernels older than 5.16 have no netdev egress hook; fall back to WAN # ingress, which is correct as long as the client isn't masqueraded. + # IPv6 is not masqueraded at all, so the v6 rule is correct there too. if [ "$dl_ok" = "0" ] && wan_dev=$(tctl_get_wan_device); then nft add chain netdev tm_ratelimit dl \ "{ type filter hook ingress device $wan_dev priority -200; policy accept; }" 2>/dev/null nft add rule netdev tm_ratelimit dl \ "$dl_expr counter drop comment \"$comment\"" 2>/dev/null \ && dl_ok=1 + [ -n "$dl6_expr" ] && nft add rule netdev tm_ratelimit dl \ + "$dl6_expr counter drop comment \"${comment}_dl6\"" 2>/dev/null \ + && dl6_ok=1 fi # Set for the caller to inspect (see trafficctl-ratelimit.sh). # shellcheck disable=SC2034 @@ -208,7 +241,7 @@ tctl_ratelimit_add() { # One chain per ingress device: a device that refuses the hook then # only loses its own chain instead of taking the whole set with it. local dev chain - local ul_ok=0 ul6_ok=0 + local ul_ok=0 ul6_ok=0 feed6_ok=0 for dev in $(tctl_ingress_devices); do chain=$(tctl_ingress_chain "$dev") nft add chain netdev tm_ratelimit "$chain" \ @@ -217,6 +250,11 @@ tctl_ratelimit_add() { "$ul_expr counter drop comment \"${comment}_ul\"" 2>/dev/null \ && ul_ok=1 [ -n "$ul6_expr" ] || continue + # Feed before police, so a packet the upload limit drops still + # keeps its address in the download set. + [ -n "$feed6_expr" ] && nft add rule netdev tm_ratelimit "$chain" \ + "$feed6_expr comment \"${comment}_v6\"" 2>/dev/null \ + && feed6_ok=1 nft add rule netdev tm_ratelimit "$chain" \ "$ul6_expr counter drop comment \"${comment}_ul6\"" 2>/dev/null \ && ul6_ok=1 @@ -226,11 +264,21 @@ tctl_ratelimit_add() { # Set for the caller to report: "limited" means v4 only unless this is 1. # shellcheck disable=SC2034 TCTL_RL_UPLOAD6_OK="$ul6_ok" + # A download rule over a set nothing feeds matches nothing, so it only + # counts when both halves loaded. + # shellcheck disable=SC2034 + if [ "$dl6_ok" = "1" ] && [ "$feed6_ok" = "1" ]; then + TCTL_RL_DOWNLOAD6_OK=1 + else + TCTL_RL_DOWNLOAD6_OK=0 + fi else # fw3/iptables stays IPv4-only. A half-built ip6tables path would be # worse than a stated gap; see docs/COMPATIBILITY.md. # shellcheck disable=SC2034 TCTL_RL_UPLOAD6_OK=0 + # shellcheck disable=SC2034 + TCTL_RL_DOWNLOAD6_OK=0 iptables -t mangle -A FORWARD -d "$ip" -m hashlimit \ --hashlimit-above "${rate_kbit}kbit/sec" --hashlimit-burst "${rate_kbit}kbit" \ --hashlimit-mode dstip --hashlimit-name "rl_${comment}" \ @@ -247,9 +295,11 @@ tctl_ratelimit_remove() { local chain h if [ "$TCTL_FW" = "nft" ]; then - # Scan the whole table once: rules for this IP live in dl (daddr), in - # one ul_ chain per ingress device (saddr), and — for a host with - # a known MAC — a second, IPv6-scoped rule in each of those (_ul6). + # Scan the whole table once: rules for this IP live in the dl chains + # (daddr, plus _dl6 over the v6 address set), in one ul_ chain per + # ingress device (saddr), and — for a host with a known MAC — the + # IPv6-scoped _ul6 rule and the _v6 rule feeding the set in each of + # those. # # Each comment is matched in FULL, closing quote included. The suffixes # are prefixes of one another ("_ul" of "_ul6"), and every comment ends @@ -258,12 +308,15 @@ tctl_ratelimit_remove() { nft -a list table netdev tm_ratelimit 2>/dev/null | awk -v cmt="$comment" ' /^[ \t]*chain [a-zA-Z0-9_]+ \{/ { chain = $2; next } index($0, "\"" cmt "\"") || index($0, "\"" cmt "_ul\"") || - index($0, "\"" cmt "_ul6\"") { + index($0, "\"" cmt "_ul6\"") || index($0, "\"" cmt "_dl6\"") || + index($0, "\"" cmt "_v6\"") { for (i = 1; i < NF; i++) if ($i == "handle") { print chain, $(i+1); break } }' | while read -r chain h; do [ -n "$chain" ] && [ -n "$h" ] && nft delete rule netdev tm_ratelimit "$chain" handle "$h" 2>/dev/null done + # Only once nothing references it: nft refuses to delete a set in use. + nft delete set netdev tm_ratelimit "tctl_v6_$(tctl_target_slug "$ip")" 2>/dev/null else while iptables -t mangle -D FORWARD -d "$ip" -m comment --comment "$comment" 2>/dev/null; do :; done while iptables -t mangle -D FORWARD -s "$ip" -m comment --comment "${comment}_ul" 2>/dev/null; do :; done @@ -931,6 +984,33 @@ TCTL_SEEN_MAX=1000 # TCTL_LEASES_FILE is set once at the top of this file (overridable for tests). TCTL_SHAPES_FILE="/etc/trafficctl/shapes.json" +# " " for every shape in shapes.json — the only way from an +# address to its HTB class, because minors are ALLOCATED (see +# trafficctl-shape.sh), not derived from the address. Readers that derived +# them anyway showed one device's rate on another. +# +# Same record pattern as shapes_entries in trafficctl-shape.sh, optional +# rate_kbit_up included: a pattern that misses a record form makes that shape +# vanish from every reader. A record from before allocation carries no +# classid, and its class is still the one the old derivation gave it +# (legacy_classid there), so that is what it maps to. +tctl_shape_classmap() { + grep -oE '\{"ip":"[^"]+","rate_kbit":[0-9]+(,"classid":"1:[0-9a-f]+")?(,"rate_kbit_up":[0-9]+)?\}' \ + "$TCTL_SHAPES_FILE" 2>/dev/null | awk ' + { + if (!match($0, /"ip":"[^"]+"/)) next + ip = substr($0, RSTART + 6, RLENGTH - 7) + if (match($0, /"classid":"1:[0-9a-f]+"/)) { + print ip, substr($0, RSTART + 11, RLENGTH - 12) + next + } + if (split(ip, o, ".") != 4) next + dec = o[3] * 256 + o[4] + if (dec < 2 || dec > 65533) next + printf "%s 1:%x\n", ip, dec + }' +} + tctl_seen_normalize() { printf '%s' "$1" | tr 'A-Z' 'a-z' } diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh index 8e55d4337..0b3b8b048 100755 --- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh +++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh @@ -70,17 +70,24 @@ else TCTL_RL_DOWNLOAD_FAILED=0 TCTL_RL_UPLOAD_FAILED=0 TCTL_RL_UPLOAD6_OK=0 + TCTL_RL_DOWNLOAD6_OK=0 tctl_ratelimit_add "$IP" "$RATE" "$COMMENT" "$MODE" "$RATE_UP" - # IPv6 coverage is partial by construction and the operator has to know - # which part: upload is policed on the MAC, download is not policed at all - # over v6 (see tctl_ratelimit_add). Claiming "both directions" without - # this note is how issue #67 stayed invisible. - if [ "$TCTL_RL_UPLOAD6_OK" = "1" ]; then + # IPv6 coverage depends on whether the target has a MAC to key on, and + # each half can fail to load on its own. The operator has to know which + # part is live: claiming "both directions" without this note is how issue + # #67 stayed invisible. + if [ "$TCTL_RL_UPLOAD6_OK" = "1" ] && [ "$TCTL_RL_DOWNLOAD6_OK" = "1" ]; then + V6NOTE=" [IPv6: both directions]" + elif [ "$TCTL_RL_UPLOAD6_OK" = "1" ]; then V6NOTE=" [IPv6: upload only]" + elif [ "$TCTL_RL_DOWNLOAD6_OK" = "1" ]; then + V6NOTE=" [IPv6: download only]" else V6NOTE=" [IPv4 only]" fi + V6UP=$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false) + V6DN=$([ "$TCTL_RL_DOWNLOAD6_OK" = "1" ] && echo true || echo false) # A half-applied limit is a silent trap: report exactly which direction # is live rather than claiming success for both. @@ -108,10 +115,12 @@ else NSS="$(tctl_nss_status)" PERSIST=$(tctl_persist_enabled && echo true || echo false) if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then - echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}" + echo "{\"ok\":true,\"ipv6_upload\":$V6UP,\"ipv6_download\":$V6DN,\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}" elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then - echo "{\"ok\":true,\"ipv6_upload\":false,\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}" + # No ingress device means nothing feeds the v6 download set either, so + # tctl_ratelimit_add has already reported that half as not live. + echo "{\"ok\":true,\"ipv6_upload\":false,\"ipv6_download\":false,\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}" else - echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}" + echo "{\"ok\":true,\"ipv6_upload\":$V6UP,\"ipv6_download\":$V6DN,\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}" fi fi diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape-stats.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape-stats.sh index d6c093b2e..509b47cbf 100755 --- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape-stats.sh +++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-shape-stats.sh @@ -21,9 +21,14 @@ fi # Map classid → real IP from persisted shapes. Minors are allocated, not derived # from the address, so this map is the only way back to an IP; classes missing # from it belong to another tc user and are skipped rather than mislabelled. -CLASSMAP=$(grep -oE '\{"ip":"[^"]+","rate_kbit":[0-9]+,"classid":"1:[0-9a-f]+"\}' \ - /etc/trafficctl/shapes.json 2>/dev/null | \ - sed -n 's/{"ip":"\([^"]*\)","rate_kbit":[0-9]*,"classid":"\(1:[0-9a-f]*\)"}/\2 \1/p') +# The pattern used to require the record to end at the classid, so every shape +# with its own upload ceiling ("rate_kbit_up") was dropped from this output, +# and with it from tctl_has_limit. +# +# Flattened to ONE "classid ip classid ip …" line: a multi-line value passed +# through `awk -v` is rejected by some awks ("newline in string"), which left +# this output empty. Neither field can contain a space. +CLASSMAP=$(tctl_shape_classmap | awk '{ printf "%s %s ", $2, $1 }') # Collect class stats into a temp file so we can merge with qdisc stats CLASS_DATA=$(tc -s class show dev "$LAN_DEV" 2>/dev/null) @@ -32,11 +37,9 @@ QDISC_DATA=$(tc -s qdisc show dev "$LAN_DEV" 2>/dev/null) # Parse class stats: emit lines "classid ip rate bytes pkts backlog drops overlimits requeues lended borrowed" CLASS_PARSED=$(echo "$CLASS_DATA" | awk -v classmap="$CLASSMAP" ' BEGIN { - n = split(classmap, cml, "\n") - for (ci = 1; ci <= n; ci++) { - split(cml[ci], cmf, " ") - if (cmf[1] != "" && cmf[2] != "") ipmap[cmf[1]] = cmf[2] - } + n = split(classmap, cmf, " ") + for (ci = 1; ci + 1 <= n; ci += 2) + if (cmf[ci] != "" && cmf[ci + 1] != "") ipmap[cmf[ci]] = cmf[ci + 1] } /class fq_codel/ { skip = 1; next } diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-summary.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-summary.sh index 518cf064f..5a5c2bc3e 100755 --- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-summary.sh +++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-summary.sh @@ -242,6 +242,9 @@ if command -v tc >/dev/null 2>&1; then }') fi +# address → classid, from shapes.json (see tctl_shape_classmap). +SHAPE_MAP=$(tctl_shape_classmap) + # Wifi ACL state, split by policy, because "listed" means opposite things in the # two modes: on a deny (blacklist) radio a listed MAC is blocked, on an allow # (whitelist) radio a listed MAC is the only kind permitted. Collected once here @@ -355,14 +358,14 @@ lookup_rate_limit() { echo "${r:-0}" } +# The classid is looked up, never derived from the address: minors are +# allocated, so deriving put the first shaped device's rate on 192.168.x.2 and +# showed 0 for the device actually shaped — and the Telegram bot built its +# "Shape — Remove" button from this field. lookup_shape_kbit() { - local ip="$1" o3 o4 dec hex classid r - o3=$(echo "$ip" | cut -d. -f3) - o4=$(echo "$ip" | cut -d. -f4) - dec=$((o3 * 256 + o4)) - hex=$(printf "%x" "$dec") - case "$hex" in 1|fffe) echo 0; return ;; esac - classid="1:$hex" + local ip="$1" classid r + classid=$(echo "$SHAPE_MAP" | awk -v ip="$ip" '$1==ip{print $2; exit}') + [ -n "$classid" ] || { echo 0; return; } r=$(echo "$TC_MAP" | awk -v c="$classid" '$1==c{print $2; exit}') echo "${r:-0}" } diff --git a/luci-app-vantage/Makefile b/luci-app-vantage/Makefile index 2fb56b2a4..4a95e0001 100644 --- a/luci-app-vantage/Makefile +++ b/luci-app-vantage/Makefile @@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-vantage PKG_VERSION:=1.0.14 -PKG_RELEASE:=21 +PKG_RELEASE:=22 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage dashboard diff --git a/luci-app-vantage/po/templates/vantage.pot b/luci-app-vantage/po/templates/vantage.pot index d7a63b8a8..94867b30b 100644 --- a/luci-app-vantage/po/templates/vantage.pot +++ b/luci-app-vantage/po/templates/vantage.pot @@ -659,7 +659,8 @@ msgstr "" msgid "DNS %s" msgstr "" -#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:3 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:3 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:14 msgid "Dashboard" msgstr "" @@ -1483,7 +1484,7 @@ msgstr "" msgid "Overlaid, log scale" msgstr "" -#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:14 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:26 msgid "Overview" msgstr "" diff --git a/luci-app-vantage/po/zh_Hans/vantage.po b/luci-app-vantage/po/zh_Hans/vantage.po index b7053ed8e..32e088dee 100644 --- a/luci-app-vantage/po/zh_Hans/vantage.po +++ b/luci-app-vantage/po/zh_Hans/vantage.po @@ -671,7 +671,8 @@ msgstr "DNS" msgid "DNS %s" msgstr "DNS %s" -#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:3 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:3 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:14 msgid "Dashboard" msgstr "仪表盘" @@ -1503,7 +1504,7 @@ msgstr "叠加" msgid "Overlaid, log scale" msgstr "叠加,对数刻度" -#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:14 +#: luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json:26 msgid "Overview" msgstr "概览" diff --git a/luci-app-vantage/root/usr/share/luci/menu.d/z-luci-app-vantage.json b/luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json similarity index 61% rename from luci-app-vantage/root/usr/share/luci/menu.d/z-luci-app-vantage.json rename to luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json index 0c60e3834..9c4a86673 100644 --- a/luci-app-vantage/root/usr/share/luci/menu.d/z-luci-app-vantage.json +++ b/luci-app-vantage/root/usr/share/luci/menu.d/zz-luci-app-vantage.json @@ -1,5 +1,5 @@ { - "admin/status/dashboard": { + "admin/dashboard": { "title": "Dashboard", "order": 1, "action": { @@ -10,6 +10,18 @@ "acl": [ "luci-app-vantage" ] } }, + "admin/status/dashboard": { + "title": "Dashboard", + "order": 2, + "action": { + "type": "view", + "path": "dashboard/index" + }, + "depends": { + "acl": [ "luci-mod-dashboard-index" ], + "fs": { "/www/luci-static/resources/view/dashboard/index.js": "file" } + } + }, "admin/network/firewall/overview": { "title": "Overview", "order": 5, diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile index 5e9432c90..24626f75b 100644 --- a/luci-mod-status/Makefile +++ b/luci-mod-status/Makefile @@ -7,9 +7,9 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Status Pages -LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket +LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-app-vantage +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket -PKG_RELEASE:=41 +PKG_RELEASE:=42 PKG_BUILD_DEPENDS:=iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/luci-theme-aurora/.dev/docs/navigation-loading.md b/luci-theme-aurora/.dev/docs/navigation-loading.md new file mode 100644 index 000000000..208036a7b --- /dev/null +++ b/luci-theme-aurora/.dev/docs/navigation-loading.md @@ -0,0 +1,71 @@ +# Navigation loading — 2026-11-03 + +The footer starts the menu and router modules in parallel. The router still +waits for the menu instance and `ui.menu.load()` before registering navigation +handlers. LuCI caches the pending promise for both its module loader and menu +request, so this removes the menu → router download dependency without adding +an `/admin/menu` request. + +The header retains LuCI's standard translation script before `cbi.js`. +The optional catalog-directory probe was removed to keep template logic small +and avoid maintaining a separate decision about whether translations exist. + +The pre-paint menu snapshot is scoped to language, user, session and theme +asset version. It is not reused after `ui.menu.flushCache()` invalidates the +LuCI session's menu. Logging out or returning to login clears the snapshot. + +## Shared runtime integration + +The theme pins `@eamonxg/luci-theme-devkit` to the published npm version +`0.3.2`. The lockfile records its registry integrity; no local dependency +patch or sibling checkout is required. + +From the theme repository's `.dev/` directory, use the pnpm version pinned +by `package.json`: + +```sh +pnpm install --frozen-lockfile +pnpm build +pnpm test +``` + +Shared runtime changes belong in the devkit repository. Publish the updated +npm package first, then update the theme dependency and lockfile before +rebuilding production assets. + +The bounded dependency prewarmer brings the generated router from 15,771 B +to 17,806 B raw, or from 5,840 B to 6,618 B with Node gzip defaults. The router budget is now +18 KiB. Independent CSS, menu, sidebar and font budgets are unchanged. +The increase is 2,035 B raw and 778 B with Node gzip defaults. +The prewarmer fetches source without evaluating it, limits parallel requests, +honors reduced-data connections and retries failed fetches. Pointer hover +immediately fetches only the root view source; a 65 ms hover dwell then permits +dependency expansion. Keyboard `:focus-visible` may start deep warming directly. +Template shell prefetch always waits for that dwell. Pointer-down cancels the +pending timer without starting another fetch, and mouse focus starts no warmup. +A warmup generation counter lets the next intent retry after failures or a +navigation clears queued requests. +Template routes +with query parameters or wildcard arguments retain full document navigation; +template shells are cached by both template name and pathname. See the shared +devkit's maintained router documentation for the exact behavior. + +## Validation + +- Tests execute the footer with a never-resolving menu promise to verify the + router download starts immediately, and remains disabled without the + Navigation API. +- Cache tests cover identity/version mismatches and LuCI menu invalidation. +- A normal frozen install and production build use the published npm package. The generated router is 17,806 B, SHA-256 `c18a6f9b3a2f9b551ac7d7ee55560fdaec16901bfca9da3c9ff0dfc42a35973b`. +- Device timing and template checks are recorded in the devkit performance + report for this coordinated change; local unit timings are not device load + timings. + +Aurora's combined admin asset budget remains 264,000 B. The sidebar/mega-menu +HTML stamp now includes the authentication session, so a fresh login by the +same user does not reuse the previous session's rendered menu. + +The full Aurora suite passed 157 tests, including browser compatibility +fixtures, after a normal frozen install and production build with the +published npm package. The output matches the coordinated runtime. +The combined admin assets are 263,992 B (budget 264,000 B). diff --git a/luci-theme-aurora/.dev/package.json b/luci-theme-aurora/.dev/package.json index 2060342ec..03199e918 100644 --- a/luci-theme-aurora/.dev/package.json +++ b/luci-theme-aurora/.dev/package.json @@ -13,7 +13,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@eamonxg/luci-theme-devkit": "^0.3.1", + "@eamonxg/luci-theme-devkit": "0.3.2", "@eamonxg/luci-theme-tokens": "^2.0.0", "@tailwindcss/vite": "^4.1.11", "@types/node": "^24.0.0", diff --git a/luci-theme-aurora/.dev/pnpm-lock.yaml b/luci-theme-aurora/.dev/pnpm-lock.yaml index bdeeecaee..a53c5b3d3 100644 --- a/luci-theme-aurora/.dev/pnpm-lock.yaml +++ b/luci-theme-aurora/.dev/pnpm-lock.yaml @@ -167,8 +167,8 @@ importers: .: devDependencies: '@eamonxg/luci-theme-devkit': - specifier: ^0.3.1 - version: 0.3.1(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)) + specifier: 0.3.2 + version: 0.3.2(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)) '@eamonxg/luci-theme-tokens': specifier: ^2.0.0 version: 2.0.0 @@ -205,8 +205,8 @@ importers: packages: - '@eamonxg/luci-theme-devkit@0.3.1': - resolution: {integrity: sha512-dgmdTPYnAvmRJ0uCzcx4+7X3msPct5uv1YVC5DzPBrox3ugzSHV65zW+rPEtlENW/tdz3//+cvT47fw7p8lbvA==} + '@eamonxg/luci-theme-devkit@0.3.2': + resolution: {integrity: sha512-Asyiszn6cYzkvNcsYeRKk9oBWXXfLiykrcau6dhbliK9zjr+DsQIRsz6faL3vICOOv822jH5KgXsB177TYTFwQ==} engines: {node: '>=20.19'} hasBin: true peerDependencies: @@ -937,7 +937,7 @@ packages: snapshots: - '@eamonxg/luci-theme-devkit@0.3.1(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0))': + '@eamonxg/luci-theme-devkit@0.3.2(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0))': dependencies: terser: 5.46.0 vite: 7.3.1(@types/node@24.10.9)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0) diff --git a/luci-theme-aurora/.dev/pnpm-workspace.yaml b/luci-theme-aurora/.dev/pnpm-workspace.yaml index 5ed0b5af0..e21c8464a 100644 --- a/luci-theme-aurora/.dev/pnpm-workspace.yaml +++ b/luci-theme-aurora/.dev/pnpm-workspace.yaml @@ -1,2 +1,4 @@ allowBuilds: esbuild: true +minimumReleaseAgeExclude: + - '@eamonxg/luci-theme-devkit@0.3.2' diff --git a/luci-theme-aurora/.dev/tests/build-performance.test.js b/luci-theme-aurora/.dev/tests/build-performance.test.js index 16dc2a330..b254a50e7 100644 --- a/luci-theme-aurora/.dev/tests/build-performance.test.js +++ b/luci-theme-aurora/.dev/tests/build-performance.test.js @@ -34,7 +34,9 @@ test("production assets stay within raw-transfer budgets", () => { // 16K: the router now ships from @eamonxg/luci-theme-devkit — theme hooks // as document events, the title format read off the document, and the // inner-scroller restoration shared with shadcn (+0.8 KB raw, +0.3 KB gz). - assert.ok(router <= 16_000, "router-aurora.js exceeds 16 KB"); + // 18 KiB: bounded dependency prewarm adds 2,035 B raw / 778 B gzip, + // with a two-request limit, retry/timeout handling and data-saver gates. + assert.ok(router <= 18_432, "router-aurora.js exceeds 18 KiB"); assert.ok(logo <= 16_000, "logo.svg exceeds 16 KB"); assert.ok( main + menu + router + font + logo <= 264_000, diff --git a/luci-theme-aurora/.dev/tests/nav-replay.test.js b/luci-theme-aurora/.dev/tests/nav-replay.test.js index 3c8195abc..7a99d756a 100644 --- a/luci-theme-aurora/.dev/tests/nav-replay.test.js +++ b/luci-theme-aurora/.dev/tests/nav-replay.test.js @@ -135,6 +135,10 @@ const run = ({ test("header.ut carries the replay and the login-page clear", () => { assert.ok(replaySource, "replay script not found"); assert.ok(loginSource, "login-page script not found"); + assert.match( + header, + /\[nav_type, dispatcher\.lang, ctx\?\.authuser \?\? '', ctx\?\.authsession \?\? ''\]/, + ); }); test("a sidebar replay paints inert and re-marks the current page", () => { diff --git a/luci-theme-aurora/.dev/tests/navigation-loading.test.js b/luci-theme-aurora/.dev/tests/navigation-loading.test.js new file mode 100644 index 000000000..eb29d6ac3 --- /dev/null +++ b/luci-theme-aurora/.dev/tests/navigation-loading.test.js @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +const footer = readFileSync( + new URL("../../ucode/template/themes/aurora/footer.ut", import.meta.url), + "utf8", +); +const boot = [...footer.matchAll(/ + {% endif %} diff --git a/luci-theme-aurora/ucode/template/themes/aurora/header.ut b/luci-theme-aurora/ucode/template/themes/aurora/header.ut index 63ef85d39..56205925f 100644 --- a/luci-theme-aurora/ucode/template/themes/aurora/header.ut +++ b/luci-theme-aurora/ucode/template/themes/aurora/header.ut @@ -227,7 +227,7 @@ {% endif %} - + {% if (!blank_page): %} {% if (nav_type == 'sidebar'): %} {# First in body: once anything resolves body's style (the page-bg script does), the column would ease shut on load. #} @@ -339,7 +339,7 @@ (() => { try { const body = document.body, cache = JSON.parse(sessionStorage.getItem('aurora.nav')); - {# The stamp (nav type, language, user, version) is rendered on body; a different user sees a different menu. #} + {# The stamp (nav type, language, user, session, version) is rendered on body; a different user sees a different menu. #} {# luci-base's one-session store loses its menu on ui.menu.flushCache() (package changes): never replay past that. #} const luci = Object.values(JSON.parse(sessionStorage.getItem('luci-session-store')) ?? {})[0]; if (!luci?.menu || cache?.[0] !== body.dataset.navStamp) return; diff --git a/luci-theme-shadcn/.dev/docs/build.md b/luci-theme-shadcn/.dev/docs/build.md index 67cf68abe..677015faf 100644 --- a/luci-theme-shadcn/.dev/docs/build.md +++ b/luci-theme-shadcn/.dev/docs/build.md @@ -13,6 +13,11 @@ `htdocs/` is generated output checked into git. Rebuild it with `pnpm build`, or trigger the manual `frontend-assets-build.yml` workflow, which builds and commits `htdocs/**`. +Install from `.dev/` using the pnpm version pinned in `package.json` and +`pnpm install --frozen-lockfile`, as the asset workflow does. The shared router +comes from the published `@eamonxg/luci-theme-devkit@0.3.2` package. +See [navigation-loading.md](navigation-loading.md) for the integration details. + ## Terser options that are load-bearing `.dev/src/resource/*.js` goes through terser (compress + local-scope mangle, no bundling); each file stays a standalone LuCI `L.require()`-able module. Two options must not be dropped: diff --git a/luci-theme-shadcn/.dev/docs/navigation-loading.md b/luci-theme-shadcn/.dev/docs/navigation-loading.md new file mode 100644 index 000000000..67dae9ee0 --- /dev/null +++ b/luci-theme-shadcn/.dev/docs/navigation-loading.md @@ -0,0 +1,75 @@ +# Navigation loading — 0.6.2 (2026-11-04) + +The footer starts the menu and router modules in parallel. The router still +waits for the menu instance and `ui.menu.load()` before registering navigation +handlers. LuCI caches the pending promise for both its module loader and menu +request, so this removes the menu → router download dependency without adding +an `/admin/menu` request. + +The header retains LuCI's standard translation script before `cbi.js`. +The optional catalog-directory probe was removed to keep template logic small +and avoid maintaining a separate decision about whether translations exist. + +The pre-paint menu snapshot is scoped to language, user, session and theme +asset version. It is not reused after `ui.menu.flushCache()` invalidates the +LuCI session's menu. Logging out or returning to login clears the snapshot. + +## Shared runtime integration + +The theme pins `@eamonxg/luci-theme-devkit` to the published npm version +`0.3.2`. The lockfile records its registry integrity; no local dependency +patch or sibling checkout is required. + +From the theme repository's `.dev/` directory, use the pnpm version pinned +by `package.json`: + +```sh +pnpm install --frozen-lockfile +pnpm build +pnpm test +``` + +Shared runtime changes belong in the devkit repository. Publish the updated +npm package first, then update the theme dependency and lockfile before +rebuilding production assets. + +The bounded dependency prewarmer brings the generated router from 15,771 B +to 17,806 B raw, or from 5,840 B to 6,618 B with Node gzip defaults. The router budget is now +18 KiB. Independent CSS, menu, sidebar and font budgets are unchanged. +The increase is 2,035 B raw and 778 B with Node gzip defaults. +The prewarmer fetches source without evaluating it, limits parallel requests, +honors reduced-data connections and retries failed fetches. Pointer hover +immediately fetches only the root view source; a 65 ms hover dwell then permits +dependency expansion. Keyboard `:focus-visible` may start deep warming directly. +Template shell prefetch always waits for that dwell. Pointer-down cancels the +pending timer without starting another fetch, and mouse focus starts no warmup. +A warmup generation counter lets the next intent retry after failures or a +navigation clears queued requests. +Template routes +with query parameters or wildcard arguments retain full document navigation; +template shells are cached by both template name and pathname. See the shared +devkit's maintained router documentation for the exact behavior. + +## Validation + +- Tests execute the footer with a never-resolving menu promise to verify the + router download starts immediately, and remains disabled without the + Navigation API. +- Cache tests cover identity/version mismatches and LuCI menu invalidation. +- Ordinary production builds use the published npm package. Both theme routers are + 17,806 B, SHA-256 `c18a6f9b3a2f9b551ac7d7ee55560fdaec16901bfca9da3c9ff0dfc42a35973b`. +- Device timing and template checks are recorded in the devkit performance + report for this coordinated change; local unit timings are not device load + timings. + +Shadcn renders its sidebar chrome and enables delegated controls while the +menu tree is still loading. Only navigation items wait for the authoritative +menu. The replay cache schema is version 2 (`{v, stamp, html, scroll}`). + +The combined admin budget increases only by the router allowance delta: +217,500 + (18,432 − 16,000) = 219,932 B. A delayed-menu test verifies chrome +and the ready event are available before the response arrives. + +The shadcn suite passed all 26 tests after the frozen install and ordinary rebuild; +color/contrast build checks also pass. Combined admin assets are 219,373 B +(budget 219,932 B). The menu asset is 17,007 B, two bytes smaller than before. diff --git a/luci-theme-shadcn/.dev/docs/router.md b/luci-theme-shadcn/.dev/docs/router.md index aeed1b992..cf2f6b10c 100644 --- a/luci-theme-shadcn/.dev/docs/router.md +++ b/luci-theme-shadcn/.dev/docs/router.md @@ -9,7 +9,7 @@ How the theme turns a menu click into an in-document view swap instead of a full page load, where it deliberately does not, and the invariants a router inside LuCI has to keep. Source: `.dev/src/resource/router-shadcn.js`, -loaded from `footer.ut` after `menu-shadcn.js`. **No changes to luci-base +requested from `footer.ut` in parallel with `menu-shadcn.js`; the router waits for the menu instance and shared menu tree before installing navigation handlers. **No changes to luci-base or to any view** — the router is additive theme JS plus small template hooks (a patch manifest, `data-shadcn-*` markers on the stylesheets header.ut itself renders, the asset version, a focusable `#maincontent`). diff --git a/luci-theme-shadcn/.dev/docs/sidebar.md b/luci-theme-shadcn/.dev/docs/sidebar.md index 0370be7bc..a2ce26cf2 100644 --- a/luci-theme-shadcn/.dev/docs/sidebar.md +++ b/luci-theme-shadcn/.dev/docs/sidebar.md @@ -2,11 +2,11 @@ - `header.ut`: near-minimal shell — empty `#sidebar` plus a parser-blocking inline script right after it that replays the sidebar cache (below) before first paint; sidebar chrome + nav are otherwise built client-side in `menu-shadcn.js`. - `sidebar-shadcn.js`: state machine for theme (light/dark/device), sidebar collapse/expand, accordion, and mobile drawer — exposed as `window.ShadcnSidebar` after the `shadcn-sidebar-ready` event fires. -- `menu-shadcn.js`: resolves the `admin` branch of `ui.menu.load()`, then renders a two-level sidebar; `ICON_MAP` maps a LuCI menu node's `name` to `/shadcn/icons/*.svg`; deeper levels render as `#tabmenu`. `syncRoute()` re-marks the active sidebar item (same longest-link-prefix rule as the header.ut replay), breadcrumb and tabs from `L.env` after a [router](router.md) swap without rebuilding the sidebar; `closeSurfaces()` closes palette/drawer/popover. +- `menu-shadcn.js`: builds the sidebar chrome and emits `shadcn-sidebar-ready` before waiting for the menu request, so the branding, theme toggle and drawer/collapse controls are immediately available. Its navigation listeners are delegated. It resolves the `admin` branch of `ui.menu.load()`, then renders a two-level sidebar; `ICON_MAP` maps a LuCI menu node's `name` to `/shadcn/icons/*.svg`; deeper levels render as `#tabmenu`. `syncRoute()` re-marks the active sidebar item (same longest-link-prefix rule as the header.ut replay), breadcrumb and tabs from `L.env` after a [router](router.md) swap without rebuilding the sidebar; `closeSurfaces()` closes palette/drawer/popover. ## Sidebar cache (anti-flash) -`menu-shadcn.js` snapshots `#sidebar.innerHTML` + scroll position into `sessionStorage['shadcn.sidebar.cache']` (`{v, lang, html, scroll}`) after render and on `pagehide`. The `header.ut` inline script replays it pre-paint on the next navigation, recomputes the active highlight for the current URL (longest link-path prefix — keep in sync with menu-shadcn's dispatchpath matching), restores accordion/scroll state, and sets `data-shadcn-built` / `data-shadcn-restored` on `#sidebar`. +`menu-shadcn.js` snapshots `#sidebar.innerHTML` + scroll position into `sessionStorage['shadcn.sidebar.cache']` (`{v: 2, stamp, html, scroll}`) after render and on `pagehide`. The stamp binds the language, user, session and asset version. The `header.ut` inline script only replays while LuCI's session store still contains its menu, so `ui.menu.flushCache()` also invalidates the snapshot; the login page clears it. It replays pre-paint on the next navigation, recomputes the active highlight for the current URL (longest link-path prefix — keep in sync with menu-shadcn's dispatchpath matching), restores accordion/scroll state, and sets `data-shadcn-built` / `data-shadcn-restored` on `#sidebar`. When restored, `renderSidebarChrome` only re-syncs the hostname, and `renderSidebarNav` preserves accordion/scroll across its authoritative rebuild. diff --git a/luci-theme-shadcn/.dev/package.json b/luci-theme-shadcn/.dev/package.json index 46d20ac49..f4e20ad74 100644 --- a/luci-theme-shadcn/.dev/package.json +++ b/luci-theme-shadcn/.dev/package.json @@ -20,7 +20,7 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@eamonxg/luci-theme-devkit": "^0.3.1", + "@eamonxg/luci-theme-devkit": "0.3.2", "@eamonxg/luci-theme-tokens": "^2.0.0", "@fontsource-variable/inter": "^5.2.8", "@tailwindcss/vite": "^4.3.0", diff --git a/luci-theme-shadcn/.dev/pnpm-lock.yaml b/luci-theme-shadcn/.dev/pnpm-lock.yaml index 75974a296..108f34cb9 100644 --- a/luci-theme-shadcn/.dev/pnpm-lock.yaml +++ b/luci-theme-shadcn/.dev/pnpm-lock.yaml @@ -167,8 +167,8 @@ importers: .: devDependencies: '@eamonxg/luci-theme-devkit': - specifier: ^0.3.1 - version: 0.3.1(terser@5.47.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)) + specifier: 0.3.2 + version: 0.3.2(terser@5.47.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0)) '@eamonxg/luci-theme-tokens': specifier: ^2.0.0 version: 2.0.0 @@ -211,8 +211,8 @@ importers: packages: - '@eamonxg/luci-theme-devkit@0.3.1': - resolution: {integrity: sha512-dgmdTPYnAvmRJ0uCzcx4+7X3msPct5uv1YVC5DzPBrox3ugzSHV65zW+rPEtlENW/tdz3//+cvT47fw7p8lbvA==} + '@eamonxg/luci-theme-devkit@0.3.2': + resolution: {integrity: sha512-Asyiszn6cYzkvNcsYeRKk9oBWXXfLiykrcau6dhbliK9zjr+DsQIRsz6faL3vICOOv822jH5KgXsB177TYTFwQ==} engines: {node: '>=20.19'} hasBin: true peerDependencies: @@ -858,7 +858,7 @@ packages: snapshots: - '@eamonxg/luci-theme-devkit@0.3.1(terser@5.47.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0))': + '@eamonxg/luci-theme-devkit@0.3.2(terser@5.47.1)(vite@8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0))': dependencies: terser: 5.47.1 vite: 8.0.13(@types/node@25.9.1)(jiti@2.7.0)(terser@5.47.1)(yaml@2.9.0) diff --git a/luci-theme-shadcn/.dev/pnpm-workspace.yaml b/luci-theme-shadcn/.dev/pnpm-workspace.yaml index 5ed0b5af0..0c72c3a48 100644 --- a/luci-theme-shadcn/.dev/pnpm-workspace.yaml +++ b/luci-theme-shadcn/.dev/pnpm-workspace.yaml @@ -1,2 +1,4 @@ allowBuilds: esbuild: true +minimumReleaseAgeExclude: + - "@eamonxg/luci-theme-devkit@0.3.2" diff --git a/luci-theme-shadcn/.dev/src/resource/menu-shadcn.js b/luci-theme-shadcn/.dev/src/resource/menu-shadcn.js index 6fbcd66df..d65c7d824 100644 --- a/luci-theme-shadcn/.dev/src/resource/menu-shadcn.js +++ b/luci-theme-shadcn/.dev/src/resource/menu-shadcn.js @@ -39,13 +39,15 @@ const RECENTS_KEY = "shadcn.palette.recents"; return baseclass.extend({ __init__() { + // Branding and sidebar controls need no menu RPC. Start them while the + // authoritative tree loads; the sidebar's nav handlers are delegated. + this.renderSidebarChrome(); + this.initUciIndicator(); + document.dispatchEvent(new Event("shadcn-sidebar-ready")); ui.menu.load().then((tree) => { - this.renderSidebarChrome(); this.render(tree); - this.initUciIndicator(); this.cacheSidebar(); window.addEventListener("pagehide", () => this.cacheSidebar()); - document.dispatchEvent(new Event("shadcn-sidebar-ready")); }); }, @@ -63,8 +65,8 @@ return baseclass.extend({ sessionStorage.setItem( CACHE_KEY, JSON.stringify({ - v: 1, - lang: document.documentElement.lang || "", + v: 2, + stamp: document.body.dataset.navStamp, html: sidebar.innerHTML, scroll: nav.scrollTop || 0, }), diff --git a/luci-theme-shadcn/.dev/tests/build-output.test.js b/luci-theme-shadcn/.dev/tests/build-output.test.js index adaaea51f..a81c94128 100644 --- a/luci-theme-shadcn/.dev/tests/build-output.test.js +++ b/luci-theme-shadcn/.dev/tests/build-output.test.js @@ -68,11 +68,14 @@ test("production assets stay within raw-transfer budgets", () => { // recents mapping, parent-aware scoring, per-segment path words) added // ~1.2 KB; the admin total below moved by the same amount. assert.ok(menu <= 17_500, `menu-shadcn.js ${menu} B exceeds 17.5 KB`); - assert.ok(router <= 16_000, `router-shadcn.js ${router} B exceeds 16 KB`); + // 18 KiB: bounded dependency prewarm adds 2,035 B raw / 778 B gzip, + // with a two-request limit, retry/timeout handling and data-saver gates. + assert.ok(router <= 18_432, `router-shadcn.js ${router} B exceeds 18 KiB`); assert.ok(sidebar <= 6_500, `sidebar-shadcn.js ${sidebar} B exceeds 6.5 KB`); assert.ok( - main + menu + router + sidebar + font <= 217_500, - "admin assets exceed 217.5 KB", + // Only the router allowance grows: 18,432 - 16,000 = 2,432 B. + main + menu + router + sidebar + font <= 219_932, + "admin assets exceed 219,932 B", ); assert.ok(login + font + logo <= 60_000, "login assets exceed 60 KB"); }); diff --git a/luci-theme-shadcn/.dev/tests/navigation-loading.test.js b/luci-theme-shadcn/.dev/tests/navigation-loading.test.js new file mode 100644 index 000000000..aed72ff27 --- /dev/null +++ b/luci-theme-shadcn/.dev/tests/navigation-loading.test.js @@ -0,0 +1,129 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +const footer = readFileSync( + new URL("../../ucode/template/themes/shadcn/footer.ut", import.meta.url), + "utf8", +); +const boot = [...footer.matchAll(/ {% endif %} diff --git a/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut b/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut index 19011e46f..a4526ef7e 100644 --- a/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut +++ b/luci-theme-shadcn/ucode/template/themes/shadcn/header.ut @@ -49,6 +49,9 @@ {{ striptags(((boardinfo && boardinfo.hostname) ? boardinfo.hostname : '?') + ((dispatched && dispatched.title) ? (' | ' + _(dispatched.title)) : '')) }} - + {% if (!blank_page): %}
-# Copyright (C) 2015-2017 wongsyrone - -include $(TOPDIR)/rules.mk - -PKG_NAME:=Toolkit -PKG_UPSTREAM_VERSION:=0.4.0.2 -PKG_UPSTREAM_GITHASH:=631954eac283e38822c47e3122e9e2a3ac5984c6 -PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)) -PKG_RELEASE:=1 - -PKG_SOURCE_PROTO:=git -PKG_SOURCE_URL:=https://github.com/muink/Toolkit.git -PKG_SOURCE_VERSION:=$(PKG_UPSTREAM_GITHASH) -PKG_MIRROR_HASH:=skip - -PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION) -PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz - -PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_SOURCE_SUBDIR) - -PKG_LICENSE:=GPL-2.0 -PKG_LICENSE_FILES:=LICENSE -PKG_MAINTAINER:=Chengr28 - -PKG_USE_MIPS16:=0 -PKG_BUILD_PARALLEL:=1 -CMAKE_INSTALL:=1 - -include $(INCLUDE_DIR)/package.mk -include $(INCLUDE_DIR)/cmake.mk - -TARGET_CFLAGS += $(FPIC) - -CMAKE_OPTIONS += \ - -DPLATFORM_OPENWRT=ON - -# Note: GCC 4.6 and 4.8 dont have complete C++11 support -# Please use GCC 4.9 or higher to compile -define Package/Toolkit - SECTION:=net - CATEGORY:=Network - TITLE:=A useful and powerful toolkit (DNSPing+FileHash) - URL:=https://github.com/chengr28/Toolkit - DEPENDS:=+libstdcpp \ - @GCC_VERSION_4_6:BROKEN @GCC_VERSION_4_8:BROKEN -endef - -define Package/Toolkit/config -if PACKAGE_Toolkit - - config PACKAGE_Toolkit_advancedoptions - bool "Use advanced compile options, see Makefile for details." - default n -endif -endef - -define Package/Toolkit/description -A network util can ping with DNS request. -endef - -# Some advanced compile flags for expert -ifneq ($(CONFIG_PACKAGE_Toolkit_advancedoptions),) - # Try to reduce binary size - TARGET_CFLAGS += -ffunction-sections -fdata-sections - TARGET_LDFLAGS += -Wl,--gc-sections - # Use Link time optimization - TARGET_CFLAGS += -flto - TARGET_LDFLAGS += -Wl,-flto -endif - -define Package/Toolkit/install - $(INSTALL_DIR) $(1)/usr/sbin - $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/{DNSPing,FileHash} $(1)/usr/sbin/ -endef - - -$(eval $(call BuildPackage,Toolkit)) diff --git a/openwrt-Toolkit/README.md b/openwrt-Toolkit/README.md deleted file mode 100644 index 4ae038572..000000000 --- a/openwrt-Toolkit/README.md +++ /dev/null @@ -1,61 +0,0 @@ -Toolkit for OpenWrt/LEDE -=== - -简介 ---- - - 本项目是 [Toolkit][1] 运行在 OpenWrt/LEDE 上的软件包,目前包含 DNSPing 和 FileHash - 当前版本: 0.4.0.2-2 - -编译 ---- - - - 从 OpenWrt/LEDE 的 [SDK][S] 编译 - - ```bash - # 以 ar71xx 平台为例,此处文件名为示例,仅供参考,请以实际文件名为准 - # 有对应平台的 SDK 即可编译软件包,不仅限于 ar71xx - tar xjf OpenWrt-SDK-ar71xx-for-linux-x86_64-gcc-4.8-linaro_uClibc-0.9.33.2.tar.bz2 - # 进入 SDK 根目录 - cd OpenWrt-SDK-ar71xx-* - # 首先验证 SDK 是否需要 ccache - cat .config | grep CONFIG_CCACHE - # 如果返回结果为 "y",则需要使用系统软件包管理器,如 yum、apt-get,安装 ccache - # 获取 Makefile - git clone --depth 1 --branch master --single-branch https://github.com/wongsyrone/openwrt-Toolkit.git package/Toolkit - # 选择要编译的包 Network -> Toolkit 并进行个人定制,或者保持默认 - # 这时根据提供的选项确认依赖已经被选中 - make menuconfig - # 开始编译 - make package/Toolkit/compile V=99 - # 编译结束之后从 bin 文件夹复制本程序的 ipk 文件到设备中,使用 opkg 进行安装 - ``` - - - 从 OpenWrt/LEDE 的代码树编译 - - 也可将本项目文件夹命名为 `Toolkit` 直接放置于 OpenWrt/LEDE 代码树的 `package` 文件夹下,之后按照编译的正常步骤进行,最后可在 bin 目录中找到编译好的软件包。下面简述编译步骤 - - ```bash - # 获取OpenWrt/LEDE代码树,根据需求选择稳定版(如Chaos Calmer 15.05)或开发版Trunk - # 如果是 Trunk 使用 - git clone git://git.openwrt.org/openwrt.git - # 如果是 Chaos Calmer 15.05 稳定版使用 - git clone git://git.openwrt.org/15.05/openwrt.git - # 进入代码树根目录 - cd openwrt - # 获取 Makefile - git clone --depth 1 --branch master --single-branch https://github.com/wongsyrone/openwrt-Toolkit.git package/Toolkit - # 首先选择目标平台以及设备型号 - # 接下来选择要编译的包 Network -> Toolkit 并进行个人定制,或者保持默认 - make menuconfig - # 如果只想编译 Toolkit 使用 - make package/Toolkit/{prepare,compile} V=99 - # 如果想编译集成好 Toolkit 的固件使用 - make V=99 - ``` - ----------- - - [1]: https://github.com/muink/Toolkit - [2]: https://github.com/muink/Toolkit/tree/master/Documents - [S]: http://wiki.openwrt.org/doc/howto/obtain.firmware.sdk diff --git a/openwrt-Toolkit/patches/001-Increase-CMake-minimum-version-to-3.5.patch b/openwrt-Toolkit/patches/001-Increase-CMake-minimum-version-to-3.5.patch deleted file mode 100644 index 68cacadf2..000000000 --- a/openwrt-Toolkit/patches/001-Increase-CMake-minimum-version-to-3.5.patch +++ /dev/null @@ -1,33 +0,0 @@ ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -18,7 +18,7 @@ - - - # CMake minimum version --cmake_minimum_required(VERSION 3.1) -+cmake_minimum_required(VERSION 3.5) - - # Sub directories - add_subdirectory(Source/DNSPing) ---- a/Source/DNSPing/CMakeLists.txt -+++ b/Source/DNSPing/CMakeLists.txt -@@ -18,7 +18,7 @@ - - - # CMake minimum version --cmake_minimum_required(VERSION 3.1) -+cmake_minimum_required(VERSION 3.5) - - # Project name, build type and list all source files. - project(DNSPing) ---- a/Source/FileHash/CMakeLists.txt -+++ b/Source/FileHash/CMakeLists.txt -@@ -18,7 +18,7 @@ - - - # CMake minimum version --cmake_minimum_required(VERSION 3.1) -+cmake_minimum_required(VERSION 3.5) - - # Project name, build type, list all source files and sub directories. - project(FileHash) diff --git a/openwrt-fastfetch b/openwrt-fastfetch new file mode 160000 index 000000000..287b4f7c9 --- /dev/null +++ b/openwrt-fastfetch @@ -0,0 +1 @@ +Subproject commit 287b4f7c95051d58a00c02286b5ca3d9f98df698 diff --git a/openwrt-fastfetch/LICENSE b/openwrt-fastfetch/LICENSE deleted file mode 100644 index ad029481a..000000000 --- a/openwrt-fastfetch/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Anya Lin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/openwrt-fastfetch/Makefile b/openwrt-fastfetch/Makefile deleted file mode 100644 index a2621e32f..000000000 --- a/openwrt-fastfetch/Makefile +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-License-Identifier: MIT -# -# Copyright (C) 2025 Anya Lin - -include $(TOPDIR)/rules.mk - -PKG_NAME:=fastfetch -PKG_VERSION:=2.70.0 -PKG_RELEASE:=17 - -PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz -PKG_SOURCE_URL:=https://codeload.github.com/fastfetch-cli/fastfetch/tar.gz/$(PKG_VERSION)? -PKG_HASH:=skip - -PKG_MAINTAINER:=Anya Lin -PKG_LICENSE:=MIT -PKG_LICENSE_FILES:=LICENSE - -PKG_BUILD_PARALLEL:=1 -PKG_BUILD_FLAGS:=no-mips16 - -include $(INCLUDE_DIR)/package.mk -include $(INCLUDE_DIR)/cmake.mk - -define Package/fastfetch - SECTION:=utils - CATEGORY:=Utilities - TITLE:=Fetch and display system information - URL:=https://github.com/fastfetch-cli/fastfetch - DEPENDS+= -endef - -define Package/fastfetch/description - Fastfetch is a neofetch-like tool for fetching - system information and displaying it prettily. -endef - -define Package/fastfetch/install - $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_BIN) $(PKG_BUILD_DIR)/fastfetch $(1)/usr/bin/ -endef - -$(eval $(call BuildPackage,fastfetch)) diff --git a/openwrt-fastfetch/README.md b/openwrt-fastfetch/README.md deleted file mode 100644 index fd888dfd3..000000000 --- a/openwrt-fastfetch/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# openwrt-fastfetch - -## Releases -You can find the prebuilt-ipks [here](https://fantastic-packages.github.io/releases/) - -## Build - -```shell -# Take the x86_64 platform as an example -tar xjf openwrt-sdk-23.05.5-x86-64_gcc-8.4.0_musl.Linux-x86_64.tar.xz -# Go to the SDK root dir -cd OpenWrt-sdk-*-x86_64_* -# First run to generate a .config file -make menuconfig -./scripts/feeds update -a -./scripts/feeds install -a -# Get Makefile -git clone --depth 1 --branch master --single-branch --no-checkout https://github.com/muink/openwrt-fastfetch.git package/fastfetch -pushd package/fastfetch -umask 022 -git checkout -popd -# Select the package Utilities -> fastfetch -make menuconfig -# Start compiling -make package/fastfetch/compile V=s BUILD_LOG=y -j$(nproc) -``` diff --git a/openwrt-natmapt/Makefile b/openwrt-natmapt/Makefile index 5faf7fc28..f94c5e33e 100644 --- a/openwrt-natmapt/Makefile +++ b/openwrt-natmapt/Makefile @@ -9,7 +9,7 @@ PKG_NAME:=natmapt PKG_UPSTREAM_VERSION:=20260214 PKG_UPSTREAM_GITHASH:= PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))) -PKG_RELEASE:=15 +PKG_RELEASE:=16 SCRIPTS_VERSION:=0.2026.01.24 PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION) @@ -23,7 +23,7 @@ PKG_SOURCE:=$(PKG_SOURCE_SUBDIR).tar.xz else PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/heiher/natmap.git -PKG_SOURCE_VERSION:=a56329a9964441014abcdd5f379fb9f6133aba1c +PKG_SOURCE_VERSION:=b24d590cb413639c0d17783b7377367d398bd996 PKG_MIRROR_HASH:=skip PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz