🌈 Sync 2026-09-29 11:54:37

This commit is contained in:
github-actions[bot] committed 2026-09-29 11:54:37 +08:00
1 parent eb664281f6
commit 7eac2a209f
44 files changed
+1356 -869

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter +coreutils-timeout
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.1.47 PKG_VERSION:=0.1.47
PKG_RELEASE:=48 PKG_RELEASE:=49
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH ifdef SOURCE_DATE_EPOCH
@@ -8,9 +8,9 @@
* Pure Map helpers — safe to unit-test on host without LuCI. * Pure Map helpers — safe to unit-test on host without LuCI.
*/ */
return baseclass.extend({ return baseclass.extend({
CACHE_MAX: 2000, CACHE_MAX: 4000,
FAIL_TTL_MS: 60000, FAIL_TTL_MS: 60000,
FAIL_MAX: 2000, FAIL_MAX: 4000,
/* Touch-on-write LRU: re-insert moves key to newest; evict oldest when over max. */ /* Touch-on-write LRU: re-insert moves key to newest; evict oldest when over max. */
lruSet: function (map, key, value, max) { lruSet: function (map, key, value, max) {
@@ -293,13 +293,16 @@ function buildEmptyStateNodes(state, callbacks) {
nodes.push( nodes.push(
E('p', {}, [ E('p', {}, [
_( _(
'Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-log6, then reload the firewall.' 'Install kmod-nf-log and kmod-nf-log6 with the command for your OpenWrt release, then reload the firewall.'
) )
]) ])
); );
nodes.push(E('p', {}, [_('OpenWrt 24.10 and older (opkg):')]));
nodes.push( nodes.push(
E('p', {}, [E('code', {}, ['opkg update && opkg install kmod-nf-log kmod-nf-log6'])]) E('p', {}, [E('code', {}, ['opkg update && opkg install kmod-nf-log kmod-nf-log6'])])
); );
nodes.push(E('p', {}, [_('OpenWrt 25.12 and newer (apk):')]));
nodes.push(E('p', {}, [E('code', {}, ['apk -U add kmod-nf-log kmod-nf-log6'])]));
return nodes; return nodes;
} }
@@ -23,6 +23,10 @@
'require fwlive.render-policy as renderPolicy'; 'require fwlive.render-policy as renderPolicy';
'require fwlive.render-scheduler as renderScheduler'; 'require fwlive.render-scheduler as renderScheduler';
const RULES_RETRY_BASE_MS = 5000;
/* Includes the initial read; a page reload starts a fresh retry budget. */
const RULES_RETRY_MAX_ATTEMPTS = 6;
const callFwlivePoll = rpc.declare({ const callFwlivePoll = rpc.declare({
object: 'fwlive', object: 'fwlive',
method: 'poll', method: 'poll',
@@ -162,11 +166,13 @@ return view.extend({
hostnameFailed: null, hostnameFailed: null,
resolveInFlight: false, resolveInFlight: false,
resolveGeneration: 0, resolveGeneration: 0,
/* Coalesce hostname-cache paints deferred while tablePaused. */ /* Track hostname paints queued or deferred until a visible forced paint. */
resolvePaintPending: false, resolvePaintPending: false,
lastPollError: false, lastPollError: false,
lastPollErrorCode: null, lastPollErrorCode: null,
lastRulesError: null, lastRulesError: null,
rulesRetryAttempt: 0,
nextRulesRetryAt: 0,
followLive: true, followLive: true,
rulesMap: {}, rulesMap: {},
firewallBackend: 'nft', firewallBackend: 'nft',
@@ -216,7 +222,7 @@ return view.extend({
updateHash(filters) { updateHash(filters) {
const parts = Object.keys(filters) const parts = Object.keys(filters)
.filter((k) => filters[k]) .filter((k) => filters[k] && log.parseFilterValue(filters[k]).value)
.map((k) => '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(filters[k]))); .map((k) => '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(filters[k])));
if (this.rowLimit !== constants.DEFAULT_ROW_LIMIT) if (this.rowLimit !== constants.DEFAULT_ROW_LIMIT)
parts.push('limit=%s'.format(encodeURIComponent(this.rowLimit))); parts.push('limit=%s'.format(encodeURIComponent(this.rowLimit)));
@@ -703,7 +709,7 @@ return view.extend({
this.rulesMap = (res && res.rules) || {}; this.rulesMap = (res && res.rules) || {};
this.firewallBackend = (res && res.backend) || 'nft'; this.firewallBackend = (res && res.backend) || 'nft';
/* Bounds / mktemp failures are reply.error — same idea as poll. */ /* Bounds / mktemp failures are reply.error — same idea as poll. */
this.lastRulesError = (res && res.error) || null; this.noteRulesMapOutcome((res && res.error) || null);
if (this.lastRulesError) console.warn('fwlive rules map error:', this.lastRulesError); if (this.lastRulesError) console.warn('fwlive rules map error:', this.lastRulesError);
this.refreshBufferedRuleLabels(); this.refreshBufferedRuleLabels();
} catch (_e) { } catch (_e) {
@@ -712,11 +718,35 @@ return view.extend({
this.rulesMap = {}; this.rulesMap = {};
this.firewallBackend = 'nft'; this.firewallBackend = 'nft';
} }
this.lastRulesError = 'rules_unavailable'; this.noteRulesMapOutcome('rules_unavailable');
} }
this.updateBackendUi(); this.updateBackendUi();
}, },
noteRulesMapOutcome(error) {
this.lastRulesError = error;
if (error !== 'rules_unavailable') {
this.rulesRetryAttempt = 0;
this.nextRulesRetryAt = 0;
return;
}
/* Back off consecutive failures, then stop after a bounded retry budget. */
this.rulesRetryAttempt = Math.min(
(this.rulesRetryAttempt || 0) + 1,
RULES_RETRY_MAX_ATTEMPTS
);
if (this.rulesRetryAttempt >= RULES_RETRY_MAX_ATTEMPTS) {
this.nextRulesRetryAt = 0;
return;
}
const delay = Math.min(
RULES_RETRY_BASE_MS * Math.pow(2, this.rulesRetryAttempt - 1),
60000
);
this.nextRulesRetryAt = this.nowMs() + delay;
},
backendDisplayLabel() { backendDisplayLabel() {
if (this.firewallBackend === 'nft') return _('using fw4'); if (this.firewallBackend === 'nft') return _('using fw4');
return ''; return '';
@@ -1185,7 +1215,13 @@ return view.extend({
/* A throw here is local UI work after a successful poll. Leave /* A throw here is local UI work after a successful poll. Leave
* lastPollError / lastPollErrorCode as applyPollReply set them. */ * lastPollError / lastPollErrorCode as applyPollReply set them. */
} }
} else if (this.lastRulesError === 'rules_unavailable') { } else if (
this.lastRulesError === 'rules_unavailable' &&
this.rulesRetryAttempt < RULES_RETRY_MAX_ATTEMPTS &&
!this.tablePaused &&
!this.lastPollError &&
this.nowMs() >= (this.nextRulesRetryAt || 0)
) {
await this.loadRulesMap(epoch); await this.loadRulesMap(epoch);
if (epoch !== this.currentPollEpoch() || this.viewDisposed) return; if (epoch !== this.currentPollEpoch() || this.viewDisposed) return;
} }
@@ -2082,6 +2118,7 @@ return view.extend({
actionRowTintClass: (action) => this.actionRowTintClass(action) actionRowTintClass: (action) => this.actionRowTintClass(action)
} }
); );
if (force && !this.isTabHidden()) this.resolvePaintPending = false;
if (scroll) { if (scroll) {
if (!this.tablePaused && this.followLive) scroll.scrollTop = 0; if (!this.tablePaused && this.followLive) scroll.scrollTop = 0;
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+182 -174
View File
@@ -1,15 +1,15 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8" msgstr "Content-Type: text/plain; charset=UTF-8"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1599 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1635
msgid "%d matching · %d/%d stored" msgid "%d matching · %d/%d stored"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1393 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1429
msgid "%s · requested up to %d raw lines · %d firewall messages returned" msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1383 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1419
msgid "" msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall " "%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned" "messages returned"
@@ -19,11 +19,11 @@ msgstr ""
msgid "%s: %s" msgid "%s: %s"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1601 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1637
msgid "0 matching · %d/%d stored" msgid "0 matching · %d/%d stored"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:447 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:453
msgid "Accessible (teal/orange)" msgid "Accessible (teal/orange)"
msgstr "" msgstr ""
@@ -32,11 +32,11 @@ msgstr ""
msgid "Action" msgid "Action"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:915 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
msgid "Administrator access is required to disable logging." msgid "Administrator access is required to disable logging."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:882 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912
msgid "Administrator access is required to enable logging." msgid "Administrator access is required to enable logging."
msgstr "" msgstr ""
@@ -44,32 +44,32 @@ msgstr ""
msgid "Allow/deny rules, LAN logging, or anything else." msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1829
msgid "Also seen" msgid "Also seen"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:853 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:883
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:924
msgid "Another change is staged for the firewall; apply or revert it first." msgid "Another change is staged for the firewall; apply or revert it first."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:871 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:940
msgid "" msgid ""
"Another change overwrote WAN logging after it was saved; check the current " "Another change overwrote WAN logging after it was saved; check the current "
"state." "state."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2601 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
msgid "Any action" msgid "Any action"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1786 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1822
msgid "Any protocol" msgid "Any protocol"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1374 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1434 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1470
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
@@ -77,7 +77,7 @@ msgstr ""
msgid "Before you enable logging" msgid "Before you enable logging"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:850 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880
msgid "Cannot enable logging until kernel log modules are installed." msgid "Cannot enable logging until kernel log modules are installed."
msgstr "" msgstr ""
@@ -85,11 +85,11 @@ msgstr ""
msgid "Changes:" msgid "Changes:"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:446 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:452
msgid "Classic (green/red)" msgid "Classic (green/red)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2332 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369
msgid "Classic uses green/red; Accessible uses teal/orange" msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "" msgstr ""
@@ -97,13 +97,13 @@ msgstr ""
msgid "Clear all" msgid "Clear all"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2710
msgid "" msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for " "Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, click a row for the full message" "firewall settings · in Simple view, click a row for the full message"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2400 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2437
msgid "" msgid ""
"Click a row (Time or other non-link cells) to see the full log line (Simple " "Click a row (Time or other non-link cells) to see the full log line (Simple "
"view)." "view)."
@@ -113,61 +113,61 @@ msgstr ""
msgid "Click a row for the full message" msgid "Click a row for the full message"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2405 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2442
msgid "" msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a " "Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude." "chip) to exclude."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1787 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1823
msgid "Common" msgid "Common"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1618 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1654
msgid "Connection lost — retrying…" msgid "Connection lost — retrying…"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:858 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:888
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:899 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929
msgid "Could not acquire the logging lock." msgid "Could not acquire the logging lock."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:866 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:896
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:905 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:935
msgid "Could not clear the WAN zone log option." msgid "Could not clear the WAN zone log option."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:912 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:942
msgid "Could not disable logging." msgid "Could not disable logging."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:873 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903
msgid "Could not enable logging." msgid "Could not enable logging."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:784 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:814
msgid "Could not load logging status." msgid "Could not load logging status."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:783 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813
msgid "Could not refresh logging status; showing the last known state." msgid "Could not refresh logging status; showing the last known state."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:868 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:898
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:937
msgid "Could not save the firewall configuration." msgid "Could not save the firewall configuration."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:860 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:890
msgid "Could not snapshot the current logging state." msgid "Could not snapshot the current logging state."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:864 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:894
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:903 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:933
msgid "Could not write the WAN zone log option." msgid "Could not write the WAN zone log option."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2665
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set." msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "" msgstr ""
@@ -175,11 +175,11 @@ msgstr ""
msgid "DPort" msgid "DPort"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1236 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1272
msgid "Degraded — sampling" msgid "Degraded — sampling"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1405 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1441
msgid "Degraded — sampling (slow poll RTT; cadence reduced)." msgid "Degraded — sampling (slow poll RTT; cadence reduced)."
msgstr "" msgstr ""
@@ -188,7 +188,7 @@ msgstr ""
msgid "Destination" msgid "Destination"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695
msgid "Destination IP contains (! to exclude)" msgid "Destination IP contains (! to exclude)"
msgstr "" msgstr ""
@@ -196,11 +196,11 @@ msgstr ""
msgid "Destination port" msgid "Destination port"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2700
msgid "Destination port (! to exclude)" msgid "Destination port (! to exclude)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2516 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2553
msgid "Detail" msgid "Detail"
msgstr "" msgstr ""
@@ -212,15 +212,15 @@ msgstr ""
msgid "Disabling…" msgid "Disabling…"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1232 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1268
msgid "Display limited to %d rows on this device" msgid "Display limited to %d rows on this device"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2273 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2310
msgid "Display options" msgid "Display options"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2365 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2402
msgid "Display options on the bar set Limit, row tint, palette, and hostnames." msgid "Display options on the bar set Limit, row tint, palette, and hostnames."
msgstr "" msgstr ""
@@ -244,7 +244,7 @@ msgstr ""
msgid "Enable logging" msgid "Enable logging"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2361 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2398
msgid "" msgid ""
"Enable logging turns on WAN zone drop/reject logging only (same as Network → " "Enable logging turns on WAN zone drop/reject logging only (same as Network → "
"Firewall). It does not add rules or log normal LAN browsing." "Firewall). It does not add rules or log normal LAN browsing."
@@ -255,7 +255,7 @@ msgstr ""
msgid "Enabling…" msgid "Enabling…"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1800 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1836
msgid "Exclude" msgid "Exclude"
msgstr "" msgstr ""
@@ -263,11 +263,11 @@ msgstr ""
msgid "Exclude instead" msgid "Exclude instead"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2287 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2324
msgid "Fetch budget" msgid "Fetch budget"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2369 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2406
msgid "" msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; " "Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum." "Manual selects a bounded maximum."
@@ -283,7 +283,7 @@ msgstr ""
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings." msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2443 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2480
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3 #: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View" msgid "Firewall Live View"
msgstr "" msgstr ""
@@ -296,7 +296,7 @@ msgstr ""
msgid "Flow" msgid "Flow"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2379 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2416
msgid "" msgid ""
"For a responsive table on a weak device, keep Limit at 250 rows or below. " "For a responsive table on a weak device, keep Limit at 250 rows or below. "
"The weak-device cap affects rendered rows; the buffer can still retain more." "The weak-device cap affects rendered rows; the buffer can still retain more."
@@ -310,25 +310,25 @@ msgstr ""
msgid "Grant access to firewall live log view" msgid "Grant access to firewall live log view"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2352 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389
msgid "Help" msgid "Help"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1512 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
msgid "Hide rows" msgid "Hide rows"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1567 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1603
msgid "" msgid ""
"High event rate — table refresh is throttled to protect the browser. The " "High event rate — table refresh is throttled to protect the browser. The "
"buffer still updates; refresh will resume automatically." "buffer still updates; refresh will resume automatically."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1412 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1448
msgid "Hostname resolve paused while the router is under load." msgid "Hostname resolve paused while the router is under load."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1827
msgid "ICMPv6" msgid "ICMPv6"
msgstr "" msgstr ""
@@ -336,14 +336,14 @@ msgstr ""
msgid "IN" msgid "IN"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2416 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2453
msgid "" msgid ""
"If Row tint looks missing, the active LuCI theme may omit success/error or " "If Row tint looks missing, the active LuCI theme may omit success/error or "
"info/warn CSS variables; fwlive falls back to local colors (air-gapped, no " "info/warn CSS variables; fwlive falls back to local colors (air-gapped, no "
"data leaves the device)." "data leaves the device)."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:331
msgid "" msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in " "If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide." "Help / the enabling-logs guide."
@@ -357,7 +357,13 @@ msgstr ""
msgid "Include instead" msgid "Include instead"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296
msgid ""
"Install kmod-nf-log and kmod-nf-log6 with the command for your OpenWrt "
"release, then reload the firewall."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1653
msgid "Installation is incomplete. Reinstall luci-app-fwlive." msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "" msgstr ""
@@ -366,12 +372,12 @@ msgstr ""
msgid "Interface" msgid "Interface"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2680
msgid "Interface (prefix ! to exclude)" msgid "Interface (prefix ! to exclude)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:250
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:365 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:368
msgid "I’ll configure this under Network → Firewall" msgid "I’ll configure this under Network → Firewall"
msgstr "" msgstr ""
@@ -379,59 +385,53 @@ msgstr ""
msgid "Kernel log modules missing" msgid "Kernel log modules missing"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:296
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log and kmod-nf-"
"log6, then reload the firewall."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:49
msgid "Len" msgid "Len"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2276 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2313
msgid "Limit" msgid "Limit"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:746 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:776
msgid "Live view may be incomplete — a legacy iptables table is registered" msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:336 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:339
msgid "Logging is off on this router" msgid "Logging is off on this router"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1374 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1410
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1435 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1471
msgid "Manual" msgid "Manual"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2305 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2342
msgid "Manual still uses server protection and poll cadence" msgid "Manual still uses server protection and poll cadence"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2374 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2411
msgid "" msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch " "Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval." "budget, not the polling interval."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:384 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:387
msgid "Manual test (System → Terminal):" msgid "Manual test (System → Terminal):"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2299 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2336
msgid "Maximum raw lines" msgid "Maximum raw lines"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:51
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:344
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2528 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2565
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2535 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2572
msgid "Message" msgid "Message"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2675
msgid "More filters" msgid "More filters"
msgstr "" msgstr ""
@@ -448,8 +448,8 @@ msgstr ""
msgid "No WAN zone found" msgid "No WAN zone found"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:856 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:886
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:897 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:927
msgid "No WAN zone found; cannot toggle logging without one." msgid "No WAN zone found; cannot toggle logging without one."
msgstr "" msgstr ""
@@ -457,7 +457,7 @@ msgstr ""
msgid "Not now" msgid "Not now"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:362
msgid "Nothing changes until you click Enable." msgid "Nothing changes until you click Enable."
msgstr "" msgstr ""
@@ -465,15 +465,23 @@ msgstr ""
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2558 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2595
msgid "One line" msgid "One line"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
msgid "Open firewall zone settings" msgid "Open firewall zone settings"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:340 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:300
msgid "OpenWrt 24.10 and older (opkg):"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
msgid "OpenWrt 25.12 and newer (apk):"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:343
msgid "" msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. " "OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/" "Live View only shows what the firewall already logs — it does not add allow/"
@@ -484,16 +492,16 @@ msgstr ""
msgid "Outbound" msgid "Outbound"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2326 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2363
msgid "Palette" msgid "Palette"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1702
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2482 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2519
msgid "Pause" msgid "Pause"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1701
msgid "Paused" msgid "Paused"
msgstr "" msgstr ""
@@ -502,11 +510,11 @@ msgstr ""
msgid "Proto" msgid "Proto"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2619 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2656
msgid "Protocol — common values" msgid "Protocol — common values"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2635
msgid "Quick search" msgid "Quick search"
msgstr "" msgstr ""
@@ -514,22 +522,22 @@ msgstr ""
msgid "Remove filter" msgid "Remove filter"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1666 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1702
msgid "Resume" msgid "Resume"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2316 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2353
msgid "Row tint" msgid "Row tint"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2410 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2447
msgid "" msgid ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/" "Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either " "red, default) or Accessible (teal/orange). Action text stays colored either "
"way." "way."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2466 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2503
msgid "" msgid ""
"Row tint used a local color fallback because the active LuCI theme did not " "Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds." "apply pass/deny backgrounds."
@@ -539,15 +547,15 @@ msgstr ""
msgid "Rule" msgid "Rule"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:737 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:767
msgid "Rule labels incomplete — map truncated" msgid "Rule labels incomplete — map truncated"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:740 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:770
msgid "Rule labels unavailable" msgid "Rule labels unavailable"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:739 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:769
msgid "Rule labels unavailable — temp file failed" msgid "Rule labels unavailable — temp file failed"
msgstr "" msgstr ""
@@ -559,40 +567,40 @@ msgstr ""
msgid "Search" msgid "Search"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1403 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1439
msgid "Server adaptive protection is disabled." msgid "Server adaptive protection is disabled."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1401 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1437
msgid "Server protection state is unknown." msgid "Server protection state is unknown."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1408 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1444
msgid "Server shedding — at most %d log lines per poll." msgid "Server shedding — at most %d log lines per poll."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1410 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1446
msgid "Server truncated this poll (adaptive cap)." msgid "Server truncated this poll (adaptive cap)."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2343 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2380
msgid "Show hostnames" msgid "Show hostnames"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2314 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2351
msgid "Show pass/deny row background colors" msgid "Show pass/deny row background colors"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1512 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1548
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2581 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618
msgid "Show rows" msgid "Show rows"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1469 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
msgid "Showing a compact summary of the top of the shown sample." msgid "Showing a compact summary of the top of the shown sample."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2505 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2542
msgid "Simple" msgid "Simple"
msgstr "" msgstr ""
@@ -601,7 +609,7 @@ msgstr ""
msgid "Source" msgid "Source"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2685
msgid "Source IP contains (! to exclude)" msgid "Source IP contains (! to exclude)"
msgstr "" msgstr ""
@@ -609,61 +617,61 @@ msgstr ""
msgid "Source port" msgid "Source port"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2653 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2690
msgid "Source port (! to exclude)" msgid "Source port (! to exclude)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1471 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid "Summary data is unavailable; show rows to inspect the sample." msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2572 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2609
msgid "Summary mode" msgid "Summary mode"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1473 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1509
msgid "Summary was bounded before all categories could be included." msgid "Summary was bounded before all categories could be included."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2384 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2421
msgid "" msgid ""
"Switching tabs pauses polling; returning performs one catch-up poll. " "Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work." "Hostnames are off by default because lookups add work."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:862 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:892
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:901 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:931
msgid "The firewall did not reload; saved and live logging may differ." msgid "The firewall did not reload; saved and live logging may differ."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2394 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2431
msgid "" msgid ""
"The rate shown for WAN logging is the firewall zone log_limit. OpenWrt " "The rate shown for WAN logging is the firewall zone log_limit. OpenWrt "
"defaults to 10/minute when no explicit limit is configured; fwlive does not " "defaults to 10/minute when no explicit limit is configured; fwlive does not "
"impose this cap." "impose this cap."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2389 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2426
msgid "" msgid ""
"The router log ring may evict older events before fwlive reads them. fwlive " "The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior." "cannot recover evicted entries or change forwarding behavior."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2356 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2393
msgid "" msgid ""
"The table updates automatically when your firewall logs traffic. Use Pause " "The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast." "if it moves too fast."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2469 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2506
msgid "Theme tint fallback" msgid "Theme tint fallback"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:392 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:395
msgid "Then ping the router." msgid "Then ping the router."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
msgid "" msgid ""
"This router reported a logging blocker that Live View does not recognize yet." "This router reported a logging blocker that Live View does not recognize yet."
msgstr "" msgstr ""
@@ -672,15 +680,15 @@ msgstr ""
msgid "Time" msgid "Time"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1478 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1514
msgid "Top drops" msgid "Top drops"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1481 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "Top rules" msgid "Top rules"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1475 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1511
msgid "Top talkers" msgid "Top talkers"
msgstr "" msgstr ""
@@ -688,7 +696,7 @@ msgstr ""
msgid "Turn it back off with the WAN logging control on the watch strip." msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:354 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:357
msgid "" msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal " "Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
@@ -703,32 +711,32 @@ msgstr ""
msgid "Undo:" msgid "Undo:"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2413 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2450
msgid "Use Detail for all columns (flags, length, raw message)." msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2487 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2524
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2493 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2530
msgid "View" msgid "View"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:914 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:944
msgid "WAN drop/reject logging is off." msgid "WAN drop/reject logging is off."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:324
msgid "" msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not." "here. Normal LAN browsing will not."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:878 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:908
msgid "" msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as " "WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing." "it happens — not normal LAN browsing."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:880 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:910
msgid "WAN logging is already enabled." msgid "WAN logging is already enabled."
msgstr "" msgstr ""
@@ -741,7 +749,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:126
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:307 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310
msgid "WAN logging unavailable" msgid "WAN logging unavailable"
msgstr "" msgstr ""
@@ -753,46 +761,46 @@ msgstr ""
msgid "WAN logging unavailable: no WAN zone" msgid "WAN logging unavailable: no WAN zone"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:317 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:320
msgid "Waiting for firewall events" msgid "Waiting for firewall events"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1665 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1701
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2457 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2494
msgid "Watching" msgid "Watching"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2547 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2584
msgid "Wrap" msgid "Wrap"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:71
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2603 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2640
msgid "block" msgid "block"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1223 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1259
msgid "buffer filling" msgid "buffer filling"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1227 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1263
msgid "buffer full" msgid "buffer full"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133
msgid "default 10/minute"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:36 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:36
msgid "contains" msgid "contains"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:133
msgid "default 10/minute"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:50 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:50
msgid "does not contain" msgid "does not contain"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:72
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2641
msgid "drop" msgid "drop"
msgstr "" msgstr ""
@@ -800,16 +808,16 @@ msgstr ""
msgid "is" msgid "is"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1222 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1258
msgid "loading buffer" msgid "loading buffer"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1597 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1633
msgid "loading…" msgid "loading…"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1451 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1487
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1460 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1496
msgid "none" msgid "none"
msgstr "" msgstr ""
@@ -817,98 +825,98 @@ msgstr ""
msgid "not" msgid "not"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1808 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1844
msgid "not AH" msgid "not AH"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1807 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1843
msgid "not ESP" msgid "not ESP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1806 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1842
msgid "not GRE" msgid "not GRE"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
msgid "not ICMP" msgid "not ICMP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
msgid "not ICMPv6" msgid "not ICMPv6"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1805 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1841
msgid "not IGMP" msgid "not IGMP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1809 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1845
msgid "not SCTP" msgid "not SCTP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1801 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1837
msgid "not TCP" msgid "not TCP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1838
msgid "not UDP" msgid "not UDP"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2609 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2646
msgid "not block" msgid "not block"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2608 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2645
msgid "not drop" msgid "not drop"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2607 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2644
msgid "not pass" msgid "not pass"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2610 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2647
msgid "not reject" msgid "not reject"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2611 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid "not unknown" msgid "not unknown"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2626 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2663
msgid "or type…" msgid "or type…"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:70
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2602 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2639
msgid "pass" msgid "pass"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:73
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2605 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2642
msgid "reject" msgid "reject"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1229 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1265
msgid "render paused (high rate)" msgid "render paused (high rate)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1238 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1274
msgid "resolve paused (load)" msgid "resolve paused (load)"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1240 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1276
msgid "scroll frozen — scroll to top to follow live" msgid "scroll frozen — scroll to top to follow live"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1237 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1273
msgid "truncated" msgid "truncated"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:62
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2606 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
msgid "unknown" msgid "unknown"
msgstr "" msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:721 #: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
msgid "using fw4" msgid "using fw4"
msgstr "" msgstr ""
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.27 PKG_VERSION:=26.9.27
PKG_RELEASE:=299 PKG_RELEASE:=300
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -1779,6 +1779,9 @@ local function curl(url, file, ua, mode)
ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua
curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"' curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"'
local cookie_file = "/tmp/cookie_" .. api.gen_random_char(5)
curl_args[#curl_args + 1] = '-c "' .. cookie_file .. '" -b "' .. cookie_file .. '"'
local return_code, result local return_code, result
if mode == "direct" then if mode == "direct" then
return_code, result = api.curl_base(url, file, curl_args) return_code, result = api.curl_base(url, file, curl_args)
@@ -1801,6 +1804,8 @@ local function curl(url, file, ua, mode)
header_str = header_str:gsub("\r", "") header_str = header_str:gsub("\r", "")
end end
luci.sys.call('rm -f "%s"' % cookie_file)
return return_code, http_code, header_str return return_code, http_code, header_str
end end
+41
View File
@@ -1,5 +1,46 @@
# 변경 기록 # 변경 기록
## [0.2.25-r1] - 2026-09-29
### 개선
- 관리자 비밀번호 변경 UI를 개선하였습니다.
- 내부 모듈을 정리하였습니다.
## [0.2.24-r5] - 2026-09-29
### 개선
- 관리자 비밀번호 변경 화면에서 비밀번호 요구 사항 안내와 `새 비밀번호 확인` 입력 영역 사이의 간격을 넓혀 각 입력 단계가 더 명확하게 구분되도록 개선했습니다.
### 테스트
- 비밀번호 요구 사항 안내 다음의 확인 입력 영역에 추가 여백이 유지되는지 UI 계약 테스트를 보강했습니다.
## [0.2.24-r4] - 2026-09-29
### 수정
- Cloud 활동 기록 전송 설정이 없거나 잘못된 값인 경우 전송을 켜지 않고 OFF로 처리하도록 변경했습니다.
- 과거 버전에서 Cloud 활동 기록을 암묵적으로 켜던 업그레이드 호환 코드를 제거해, 패키지 업데이트만으로 Cloud 전송이 자동 활성화되지 않도록 했습니다.
### 테스트
- 패키지 설치 과정에서 Cloud 전송을 강제로 켜는 레거시 migration이 남아 있지 않은지 검증하고, RPC·이벤트 기록·동기화·라이선스 경로 모두 명시적으로 ON인 경우에만 Cloud 기능을 사용하도록 계약 테스트를 보강했습니다.
## [0.2.24-r3] - 2026-09-29
### 수정
- `90-smartsafehub-system-defaults`가 펌웨어 업그레이드 때 기존 hostname, 시간대, 로그 및 NTP 사용자 설정을 다시 SmartSafeHub 기본값으로 덮어쓰지 않도록 하고, 새 설치의 OpenWrt 기본값 또는 누락된 옵션에만 기본값을 적용하도록 변경했습니다.
- OpenWrt 표준 설정 백업에 기존 `/usr/share/smartsafehub/firmware.json`을 포함하고 `device_code`가 현재 공유기와 일치하는 경우에만 복원을 허용해 다른 모델의 설정 백업 적용을 차단했습니다. 별도의 장치 identity 파일은 추가하지 않습니다.
- 백업에 포함된 과거 `firmware.json`은 호환성 검증에만 사용하고, 복원 직후 현재 설치된 펌웨어 metadata를 다시 복구해 `build_id`와 firmware identity가 과거 값으로 되돌아가지 않도록 했습니다.
### 테스트
- 기존 사용자 system/NTP 설정 보존, 새 설치 기본값 적용, `compat_version` 비변경을 검증하는 system-defaults 회귀 테스트를 보강했습니다.
- 같은 `device_code` 백업 허용, 다른 장치 및 식별 metadata 없는 백업 거부, 성공/실패 복원 후 현재 `firmware.json` 보존을 검증하는 백업 회귀 테스트를 추가했습니다.
## [0.2.24-r2] - 2026-09-29 ## [0.2.24-r2] - 2026-09-29
### 개선 ### 개선
+2 -15
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-smartsafehub PKG_NAME:=luci-app-smartsafehub
PKG_VERSION:=0.2.24 PKG_VERSION:=0.2.25
PKG_RELEASE:=4 PKG_RELEASE:=5
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com> PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
@@ -36,15 +36,6 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
echo "luci-app-smartsafehub: firmware update required for SmartSafeHub Reset Policy v1" >&2 echo "luci-app-smartsafehub: firmware update required for SmartSafeHub Reset Policy v1" >&2
exit 1 exit 1
fi fi
# r18 and earlier had Cloud activity sync enabled implicitly whenever the
# device was eligible. Preserve that behavior across the r19 upgrade even
# if opkg replaces an otherwise-unmodified conffile with the new default-off
# package config. Fresh installs have no existing activity-sync helper and
# therefore keep the new explicit opt-in default.
if [ -x /usr/libexec/smartsafehub-activity-sync ] && ! uci -q get smartsafehub.activity.cloud_sync_enabled >/dev/null 2>&1; then
mkdir -p /tmp/smartsafehub
: > /tmp/smartsafehub/activity-cloud-sync-upgrade-enable
fi
fi fi
exit 0 exit 0
endef endef
@@ -54,10 +45,6 @@ define Package/luci-app-smartsafehub/postinst
if [ -z "$${IPKG_INSTROOT}" ]; then if [ -z "$${IPKG_INSTROOT}" ]; then
mkdir -p /tmp/smartsafehub mkdir -p /tmp/smartsafehub
uci -q delete smartsafehub.firmware.check_enabled >/dev/null 2>&1 || true uci -q delete smartsafehub.firmware.check_enabled >/dev/null 2>&1 || true
if [ -f /tmp/smartsafehub/activity-cloud-sync-upgrade-enable ]; then
uci -q set smartsafehub.activity.cloud_sync_enabled='1' >/dev/null 2>&1 || true
rm -f /tmp/smartsafehub/activity-cloud-sync-upgrade-enable
fi
uci -q commit smartsafehub >/dev/null 2>&1 || true uci -q commit smartsafehub >/dev/null 2>&1 || true
if [ -x /etc/init.d/smartsafehub-events ]; then if [ -x /etc/init.d/smartsafehub-events ]; then
/etc/init.d/smartsafehub-events enable /etc/init.d/smartsafehub-events enable
+1 -1
View File
@@ -32,7 +32,7 @@ SmartSafeHub는 OpenWrt 공유기를 위한 통합 홈 게이트웨이 관리 UI
- **업데이트 관리**: SmartSafeHub 관리 소프트웨어와 펌웨어 업데이트를 확인하고 적용합니다. - **업데이트 관리**: SmartSafeHub 관리 소프트웨어와 펌웨어 업데이트를 확인하고 적용합니다.
- **장치 진단**: 로컬 진단, 진단 정보 다운로드와 사용자가 동의한 원격 Health Reporter를 제공합니다. - **장치 진단**: 로컬 진단, 진단 정보 다운로드와 사용자가 동의한 원격 Health Reporter를 제공합니다.
- **관리자 보안**: 최초 관리자 비밀번호 설정과 현재 비밀번호 확인을 거친 비밀번호 변경, 변경 후 재로그인을 제공합니다. - **관리자 보안**: 최초 관리자 비밀번호 설정과 현재 비밀번호 확인을 거친 비밀번호 변경, 변경 후 재로그인을 제공합니다.
- **최근 활동**: 인터넷, 보호, 업데이트, 라이선스와 진단 상태 변화를 로컬 이벤트 타임라인으로 정규화합니다. - **최근 활동**: 인터넷, 보호, 업데이트, 라이선스와 진단 상태 변화를 로컬 이벤트 타임라인으로 정규화하며, Cloud 활동 기록은 사용자가 명시적으로 켠 경우에만 전송합니다.
- **IPTV (Beta)**: SK Broadband와 LG U+ 환경을 위한 IGMP Proxy/Snooping 구성을 제공합니다. - **IPTV (Beta)**: SK Broadband와 LG U+ 환경을 위한 IGMP Proxy/Snooping 구성을 제공합니다.
기능별 동작과 현재 제약은 [docs/FEATURES.md](docs/FEATURES.md)에서 확인할 수 있습니다. 기능별 동작과 현재 제약은 [docs/FEATURES.md](docs/FEATURES.md)에서 확인할 수 있습니다.
@@ -88,6 +88,7 @@ luci-app-smartsafehub/
│ ├── usr/libexec/smartsafehub-health │ ├── usr/libexec/smartsafehub-health
│ ├── usr/libexec/smartsafehub-license │ ├── usr/libexec/smartsafehub-license
│ ├── usr/libexec/smartsafehub-maintenance │ ├── usr/libexec/smartsafehub-maintenance
│ ├── lib/upgrade/keep.d/smartsafehub
│ ├── usr/libexec/smartsafehub-backup │ ├── usr/libexec/smartsafehub-backup
│ ├── usr/share/luci/menu.d/ │ ├── usr/share/luci/menu.d/
│ ├── usr/share/rpcd/acl.d/ │ ├── usr/share/rpcd/acl.d/
+4 -4
View File
@@ -187,8 +187,8 @@ README에는 제품을 빠르게 파악하는 데 필요한 핵심 기능만 유
- 진단 파일에 Wi-Fi 비밀번호와 SafeShield 라이선스 키를 포함하지 않음 - 진단 파일에 Wi-Fi 비밀번호와 SafeShield 라이선스 키를 포함하지 않음
- 진단 파일에는 호스트명, WAN IPv4와 Wi-Fi SSID가 포함될 수 있으므로 외부 전달 전 확인 필요 - 진단 파일에는 호스트명, WAN IPv4와 Wi-Fi SSID가 포함될 수 있으므로 외부 전달 전 확인 필요
- OpenWrt 표준 `sysupgrade` 설정 백업을 SmartSafeHub에서 직접 다운로드하고, SmartSafeHub 또는 기본 LuCI에서 만든 `.tar.gz` 백업을 업로드·검증한 뒤 복원 가능 - OpenWrt 표준 `sysupgrade` 설정 백업을 SmartSafeHub에서 직접 다운로드하고, SmartSafeHub 또는 기본 LuCI에서 만든 `.tar.gz` 백업을 업로드·검증한 뒤 복원 가능
- 복원 archive는 16MB로 제한하고 gzip/tar 구조, `/etc/config` 포함 여부와 위험한 경로를 검사하며, 업데이트나 펌웨어 작업 중에는 복원을 차단 - 복원 archive는 16MB로 제한하고 gzip/tar 구조, `/etc/config` 포함 여부와 위험한 경로를 검사하며, 백업에 포함된 기존 `/usr/share/smartsafehub/firmware.json`의 `device_code`가 현재 장치와 일치할 때만 복원을 허용
- 설정 복원 후 현재 펌웨어 이미지의 `firmware.json`을 기준으로 `current_build_id`를 다시 동기화하고 자동 재부팅해 이전 백업의 펌웨어 identity가 남지 않도록 처리 - 백업의 `firmware.json`은 장치 식별에만 사용하고 복원 후에는 현재 설치된 펌웨어의 metadata를 다시 고정한 뒤 `current_build_id`를 동기화하므로 오래된 백업이 현재 firmware identity를 되돌리지 않음
- 설정 백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키와 라이선스 정보 등 민감한 설정이 포함될 수 있으므로 안전한 위치에 보관해야 하며, 펌웨어 이미지와 설치 패키지 자체는 포함하지 않음 - 설정 백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키와 라이선스 정보 등 민감한 설정이 포함될 수 있으므로 안전한 위치에 보관해야 하며, 펌웨어 이미지와 설치 패키지 자체는 포함하지 않음
- 데스크톱의 시스템 관리 영역은 `관리자 비밀번호`와 `설정 백업 및 복원`을 1:1 두 열로 배치하고 `시스템 도구`는 그 아래 전체 폭으로 표시합니다. 모바일에서는 모든 카드를 한 열로 쌓습니다. - 데스크톱의 시스템 관리 영역은 `관리자 비밀번호`와 `설정 백업 및 복원`을 1:1 두 열로 배치하고 `시스템 도구`는 그 아래 전체 폭으로 표시합니다. 모바일에서는 모든 카드를 한 열로 쌓습니다.
- 백업/복원 카드는 절반 폭에서도 읽기 쉽도록 `현재 설정 백업`과 `설정 복원`을 compact 세로 섹션으로 표시 - 백업/복원 카드는 절반 폭에서도 읽기 쉽도록 `현재 설정 백업`과 `설정 복원`을 compact 세로 섹션으로 표시
@@ -202,7 +202,7 @@ README에는 제품을 빠르게 파악하는 데 필요한 핵심 기능만 유
설정 UI에서는 예약 재부팅을 별도 시스템 관리 카드로 분리하지 않고 `시간 및 시간대` 카드의 하위 섹션으로 표시합니다. 시간대 변경과 예약 시각의 관계를 한 화면에서 확인할 수 있고, 데스크톱에서는 주기·요일·시각을 한 행에 배치하며 모바일에서는 세로로 자연스럽게 쌓입니다. 설정 UI에서는 예약 재부팅을 별도 시스템 관리 카드로 분리하지 않고 `시간 및 시간대` 카드의 하위 섹션으로 표시합니다. 시간대 변경과 예약 시각의 관계를 한 화면에서 확인할 수 있고, 데스크톱에서는 주기·요일·시각을 한 행에 배치하며 모바일에서는 세로로 자연스럽게 쌓입니다.
설정 백업 다운로드는 LuCI의 인증된 `/cgi-bin/cgi-backup` 경로를 통해 OpenWrt `sysupgrade --create-backup` 형식을 그대로 사용합니다. 복원은 `/cgi-bin/cgi-upload`로 전용 `/tmp/smartsafehub/config-backup.tar.gz` 경로에만 업로드한 뒤 `/usr/libexec/smartsafehub-backup`이 archive 구조와 업데이트 충돌 여부를 확인하고 `sysupgrade --restore-backup`을 실행합니다. 따라서 SmartSafeHub 백업은 기본 LuCI/CLI와 상호 호환되며 별도의 독자 백업 포맷을 만들지 않습니다. 설정 백업 다운로드는 LuCI의 인증된 `/cgi-bin/cgi-backup` 경로를 통해 OpenWrt `sysupgrade --create-backup` 형식을 그대로 사용합니다. `/lib/upgrade/keep.d/smartsafehub`는 이미 펌웨어에 존재하는 `/usr/share/smartsafehub/firmware.json`을 표준 백업에 포함해 별도의 장치 식별 파일을 만들지 않습니다. 복원은 `/cgi-bin/cgi-upload`로 전용 `/tmp/smartsafehub/config-backup.tar.gz` 경로에만 업로드한 뒤 `/usr/libexec/smartsafehub-backup`이 archive 구조와 `device_code` 일치 여부, 업데이트 충돌 여부를 확인하고 `sysupgrade --restore-backup`을 실행합니다. 백업 metadata는 검증에만 사용하며 복원 직후 현재 설치된 펌웨어의 `firmware.json`을 다시 복구합니다.
SmartSafeHub가 생성하는 휘발성 런타임 상태와 임시 파일은 `/tmp/smartsafehub/` 한 디렉터리에 모읍니다. 업데이트 상태와 릴리즈 노트, 펌웨어 상태·다운로드 이미지, Health 진단/Reporter 상태, 예약 재부팅 상태, Wi-Fi 변경 lock, 설정 백업 업로드 파일이 이 경로를 공유하며 `updater/`나 `firmware/` 같은 추가 하위 분류 디렉터리는 만들지 않습니다. `/tmp` 기반이므로 재부팅 시 함께 초기화되고 flash 저장 공간에는 기록하지 않습니다. 각 helper와 init script가 필요할 때 디렉터리를 다시 생성합니다. SmartSafeHub가 생성하는 휘발성 런타임 상태와 임시 파일은 `/tmp/smartsafehub/` 한 디렉터리에 모읍니다. 업데이트 상태와 릴리즈 노트, 펌웨어 상태·다운로드 이미지, Health 진단/Reporter 상태, 예약 재부팅 상태, Wi-Fi 변경 lock, 설정 백업 업로드 파일이 이 경로를 공유하며 `updater/`나 `firmware/` 같은 추가 하위 분류 디렉터리는 만들지 않습니다. `/tmp` 기반이므로 재부팅 시 함께 초기화되고 flash 저장 공간에는 기록하지 않습니다. 각 helper와 init script가 필요할 때 디렉터리를 다시 생성합니다.
@@ -247,7 +247,7 @@ Health observer의 첫 주기는 WAN/Health 현재 상태를 baseline으로만
공유기 웹사이트는 기존 로그인 세션과 RPC 시그니처를 그대로 유지하기 위해 인자 없는 read-only `smartsafehub.status` 응답에 최근 활동을 함께 포함해 읽고, 대시보드에는 최근 3건, `최근 활동` 전용 화면에는 최대 128건을 날짜별로 묶어 표시합니다. 대시보드의 compact 타임라인은 각 항목 사이에 작은 세로 간격을 두어 짧은 제목/설명이 연속해서 붙어 보이지 않도록 합니다. 저장된 원본에는 한국어 제목/설명을 넣지 않고 프론트엔드가 `event_type + metadata`를 렌더링합니다. 로컬 UI는 무료 장치 기능이며 **현재 부팅 이후의 휘발성 이력**을 제공합니다. 공유기 웹사이트는 기존 로그인 세션과 RPC 시그니처를 그대로 유지하기 위해 인자 없는 read-only `smartsafehub.status` 응답에 최근 활동을 함께 포함해 읽고, 대시보드에는 최근 3건, `최근 활동` 전용 화면에는 최대 128건을 날짜별로 묶어 표시합니다. 대시보드의 compact 타임라인은 각 항목 사이에 작은 세로 간격을 두어 짧은 제목/설명이 연속해서 붙어 보이지 않도록 합니다. 저장된 원본에는 한국어 제목/설명을 넣지 않고 프론트엔드가 `event_type + metadata`를 렌더링합니다. 로컬 UI는 무료 장치 기능이며 **현재 부팅 이후의 휘발성 이력**을 제공합니다.
Pro/Ultimate 장치에서는 공유기 `최근 활동` 화면에서 **Cloud 활동 기록 전송을 사용자가 직접 ON/OFF**할 수 있습니다. 새 설치는 `smartsafehub.activity.cloud_sync_enabled=0`으로 시작하지만, r18 이하 장치에는 이 옵션이 존재하지 않았으므로 업그레이드 시 누락된 값은 기존 동작을 보존하기 위해 ON으로 해석합니다. 사용자가 OFF로 저장한 값 `0`은 명시적 opt-out으로 취급합니다. Pro/Ultimate 장치에서는 공유기 `최근 활동` 화면에서 **Cloud 활동 기록 전송을 사용자가 직접 ON/OFF**할 수 있습니다. `smartsafehub.activity.cloud_sync_enabled=0`이 기본값이며, Cloud 전송은 사용자가 명시적으로 ON으로 저장한 경우에만 동작합니다. 옵션이 없거나 해석할 수 없는 값이면 OFF로 처리해 설정 누락만으로 활동 기록이 서버에 전송되지 않도록 합니다.
Cloud 전송이 ON일 때 `/usr/libexec/smartsafehub-activity-sync`가 같은 이벤트의 Cloud outbox를 `/api/v1/activity/events`로 batch 전송합니다. Hub 1.4.86부터 `activity_history` upload credential은 artifact resolve와 분리된 `/api/v1/licenses/status`에서 발급되지만, **license status API의 단일 소유자는 `/usr/libexec/smartsafehub-license`** 입니다. `smartsafehub-license status-sync`가 기존 라이선스 reconciliation과 함께 `activity_history` token/URL을 검증해 `/tmp/smartsafehub/activity-sync-credential.json`에 원자적으로 저장하고, `smartsafehub-activity-sync`는 이 runtime credential을 소비해 upload/ack만 담당합니다. 따라서 activity sync가 license key/device fingerprint를 다시 읽거나 `/licenses/status`와 `/licenses/resolve`를 별도로 호출하지 않습니다. Cloud 전송이 ON일 때 `/usr/libexec/smartsafehub-activity-sync`가 같은 이벤트의 Cloud outbox를 `/api/v1/activity/events`로 batch 전송합니다. Hub 1.4.86부터 `activity_history` upload credential은 artifact resolve와 분리된 `/api/v1/licenses/status`에서 발급되지만, **license status API의 단일 소유자는 `/usr/libexec/smartsafehub-license`** 입니다. `smartsafehub-license status-sync`가 기존 라이선스 reconciliation과 함께 `activity_history` token/URL을 검증해 `/tmp/smartsafehub/activity-sync-credential.json`에 원자적으로 저장하고, `smartsafehub-activity-sync`는 이 runtime credential을 소비해 upload/ack만 담당합니다. 따라서 activity sync가 license key/device fingerprint를 다시 읽거나 `/licenses/status`와 `/licenses/resolve`를 별도로 호출하지 않습니다.
+5 -1
View File
@@ -156,7 +156,11 @@ SmartSafeHub 펌웨어는 OpenWrt 기본 `/etc/rc.button/reset`과 제품 전용
펌웨어 build overlay에서 `/etc/config/system` 전체를 제공하면 OpenWrt가 장치별로 생성하는 `compat_version` 같은 메타데이터를 덮어쓸 수 있습니다. 특히 A3004T처럼 Sysupgrade metadata가 `compat_version=1.1`인 장치에서는 실행 중인 `/etc/config/system`에 값이 없을 경우 현재 버전을 1.0으로 판단해 정상 이미지도 호환성 불일치로 거부할 수 있습니다. 펌웨어 build overlay에서 `/etc/config/system` 전체를 제공하면 OpenWrt가 장치별로 생성하는 `compat_version` 같은 메타데이터를 덮어쓸 수 있습니다. 특히 A3004T처럼 Sysupgrade metadata가 `compat_version=1.1`인 장치에서는 실행 중인 `/etc/config/system`에 값이 없을 경우 현재 버전을 1.0으로 판단해 정상 이미지도 호환성 불일치로 거부할 수 있습니다.
따라서 build 서버의 `overlays/common/etc/config/system`은 사용하지 않고, SmartSafeHub 패키지의 `/etc/uci-defaults/90-smartsafehub-system-defaults`가 기존 system 섹션을 유지한 채 hostname, 시간대, 로그와 NTP 옵션만 갱신합니다. 새 펌웨어를 만들기 전에 외부 build overlay의 기존 `/etc/config/system` 파일을 반드시 제거해야 합니다. 따라서 build 서버의 `overlays/common/etc/config/system`은 사용하지 않고, SmartSafeHub 패키지의 `/etc/uci-defaults/90-smartsafehub-system-defaults`가 기존 system 섹션을 유지합니다. 새 설치의 OpenWrt 기본 hostname/UTC 값이나 누락된 옵션에만 SmartSafeHub 기본값을 채우고, 이미 사용자가 바꾼 hostname, 시간대, 로그와 NTP 설정 및 `compat_version`은 그대로 보존합니다. 새 펌웨어를 만들기 전에 외부 build overlay의 기존 `/etc/config/system` 파일을 반드시 제거해야 합니다.
### 설정 백업 장치 검증
SmartSafeHub는 별도의 `backup-device.json`을 만들지 않습니다. `/lib/upgrade/keep.d/smartsafehub`가 펌웨어에 이미 존재하는 `/usr/share/smartsafehub/firmware.json`을 OpenWrt 표준 설정 백업에 포함하고, 복원 시 해당 파일의 `device_code`와 현재 설치된 펌웨어의 `device_code`를 비교합니다. 다른 모델의 백업이나 장치 식별 metadata가 없는 기존 백업은 fail-closed로 거부합니다. `firmware.json`은 검증용 metadata일 뿐이므로 `sysupgrade --restore-backup` 후에는 복원 전에 보관해 둔 현재 펌웨어의 파일을 다시 기록하여 `build_id`가 과거 값으로 되돌아가지 않도록 합니다.
비밀번호 복구는 `/etc/smartsafehub/password-recovery` marker로 추적합니다. helper는 root 비밀번호만 비우고 Dropbear의 기존 enable 상태를 marker에 기록한 뒤 SSH를 중지/비활성화합니다. 재부팅 후 공개 recovery bridge는 marker가 존재하고 root 비밀번호가 비어 있을 때만 `system_root_password_status`와 `system_root_password_set` 두 RPC로 제한된 15분 ubus 세션을 발급하므로 일반 로그인 화면 없이 복구 UI로 바로 진입할 수 있습니다. 새 관리자 비밀번호 설정이 완료되면 marker를 삭제하고 이전 SSH enable 상태를 복원합니다. 비밀번호 복구는 `/etc/smartsafehub/password-recovery` marker로 추적합니다. helper는 root 비밀번호만 비우고 Dropbear의 기존 enable 상태를 marker에 기록한 뒤 SSH를 중지/비활성화합니다. 재부팅 후 공개 recovery bridge는 marker가 존재하고 root 비밀번호가 비어 있을 때만 `system_root_password_status`와 `system_root_password_set` 두 RPC로 제한된 15분 ubus 세션을 발급하므로 일반 로그인 화면 없이 복구 UI로 바로 진입할 수 있습니다. 새 관리자 비밀번호 설정이 완료되면 marker를 삭제하고 이전 SSH enable 상태를 복원합니다.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "luci-app-smartsafehub-frontend", "name": "luci-app-smartsafehub-frontend",
"version": "0.2.24", "version": "0.2.25",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "luci-app-smartsafehub-frontend", "name": "luci-app-smartsafehub-frontend",
"version": "0.2.24", "version": "0.2.25",
"dependencies": { "dependencies": {
"chart.js": "4.5.1", "chart.js": "4.5.1",
"preact": "10.29.8" "preact": "10.29.8"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "luci-app-smartsafehub-frontend", "name": "luci-app-smartsafehub-frontend",
"private": true, "private": true,
"version": "0.2.24", "version": "0.2.25",
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=24.0" "node": ">=24.0"
@@ -85,9 +85,9 @@ export async function fetchActivityHistory(): Promise<ActivityHistory> {
...activity, ...activity,
cloud: { cloud: {
...activity.cloud, ...activity.cloud,
// Pre-r19 RPC responses have no explicit toggle and always used Cloud // Cloud activity upload is explicit opt-in. Older or partial RPC
// sync when eligible. Preserve that behavior during rolling upgrades. // responses without the toggle must render as OFF rather than opt in.
enabled: activity.cloud.enabled ?? true, enabled: activity.cloud.enabled ?? false,
}, },
}; };
} }
+1 -1
View File
@@ -44,7 +44,7 @@ function installBootstrap(sessionId: string, host: HTMLElement): void {
sessionId, sessionId,
rpcUrl: luciUrl('/admin/ubus'), rpcUrl: luciUrl('/admin/ubus'),
assetBase: host.dataset.assetBase ?? '/luci-static/smartsafehub/', assetBase: host.dataset.assetBase ?? '/luci-static/smartsafehub/',
assetVersion: host.dataset.assetVersion ?? '0.2.24-r2', assetVersion: host.dataset.assetVersion ?? '0.2.25-r1',
locale: document.documentElement.lang || 'ko', locale: document.documentElement.lang || 'ko',
}); });
} }
@@ -912,16 +912,18 @@ function AdministratorPasswordCard(props: { onChanged: () => void }) {
<PasswordRequirement label="숫자 포함" met={policy.number} /> <PasswordRequirement label="숫자 포함" met={policy.number} />
</ul> </ul>
<PasswordInputField <div class="pt-2">
autoComplete="new-password" <PasswordInputField
disabled={submitting} autoComplete="new-password"
id="smartsafehub-confirm-admin-password" disabled={submitting}
label="새 비밀번호 확인" id="smartsafehub-confirm-admin-password"
name="confirm-password" label="새 비밀번호 확인"
onInput={setConfirmation} name="confirm-password"
placeholder="새 관리자 비밀번호 다시 입력" onInput={setConfirmation}
value={confirmation} placeholder="새 관리자 비밀번호 다시 입력"
/> value={confirmation}
/>
</div>
{confirmation.length > 0 && ( {confirmation.length > 0 && (
<p <p
@@ -943,9 +945,11 @@ function AdministratorPasswordCard(props: { onChanged: () => void }) {
</p> </p>
)} )}
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs leading-5 text-slate-600"> <div class="pt-2">
비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다. <div class="rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs leading-5 text-slate-600">
변경 후 현재 로그인 세션은 종료됩니다. 비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다.
변경 후 현재 로그인 세션은 종료됩니다.
</div>
</div> </div>
<div class="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs leading-5 text-amber-900"> <div class="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs leading-5 text-amber-900">
@@ -1,31 +1,59 @@
#!/bin/sh #!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# Keep OpenWrt-generated system metadata such as compat_version intact. Only # Preserve OpenWrt-generated metadata (including compat_version) and user
# apply SmartSafeHub product defaults to individual UCI options instead of # changes across firmware upgrades. SmartSafeHub only replaces values that are
# replacing /etc/config/system from a common firmware overlay. # still unset or at the stock defaults used on a fresh OpenWrt installation.
uci_get() {
uci -q get "$1" 2>/dev/null || true
}
set_if_missing() {
local key="$1" value="$2"
[ -n "$(uci_get "$key")" ] || uci -q set "$key=$value" || exit 1
}
if ! uci -q get system.@system[0] >/dev/null 2>&1; then if ! uci -q get system.@system[0] >/dev/null 2>&1; then
uci -q add system system >/dev/null || exit 1 uci -q add system system >/dev/null || exit 1
fi fi
uci -q set system.@system[0].hostname='SmartRouter' || exit 1 hostname="$(uci_get system.@system[0].hostname)"
uci -q set system.@system[0].zonename='Asia/Seoul' || exit 1 case "$hostname" in
uci -q set system.@system[0].timezone='UTC+9' || exit 1 ''|OpenWrt)
uci -q set system.@system[0].ttylogin='0' || exit 1 uci -q set system.@system[0].hostname='SmartRouter' || exit 1
uci -q set system.@system[0].log_size='64' || exit 1 ;;
uci -q set system.@system[0].urandom_seed='0' || exit 1 esac
zonename="$(uci_get system.@system[0].zonename)"
timezone="$(uci_get system.@system[0].timezone)"
case "$zonename" in
''|UTC)
case "$timezone" in
''|UTC|UTC0)
uci -q set system.@system[0].zonename='Asia/Seoul' || exit 1
uci -q set system.@system[0].timezone='UTC+9' || exit 1
;;
esac
;;
esac
set_if_missing system.@system[0].ttylogin '0'
set_if_missing system.@system[0].log_size '64'
set_if_missing system.@system[0].urandom_seed '0'
if ! uci -q get system.ntp >/dev/null 2>&1; then if ! uci -q get system.ntp >/dev/null 2>&1; then
uci -q set system.ntp='timeserver' || exit 1 uci -q set system.ntp='timeserver' || exit 1
fi fi
uci -q set system.ntp.enabled='1' || exit 1 set_if_missing system.ntp.enabled '1'
uci -q set system.ntp.enable_server='0' || exit 1 set_if_missing system.ntp.enable_server '0'
uci -q delete system.ntp.server >/dev/null 2>&1 || true
uci -q add_list system.ntp.server='0.openwrt.pool.ntp.org' || exit 1 if [ -z "$(uci_get system.ntp.server)" ]; then
uci -q add_list system.ntp.server='1.openwrt.pool.ntp.org' || exit 1 uci -q add_list system.ntp.server='0.openwrt.pool.ntp.org' || exit 1
uci -q add_list system.ntp.server='2.openwrt.pool.ntp.org' || exit 1 uci -q add_list system.ntp.server='1.openwrt.pool.ntp.org' || exit 1
uci -q add_list system.ntp.server='3.openwrt.pool.ntp.org' || exit 1 uci -q add_list system.ntp.server='2.openwrt.pool.ntp.org' || exit 1
uci -q add_list system.ntp.server='3.openwrt.pool.ntp.org' || exit 1
fi
uci -q commit system || exit 1 uci -q commit system || exit 1
exit 0 exit 0
@@ -0,0 +1 @@
/usr/share/smartsafehub/firmware.json
@@ -54,9 +54,8 @@ cloud_sync_enabled() {
local value local value
value="$(uci_get cloud_sync_enabled)" value="$(uci_get cloud_sync_enabled)"
case "$value" in case "$value" in
0|false|off|no) return 1 ;; 1|true|on|yes) return 0 ;;
1|true|on|yes|'') return 0 ;; *) return 1 ;;
*) return 0 ;;
esac esac
} }
@@ -12,11 +12,14 @@ REBOOT_DELAY_S="${SMARTSAFEHUB_BACKUP_REBOOT_DELAY_S:-2}"
UCI_BIN="${SMARTSAFEHUB_BACKUP_UCI_BIN:-/sbin/uci}" UCI_BIN="${SMARTSAFEHUB_BACKUP_UCI_BIN:-/sbin/uci}"
JSONFILTER_BIN="${SMARTSAFEHUB_BACKUP_JSONFILTER_BIN:-/usr/bin/jsonfilter}" JSONFILTER_BIN="${SMARTSAFEHUB_BACKUP_JSONFILTER_BIN:-/usr/bin/jsonfilter}"
FIRMWARE_METADATA_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_METADATA_FILE:-/usr/share/smartsafehub/firmware.json}" FIRMWARE_METADATA_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_METADATA_FILE:-/usr/share/smartsafehub/firmware.json}"
FIRMWARE_METADATA_ARCHIVE_PATH="usr/share/smartsafehub/firmware.json"
UPDATE_STATE_FILE="${SMARTSAFEHUB_BACKUP_UPDATE_STATE_FILE:-$RUNTIME_DIR/updates.state}" UPDATE_STATE_FILE="${SMARTSAFEHUB_BACKUP_UPDATE_STATE_FILE:-$RUNTIME_DIR/updates.state}"
FIRMWARE_STATE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_STATE_FILE:-$RUNTIME_DIR/firmware.state}" FIRMWARE_STATE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_STATE_FILE:-$RUNTIME_DIR/firmware.state}"
UPDATE_LOCK_DIR="${SMARTSAFEHUB_BACKUP_UPDATE_LOCK_DIR:-$RUNTIME_DIR/updater.lock}" UPDATE_LOCK_DIR="${SMARTSAFEHUB_BACKUP_UPDATE_LOCK_DIR:-$RUNTIME_DIR/updater.lock}"
FIRMWARE_LOCK_DIR="${SMARTSAFEHUB_BACKUP_FIRMWARE_LOCK_DIR:-$RUNTIME_DIR/firmware.lock}" FIRMWARE_LOCK_DIR="${SMARTSAFEHUB_BACKUP_FIRMWARE_LOCK_DIR:-$RUNTIME_DIR/firmware.lock}"
FIRMWARE_IMAGE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_IMAGE_FILE:-$RUNTIME_DIR/firmware.bin}" FIRMWARE_IMAGE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_IMAGE_FILE:-$RUNTIME_DIR/firmware.bin}"
BACKUP_DEVICE_MISMATCH=65
BACKUP_DEVICE_UNVERIFIED=66
numeric_or_zero() { numeric_or_zero() {
case "${1:-}" in case "${1:-}" in
@@ -31,6 +34,28 @@ file_size() {
numeric_or_zero "$value" numeric_or_zero "$value"
} }
safe_identity_value() {
case "${1:-}" in
'' | *[!A-Za-z0-9._~:+-]*) return 1 ;;
*) return 0 ;;
esac
}
json_value() {
local file="$1" expression="$2"
[ -f "$file" ] || return 0
[ -x "$JSONFILTER_BIN" ] || return 0
"$JSONFILTER_BIN" -i "$file" -e "$expression" 2>/dev/null || true
}
current_device_code() {
local value
value="$(json_value "$FIRMWARE_METADATA_FILE" '@.device_code')"
safe_identity_value "$value" || return 1
printf '%s' "$value"
}
archive_paths_are_safe() { archive_paths_are_safe() {
local list_file="$1" entry normalized has_config=0 local list_file="$1" entry normalized has_config=0
@@ -50,8 +75,54 @@ archive_paths_are_safe() {
[ "$has_config" -eq 1 ] [ "$has_config" -eq 1 ]
} }
backup_firmware_metadata_archive_path() {
local list_file="$1" entry normalized found='' count=0
while IFS= read -r entry || [ -n "$entry" ]; do
normalized="${entry#./}"
if [ "$normalized" = "$FIRMWARE_METADATA_ARCHIVE_PATH" ]; then
found="$entry"
count=$((count + 1))
fi
done < "$list_file"
[ "$count" -eq 1 ] || return 1
printf '%s' "$found"
}
validate_backup_device() {
local list_file="$1" archive_path metadata_tmp metadata_size
local backup_device current_device
archive_path="$(backup_firmware_metadata_archive_path "$list_file" 2>/dev/null || true)"
[ -n "$archive_path" ] || return "$BACKUP_DEVICE_UNVERIFIED"
metadata_tmp="${BACKUP_FILE}.firmware-metadata.$$"
rm -f "$metadata_tmp"
if ! "$TAR_BIN" -xOzf "$BACKUP_FILE" "$archive_path" > "$metadata_tmp" 2>/dev/null; then
rm -f "$metadata_tmp"
return "$BACKUP_DEVICE_UNVERIFIED"
fi
metadata_size="$(file_size "$metadata_tmp")"
if [ "$metadata_size" -le 0 ] || [ "$metadata_size" -gt 4096 ]; then
rm -f "$metadata_tmp"
return "$BACKUP_DEVICE_UNVERIFIED"
fi
backup_device="$(json_value "$metadata_tmp" '@.device_code')"
rm -f "$metadata_tmp"
safe_identity_value "$backup_device" || return "$BACKUP_DEVICE_UNVERIFIED"
current_device="$(current_device_code 2>/dev/null || true)"
safe_identity_value "$current_device" || return "$BACKUP_DEVICE_UNVERIFIED"
[ "$backup_device" = "$current_device" ] || return "$BACKUP_DEVICE_MISMATCH"
return 0
}
validate_backup() { validate_backup() {
local size list_file local size list_file device_status=0
[ -f "$BACKUP_FILE" ] && [ ! -L "$BACKUP_FILE" ] || return 1 [ -f "$BACKUP_FILE" ] && [ ! -L "$BACKUP_FILE" ] || return 1
size="$(file_size "$BACKUP_FILE")" size="$(file_size "$BACKUP_FILE")"
@@ -68,7 +139,9 @@ validate_backup() {
return 1 return 1
fi fi
validate_backup_device "$list_file" || device_status=$?
rm -f "$list_file" rm -f "$list_file"
[ "$device_status" -eq 0 ] || return "$device_status"
return 0 return 0
} }
@@ -121,7 +194,7 @@ sync_firmware_identity() {
[ -x "$JSONFILTER_BIN" ] || return 0 [ -x "$JSONFILTER_BIN" ] || return 0
[ -x "$UCI_BIN" ] || return 0 [ -x "$UCI_BIN" ] || return 0
build_id="$($JSONFILTER_BIN -i "$FIRMWARE_METADATA_FILE" -e '@.build_id' 2>/dev/null || true)" build_id="$(json_value "$FIRMWARE_METADATA_FILE" '@.build_id')"
case "$build_id" in case "$build_id" in
'' | *[!A-Za-z0-9._~:+-]*) return 0 ;; '' | *[!A-Za-z0-9._~:+-]*) return 0 ;;
esac esac
@@ -130,17 +203,54 @@ sync_firmware_identity() {
"$UCI_BIN" -q commit smartsafehub 2>/dev/null || true "$UCI_BIN" -q commit smartsafehub 2>/dev/null || true
} }
save_installed_firmware_metadata() {
local output_file="$1"
[ -f "$FIRMWARE_METADATA_FILE" ] && [ ! -L "$FIRMWARE_METADATA_FILE" ] || return 1
cp "$FIRMWARE_METADATA_FILE" "$output_file" || return 1
[ "$(file_size "$output_file")" -gt 0 ] || return 1
}
restore_installed_firmware_metadata() {
local saved_file="$1" metadata_dir
[ -f "$saved_file" ] || return 1
metadata_dir="$(dirname "$FIRMWARE_METADATA_FILE")"
mkdir -p "$metadata_dir" || return 1
rm -f "$FIRMWARE_METADATA_FILE" || return 1
cp "$saved_file" "$FIRMWARE_METADATA_FILE" || return 1
chmod 0644 "$FIRMWARE_METADATA_FILE" || return 1
}
perform_restore() { perform_restore() {
validate_backup || return 1 local validation_status=0 saved_metadata restore_status=0
validate_backup || validation_status=$?
[ "$validation_status" -eq 0 ] || return "$validation_status"
restore_is_busy && return 75 restore_is_busy && return 75
if ! "$SYSUPGRADE_BIN" --restore-backup "$BACKUP_FILE" >/dev/null 2>&1; then # The backup contains firmware.json only as immutable source-device metadata
# for compatibility validation. Preserve the currently installed image's
# metadata so restoring an older backup cannot roll build_id/device metadata
# back to the source firmware.
saved_metadata="${RUNTIME_DIR}/firmware-metadata.before-restore.$$"
rm -f "$saved_metadata"
save_installed_firmware_metadata "$saved_metadata" || {
rm -f "$saved_metadata"
return 1 return 1
}
if ! "$SYSUPGRADE_BIN" --restore-backup "$BACKUP_FILE" >/dev/null 2>&1; then
restore_status=1
fi fi
# An older backup may contain the previous firmware build ID. Keep the # Even a failed restore may already have written some files. Re-pin
# restored user configuration, but re-pin the runtime firmware identity to # firmware.json before returning so the running image identity stays true.
# the image that is actually installed before rebooting. if ! restore_installed_firmware_metadata "$saved_metadata"; then
rm -f "$saved_metadata"
return 1
fi
rm -f "$saved_metadata"
[ "$restore_status" -eq 0 ] || return "$restore_status"
sync_firmware_identity sync_firmware_identity
rm -f "$BACKUP_FILE" rm -f "$BACKUP_FILE"
@@ -52,9 +52,8 @@ cloud_sync_enabled() {
value="$("$UCI_BIN" -q get smartsafehub.activity.cloud_sync_enabled 2>/dev/null || true)" value="$("$UCI_BIN" -q get smartsafehub.activity.cloud_sync_enabled 2>/dev/null || true)"
fi fi
case "$value" in case "$value" in
0|false|off|no) return 1 ;; 1|true|on|yes) return 0 ;;
1|true|on|yes|'') return 0 ;; *) return 1 ;;
*) return 0 ;;
esac esac
} }
@@ -69,9 +69,8 @@ activity_cloud_sync_enabled() {
local value local value
value="$("$UCI_BIN" -q get smartsafehub.activity.cloud_sync_enabled 2>/dev/null || true)" value="$("$UCI_BIN" -q get smartsafehub.activity.cloud_sync_enabled 2>/dev/null || true)"
case "$value" in case "$value" in
0|false|off|no) return 1 ;; 1|true|on|yes) return 0 ;;
1|true|on|yes|'') return 0 ;; *) return 1 ;;
*) return 0 ;;
esac esac
} }
@@ -98,13 +98,12 @@ function bool_config(value, fallback) {
function cloud_sync_enabled() { function cloud_sync_enabled() {
const ctx = new_uci_cursor(); const ctx = new_uci_cursor();
if (ctx == null) { if (ctx == null) {
return true; return false;
} }
// Releases before r19 had no explicit toggle and always uploaded when paid. // Cloud activity upload is explicit opt-in. Missing or malformed values
// Treat a missing option as enabled for upgrade compatibility, while fresh // must fail closed so local activity never starts uploading unexpectedly.
// r19 installations ship cloud_sync_enabled=0. return bool_config(ctx.get('smartsafehub', 'activity', 'cloud_sync_enabled'), false);
return bool_config(ctx.get('smartsafehub', 'activity', 'cloud_sync_enabled'), true);
} }
function license_cloud_entitlement() { function license_cloud_entitlement() {
@@ -13,6 +13,8 @@ import {
const BACKUP_FILE = '/tmp/smartsafehub/config-backup.tar.gz'; const BACKUP_FILE = '/tmp/smartsafehub/config-backup.tar.gz';
const BACKUP_HELPER = '/usr/libexec/smartsafehub-backup'; const BACKUP_HELPER = '/usr/libexec/smartsafehub-backup';
const MAX_BACKUP_BYTES = 16777216; const MAX_BACKUP_BYTES = 16777216;
const BACKUP_DEVICE_MISMATCH = 65;
const BACKUP_DEVICE_UNVERIFIED = 66;
function safe_upload_filename(value) { function safe_upload_filename(value) {
const filename = string_value(value, 'backup.tar.gz'); const filename = string_value(value, 'backup.tar.gz');
@@ -21,6 +23,35 @@ function safe_upload_filename(value) {
: 'backup.tar.gz'; : 'backup.tar.gz';
} }
function run_backup_helper(action, timeout) {
try {
return system([ BACKUP_HELPER, action ], timeout);
}
catch (e) {
return -1;
}
}
function backup_validation_failure(status) {
if (status == BACKUP_DEVICE_MISMATCH) {
return failure(
'SYSTEM_BACKUP_DEVICE_MISMATCH',
'이 백업은 다른 장치에서 생성되었습니다. 현재 공유기와 같은 모델에서 만든 백업 파일만 복원할 수 있습니다.'
);
}
if (status == BACKUP_DEVICE_UNVERIFIED) {
return failure(
'SYSTEM_BACKUP_DEVICE_UNVERIFIED',
'이 백업에는 SmartSafeHub 장치 식별 정보가 없어 안전하게 호환성을 확인할 수 없습니다. 현재 공유기에서 새 백업을 생성해 주세요.'
);
}
return failure(
'SYSTEM_BACKUP_INVALID',
'올바른 OpenWrt 설정 백업 파일이 아닙니다. 파일이 손상되지 않았는지 확인해 주세요.'
);
}
export function validate_uploaded_backup(request) { export function validate_uploaded_backup(request) {
const stat = fs.stat(BACKUP_FILE); const stat = fs.stat(BACKUP_FILE);
if (stat == null || stat?.size <= 0) { if (stat == null || stat?.size <= 0) {
@@ -31,12 +62,10 @@ export function validate_uploaded_backup(request) {
return failure('SYSTEM_BACKUP_TOO_LARGE', '설정 백업 파일은 16MB 이하여야 합니다.'); return failure('SYSTEM_BACKUP_TOO_LARGE', '설정 백업 파일은 16MB 이하여야 합니다.');
} }
if (!run_command([ BACKUP_HELPER, 'validate' ], 20000)) { const status = run_backup_helper('validate', 20000);
if (status != 0) {
fs.unlink(BACKUP_FILE); fs.unlink(BACKUP_FILE);
return failure( return backup_validation_failure(status);
'SYSTEM_BACKUP_INVALID',
'올바른 OpenWrt 설정 백업 파일이 아닙니다. 같은 장치에서 생성한 백업인지 확인해 주세요.'
);
} }
return success({ return success({
@@ -51,24 +80,20 @@ export function restore_uploaded_backup(request) {
return failure('SYSTEM_BACKUP_CONFIRM_REQUIRED', '설정 복원을 다시 확인해 주세요.'); return failure('SYSTEM_BACKUP_CONFIRM_REQUIRED', '설정 복원을 다시 확인해 주세요.');
} }
let status; const status = run_backup_helper('restore', 30000);
try {
status = system([ BACKUP_HELPER, 'restore' ], 30000);
}
catch (e) {
status = -1;
}
if (status == 75) { if (status == 75) {
return failure( return failure(
'SYSTEM_BACKUP_BUSY', 'SYSTEM_BACKUP_BUSY',
'업데이트 또는 펌웨어 작업이 진행 중입니다. 작업이 끝난 뒤 다시 복원해 주세요.' '업데이트 또는 펌웨어 작업이 진행 중입니다. 작업이 끝난 뒤 다시 복원해 주세요.'
); );
} }
if (status == BACKUP_DEVICE_MISMATCH || status == BACKUP_DEVICE_UNVERIFIED) {
return backup_validation_failure(status);
}
if (status != 0) { if (status != 0) {
return failure( return failure(
'SYSTEM_BACKUP_RESTORE_FAILED', 'SYSTEM_BACKUP_RESTORE_FAILED',
'설정 백업을 복원하지 못했습니다. 백업 파일과 장치 호환성을 확인해 주세요.' '설정 백업을 복원하지 못했습니다. 백업 파일이 손상되지 않았는지 확인해 주세요.'
); );
} }
@@ -10,16 +10,16 @@
<meta name="robots" content="noindex,nofollow,noarchive"> <meta name="robots" content="noindex,nofollow,noarchive">
<meta name="referrer" content="same-origin"> <meta name="referrer" content="same-origin">
<title>SmartSafeHub</title> <title>SmartSafeHub</title>
<link rel="icon" type="image/svg+xml" href="/luci-static/smartsafehub/favicon.svg?v=0.2.24-r2"> <link rel="icon" type="image/svg+xml" href="/luci-static/smartsafehub/favicon.svg?v=0.2.25-r1">
</head> </head>
<body style="margin:0;background:#f8fafc;min-height:100vh;"> <body style="margin:0;background:#f8fafc;min-height:100vh;">
<div <div
id="smartsafehub-entry-root" id="smartsafehub-entry-root"
data-asset-base="/luci-static/smartsafehub/" data-asset-base="/luci-static/smartsafehub/"
data-asset-version="0.2.24-r2" data-asset-version="0.2.25-r1"
style="display:block;width:100%;min-height:100vh;" style="display:block;width:100%;min-height:100vh;"
></div> ></div>
<noscript>SmartSafeHub를 사용하려면 JavaScript를 활성화해 주세요.</noscript> <noscript>SmartSafeHub를 사용하려면 JavaScript를 활성화해 주세요.</noscript>
<script type="module" src="/luci-static/smartsafehub/app.js?v=0.2.24-r2"></script> <script type="module" src="/luci-static/smartsafehub/app.js?v=0.2.25-r1"></script>
</body> </body>
</html> </html>
@@ -1,3 +1,3 @@
var e,t,n,r,i,a,o,s,c,l,u,d,f,p,m={},h=[],g=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,_=Array.isArray;function v(e,t){for(var n in t)e[n]=t[n];return e}function y(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function b(t,n,r){var i,a,o,s={};for(o in n)o==`key`?i=n[o]:o==`ref`?a=n[o]:s[o]=n[o];if(arguments.length>2&&(s.children=arguments.length>3?e.call(arguments,2):r),typeof t==`function`&&t.defaultProps!=null)for(o in t.defaultProps)s[o]===void 0&&(s[o]=t.defaultProps[o]);return x(t,s,i,a,null)}function x(e,r,i,a,o){var s={type:e,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o??++n,__i:-1,__u:0};return o==null&&t.vnode!=null&&t.vnode(s),s}function S(e){return e.children}function C(e,t){this.props=e,this.context=t}function w(e,t){if(t==null)return e.__?w(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type==`function`?w(e):null}function T(e){if(e.__P&&e.__d){var n=e.__v,r=n.__e,i=[],a=[],o=v({},n);o.__v=n.__v+1,t.vnode&&t.vnode(o),ie(e.__P,o,n,e.__n,e.__P.namespaceURI,32&n.__u?[r]:null,i,r??w(n),!!(32&n.__u),a),o.__v=n.__v,o.__.__k[o.__i]=o,oe(i,o,a),n.__e=n.__=null,o.__e!=r&&E(o)}}function E(e){if((e=e.__)!=null&&e.__c!=null)return e.__e=e.__c.base=null,e.__k.some(function(t){if(t!=null&&t.__e!=null)return e.__e=e.__c.base=t.__e}),E(e)}function D(e){(!e.__d&&(e.__d=!0)&&r.push(e)&&!O.__r++||i!=t.debounceRendering)&&((i=t.debounceRendering)||a)(O)}function O(){try{for(var e,t=1;r.length;)r.length>t&&r.sort(o),e=r.shift(),t=r.length,T(e)}finally{r.length=O.__r=0}}function k(e,t,n,r,i,a,o,s,c,l,u){var d,f,p,g,_,v,y=r&&r.__k||h,b=t.length;for(c=A(n,t,y,c,b),d=0;d<b;d++)(p=n.__k[d])!=null&&(f=p.__i!=-1&&y[p.__i]||m,p.__i=d,v=ie(e,p,f,i,a,o,s,c,l,u),g=p.__e,p.ref&&f.ref!=p.ref&&(f.ref&&le(f.ref,null,p),u.push(p.ref,p.__c||g,p)),_==null&&g!=null&&(_=g),4&p.__u?(c=ee(p,c,e),f.__e&&(f.__e=null)):typeof p.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),p.__u&=-7);return n.__e=_,c}function A(e,t,n,r,i){var a,o,s,c,l,u=n.length,d=u,f=0;for(e.__k=Array(i),a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o==`string`||typeof o==`number`||typeof o==`bigint`||o.constructor==String?o=e.__k[a]=x(null,o,null,null,null):_(o)?o=e.__k[a]=x(S,{children:o},null,null,null):o.constructor===void 0&&o.__b>0?o=e.__k[a]=x(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):e.__k[a]=o,c=a+f,o.__=e,o.__b=e.__b+1,s=null,(l=o.__i=te(o,n,c,d))!=-1&&(d--,(s=n[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(i>u?f--:i<u&&f++),typeof o.type!=`function`&&(o.__u|=4)):l!=c&&(l==c-1?f--:l==c+1?f++:(l>c?f--:f++,o.__u|=4))):e.__k[a]=null;if(d)for(a=0;a<u;a++)(s=n[a])!=null&&!(2&s.__u)&&(s.__e==r&&(r=w(s)),ue(s,s));return r}function ee(e,t,n){var r,i;if(typeof e.type==`function`){for(r=e.__k,i=0;r&&i<r.length;i++)r[i]&&(r[i].__=e,t=ee(r[i],t,n));return t}e.__e!=t&&(t&&e.type&&!t.parentNode&&(t=w(e)),t=n.insertBefore(e.__e,t||null));do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function j(e,t){return t=t||[],e==null||typeof e==`boolean`||(_(e)?e.some(function(e){j(e,t)}):t.push(e)),t}function te(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l!=null&&!(2&l.__u);if(l===null&&s==null||u&&s==l.key&&c==l.type)return n;if(r>+!!u){for(i=n-1,a=n+1;i>=0||a<t.length;)if((l=t[o=i>=0?i--:a++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function ne(e,t,n){t[0]==`-`?e.setProperty(t,n??``):e[t]=n==null?``:typeof n!=`number`||g.test(t)?n:n+`px`}function M(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||ne(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||ne(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)a=t!=(t=t.replace(u,`$1`)),o=t.toLowerCase(),t=o in e||t==`onFocusOut`||t==`onFocusIn`?o.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+a]=n,n?r?n[l]=r[l]:(n[l]=d,e.addEventListener(t,a?p:f,a)):e.removeEventListener(t,a?p:f,a);else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function re(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n[c]==null)n[c]=d++;else if(n[c]<r[l])return;return r(t.event?t.event(n):n)}}}function ie(e,n,r,i,a,o,s,c,l,u){var d,f,p,m,g,b,x,T,E,D,O,A,ee,j,te,ne,M=n.type;if(n.constructor!==void 0)return null;128&r.__u&&(l=!!(32&r.__u),o=[c=n.__e=r.__e]),(d=t.__b)&&d(n);n:if(typeof M==`function`){f=s.length;try{if(E=n.props,D=M.prototype&&M.prototype.render,O=(d=M.contextType)&&i[d.__c],A=d?O?O.props.value:d.__:i,r.__c?T=(p=n.__c=r.__c).__=p.__E:(D?n.__c=p=new M(E,A):(n.__c=p=new C(E,A),p.constructor=M,p.render=de),O&&O.sub(p),p.stLine truncated var e,t,n,r,i,a,o,s,c,l,u,d,f,p,m={},h=[],g=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,_=Array.isArray;function v(e,t){for(var n in t)e[n]=t[n];return e}function y(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function b(t,n,r){var i,a,o,s={};for(o in n)o==`key`?i=n[o]:o==`ref`?a=n[o]:s[o]=n[o];if(arguments.length>2&&(s.children=arguments.length>3?e.call(arguments,2):r),typeof t==`function`&&t.defaultProps!=null)for(o in t.defaultProps)s[o]===void 0&&(s[o]=t.defaultProps[o]);return x(t,s,i,a,null)}function x(e,r,i,a,o){var s={type:e,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o??++n,__i:-1,__u:0};return o==null&&t.vnode!=null&&t.vnode(s),s}function S(e){return e.children}function C(e,t){this.props=e,this.context=t}function w(e,t){if(t==null)return e.__?w(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])!=null&&n.__e!=null)return n.__e;return typeof e.type==`function`?w(e):null}function T(e){if(e.__P&&e.__d){var n=e.__v,r=n.__e,i=[],a=[],o=v({},n);o.__v=n.__v+1,t.vnode&&t.vnode(o),ie(e.__P,o,n,e.__n,e.__P.namespaceURI,32&n.__u?[r]:null,i,r??w(n),!!(32&n.__u),a),o.__v=n.__v,o.__.__k[o.__i]=o,oe(i,o,a),n.__e=n.__=null,o.__e!=r&&E(o)}}function E(e){if((e=e.__)!=null&&e.__c!=null)return e.__e=e.__c.base=null,e.__k.some(function(t){if(t!=null&&t.__e!=null)return e.__e=e.__c.base=t.__e}),E(e)}function D(e){(!e.__d&&(e.__d=!0)&&r.push(e)&&!O.__r++||i!=t.debounceRendering)&&((i=t.debounceRendering)||a)(O)}function O(){try{for(var e,t=1;r.length;)r.length>t&&r.sort(o),e=r.shift(),t=r.length,T(e)}finally{r.length=O.__r=0}}function k(e,t,n,r,i,a,o,s,c,l,u){var d,f,p,g,_,v,y=r&&r.__k||h,b=t.length;for(c=A(n,t,y,c,b),d=0;d<b;d++)(p=n.__k[d])!=null&&(f=p.__i!=-1&&y[p.__i]||m,p.__i=d,v=ie(e,p,f,i,a,o,s,c,l,u),g=p.__e,p.ref&&f.ref!=p.ref&&(f.ref&&le(f.ref,null,p),u.push(p.ref,p.__c||g,p)),_==null&&g!=null&&(_=g),4&p.__u?(c=ee(p,c,e),f.__e&&(f.__e=null)):typeof p.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),p.__u&=-7);return n.__e=_,c}function A(e,t,n,r,i){var a,o,s,c,l,u=n.length,d=u,f=0;for(e.__k=Array(i),a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o==`string`||typeof o==`number`||typeof o==`bigint`||o.constructor==String?o=e.__k[a]=x(null,o,null,null,null):_(o)?o=e.__k[a]=x(S,{children:o},null,null,null):o.constructor===void 0&&o.__b>0?o=e.__k[a]=x(o.type,o.props,o.key,o.ref?o.ref:null,o.__v):e.__k[a]=o,c=a+f,o.__=e,o.__b=e.__b+1,s=null,(l=o.__i=te(o,n,c,d))!=-1&&(d--,(s=n[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(i>u?f--:i<u&&f++),typeof o.type!=`function`&&(o.__u|=4)):l!=c&&(l==c-1?f--:l==c+1?f++:(l>c?f--:f++,o.__u|=4))):e.__k[a]=null;if(d)for(a=0;a<u;a++)(s=n[a])!=null&&!(2&s.__u)&&(s.__e==r&&(r=w(s)),ue(s,s));return r}function ee(e,t,n){var r,i;if(typeof e.type==`function`){for(r=e.__k,i=0;r&&i<r.length;i++)r[i]&&(r[i].__=e,t=ee(r[i],t,n));return t}e.__e!=t&&(t&&e.type&&!t.parentNode&&(t=w(e)),t=n.insertBefore(e.__e,t||null));do t=t&&t.nextSibling;while(t!=null&&t.nodeType==8);return t}function j(e,t){return t=t||[],e==null||typeof e==`boolean`||(_(e)?e.some(function(e){j(e,t)}):t.push(e)),t}function te(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l!=null&&!(2&l.__u);if(l===null&&s==null||u&&s==l.key&&c==l.type)return n;if(r>+!!u){for(i=n-1,a=n+1;i>=0||a<t.length;)if((l=t[o=i>=0?i--:a++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function ne(e,t,n){t[0]==`-`?e.setProperty(t,n??``):e[t]=n==null?``:typeof n!=`number`||g.test(t)?n:n+`px`}function M(e,t,n,r,i){var a,o;n:if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||ne(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||ne(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)a=t!=(t=t.replace(u,`$1`)),o=t.toLowerCase(),t=o in e||t==`onFocusOut`||t==`onFocusIn`?o.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+a]=n,n?r?n[l]=r[l]:(n[l]=d,e.addEventListener(t,a?p:f,a)):e.removeEventListener(t,a?p:f,a);else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;break n}catch{}typeof n==`function`||(n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n))}}function re(e){return function(n){if(this.l){var r=this.l[n.type+e];if(n[c]==null)n[c]=d++;else if(n[c]<r[l])return;return r(t.event?t.event(n):n)}}}function ie(e,n,r,i,a,o,s,c,l,u){var d,f,p,m,g,b,x,T,E,D,O,A,ee,j,te,ne,M=n.type;if(n.constructor!==void 0)return null;128&r.__u&&(l=!!(32&r.__u),o=[c=n.__e=r.__e]),(d=t.__b)&&d(n);n:if(typeof M==`function`){f=s.length;try{if(E=n.props,D=M.prototype&&M.prototype.render,O=(d=M.contextType)&&i[d.__c],A=d?O?O.props.value:d.__:i,r.__c?T=(p=n.__c=r.__c).__=p.__E:(D?n.__c=p=new M(E,A):(n.__c=p=new C(E,A),p.constructor=M,p.render=de),O&&O.sub(p),p.stLine truncated
`)>-1?e.split(` `)>-1?e.split(`
`):e}function Cm(e,t){let{element:n,datasetIndex:r,index:i}=t,a=e.getDatasetMeta(r).controller,{label:o,value:s}=a.getLabelAndValue(i);return{chart:e,label:o,parsed:a.getParsed(i),raw:e.data.datasets[r].data[i],formattedValue:s,dataset:a.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function wm(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=gl(t.bodyFont),l=gl(t.titleFont),u=gl(t.footerFont),d=a.length,f=i.length,p=r.length,m=hl(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,Z(e.title,y),n.font=c.string,Z(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Z(r,e=>{Z(e.before,y),Z(e.lines,y),Z(e.after,y)}),v=0,n.font=u.string,Z(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Tm(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function Em(e,t,n,r){let{x:i,width:a}=r,o=n.caretSize+n.caretPadding;if(e===`left`&&i+a+o>t.width||e===`right`&&i-a-o<0)return!0}function Dm(e,t,n,r){let{x:i,width:a}=n,{width:o,chartArea:{left:s,right:c}}=e,l=`center`;return r===`center`?l=i<=(s+c)/2?`left`:`right`:i<=a/2?l=`left`:i>=o-a/2&&(l=`right`),Em(l,e,t,n)&&(l=`center`),l}function Om(e,t,n){let r=n.yAlign||t.yAlign||Tm(e,n);return{xAlign:n.xAlign||t.xAlign||Dm(e,t,n,r),yAlign:r}}function km(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Am(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function jm(e,t,n,r){let{caretSize:i,caretPadding:a,cornerRadius:o}=e,{xAlign:s,yAlign:c}=n,l=i+a,{topLeft:u,topRight:d,bottomLeft:f,bottomRight:p}=ml(o),m=km(t,s),h=Am(t,c,l);return c===`center`?s===`left`?m+=l:s===`right`&&(m-=l):s===`left`?m-=Math.max(u,f)+i:s===`right`&&(m+=Math.max(d,p)+i),{x:ic(m,0,r.width-t.width),y:ic(h,0,r.height-t.height)}}function Mm(e,t,n){let r=hl(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Nm(e){return xm([],Sm(e))}function Pm(e,t,n){return yl(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Fm(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Im={beforeTitle:ds,title(e){if(e.length>0){let t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode===`dataset`)return t.dataset.label||``;if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return``},afterTitle:ds,beforeBody:ds,beforeLabel:ds,label(e){if(this&&this.options&&this.options.mode===`dataset`)return e.label+`: `+e.formattedValue||e.formattedValue;let t=e.dataset.label||``;t&&(t+=`: `);let n=e.formattedValue;return J(n)||(t+=n),t},labelColor(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:t.borderColor,backgroundColor:t.backgroundColor,borderWidth:t.borderWidth,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:t.pointStyle,rotation:t.rotation}},afterLabel:ds,afterBody:ds,beforeFooter:ds,footer:ds,afterFooter:ds};function Lm(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Im[t].call(n,r):i}var Rm=class extends Af{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let e=this._cachedAnimations;if(e)return e;let t=this.chart,n=this.options.setContext(this.getContext()),r=n.enabled&&t.options.animation&&n.animations,i=new zu(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Pm(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=Lm(n,`beforeTitle`,this,e),i=Lm(n,`title`,this,e),a=Lm(n,`afterTitle`,this,e),o=[];return o=xm(o,Sm(r)),o=xm(o,Sm(i)),o=xm(o,Sm(a)),o}getBeforeBody(e,t){return Nm(Lm(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];return Z(e Line truncated `):e}function Cm(e,t){let{element:n,datasetIndex:r,index:i}=t,a=e.getDatasetMeta(r).controller,{label:o,value:s}=a.getLabelAndValue(i);return{chart:e,label:o,parsed:a.getParsed(i),raw:e.data.datasets[r].data[i],formattedValue:s,dataset:a.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function wm(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=gl(t.bodyFont),l=gl(t.titleFont),u=gl(t.footerFont),d=a.length,f=i.length,p=r.length,m=hl(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,Z(e.title,y),n.font=c.string,Z(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Z(r,e=>{Z(e.before,y),Z(e.lines,y),Z(e.after,y)}),v=0,n.font=u.string,Z(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Tm(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function Em(e,t,n,r){let{x:i,width:a}=r,o=n.caretSize+n.caretPadding;if(e===`left`&&i+a+o>t.width||e===`right`&&i-a-o<0)return!0}function Dm(e,t,n,r){let{x:i,width:a}=n,{width:o,chartArea:{left:s,right:c}}=e,l=`center`;return r===`center`?l=i<=(s+c)/2?`left`:`right`:i<=a/2?l=`left`:i>=o-a/2&&(l=`right`),Em(l,e,t,n)&&(l=`center`),l}function Om(e,t,n){let r=n.yAlign||t.yAlign||Tm(e,n);return{xAlign:n.xAlign||t.xAlign||Dm(e,t,n,r),yAlign:r}}function km(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Am(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function jm(e,t,n,r){let{caretSize:i,caretPadding:a,cornerRadius:o}=e,{xAlign:s,yAlign:c}=n,l=i+a,{topLeft:u,topRight:d,bottomLeft:f,bottomRight:p}=ml(o),m=km(t,s),h=Am(t,c,l);return c===`center`?s===`left`?m+=l:s===`right`&&(m-=l):s===`left`?m-=Math.max(u,f)+i:s===`right`&&(m+=Math.max(d,p)+i),{x:ic(m,0,r.width-t.width),y:ic(h,0,r.height-t.height)}}function Mm(e,t,n){let r=hl(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Nm(e){return xm([],Sm(e))}function Pm(e,t,n){return yl(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Fm(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Im={beforeTitle:ds,title(e){if(e.length>0){let t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode===`dataset`)return t.dataset.label||``;if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return``},afterTitle:ds,beforeBody:ds,beforeLabel:ds,label(e){if(this&&this.options&&this.options.mode===`dataset`)return e.label+`: `+e.formattedValue||e.formattedValue;let t=e.dataset.label||``;t&&(t+=`: `);let n=e.formattedValue;return J(n)||(t+=n),t},labelColor(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:t.borderColor,backgroundColor:t.backgroundColor,borderWidth:t.borderWidth,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:t.pointStyle,rotation:t.rotation}},afterLabel:ds,afterBody:ds,beforeFooter:ds,footer:ds,afterFooter:ds};function Lm(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Im[t].call(n,r):i}var Rm=class extends Af{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let e=this._cachedAnimations;if(e)return e;let t=this.chart,n=this.options.setContext(this.getContext()),r=n.enabled&&t.options.animation&&n.animations,i=new zu(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Pm(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=Lm(n,`beforeTitle`,this,e),i=Lm(n,`title`,this,e),a=Lm(n,`afterTitle`,this,e),o=[];return o=xm(o,Sm(r)),o=xm(o,Sm(i)),o=xm(o,Sm(a)),o}getBeforeBody(e,t){return Nm(Lm(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];return Z(e Line truncated
@@ -8,6 +8,7 @@ EVENTS_BIN="$ROOT_DIR/root/usr/libexec/smartsafehub-events"
LICENSE_BIN="$ROOT_DIR/root/usr/libexec/smartsafehub-license" LICENSE_BIN="$ROOT_DIR/root/usr/libexec/smartsafehub-license"
SAFE_ADAPTER="$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/safeshield-management.uc" SAFE_ADAPTER="$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/safeshield-management.uc"
HEALTH_BIN="$ROOT_DIR/root/usr/libexec/smartsafehub-health" HEALTH_BIN="$ROOT_DIR/root/usr/libexec/smartsafehub-health"
ACTIVITY_RPC="$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/activity.uc"
CONFIG="$ROOT_DIR/root/etc/config/smartsafehub" CONFIG="$ROOT_DIR/root/etc/config/smartsafehub"
INIT_SYNC="$ROOT_DIR/root/etc/init.d/smartsafehub-activity-sync" INIT_SYNC="$ROOT_DIR/root/etc/init.d/smartsafehub-activity-sync"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
@@ -18,7 +19,7 @@ fail() {
exit 1 exit 1
} }
for file in "$SYNC_BIN" "$EVENTS_BIN" "$LICENSE_BIN" "$SAFE_ADAPTER" "$CONFIG" "$INIT_SYNC"; do for file in "$SYNC_BIN" "$EVENTS_BIN" "$LICENSE_BIN" "$SAFE_ADAPTER" "$ACTIVITY_RPC" "$CONFIG" "$INIT_SYNC"; do
[ -f "$file" ] || fail "missing activity producer/sync component: $file" [ -f "$file" ] || fail "missing activity producer/sync component: $file"
done done
@@ -45,10 +46,18 @@ grep -Fq 'cloud_sync_enabled' "$SYNC_BIN" || fail 'activity sync must honor the
grep -Fq 'apply-config' "$SYNC_BIN" || fail 'activity sync must expose a runtime apply command for immediate toggle cleanup' grep -Fq 'apply-config' "$SYNC_BIN" || fail 'activity sync must expose a runtime apply command for immediate toggle cleanup'
grep -Fq 'activity_cloud_sync_enabled' "$LICENSE_BIN" || fail 'license status must not retain an activity credential while Cloud transfer is disabled' grep -Fq 'activity_cloud_sync_enabled' "$LICENSE_BIN" || fail 'license status must not retain an activity credential while Cloud transfer is disabled'
grep -Fq "option cloud_sync_enabled '0'" "$CONFIG" || fail 'fresh installs must default Cloud activity transfer to OFF' grep -Fq "option cloud_sync_enabled '0'" "$CONFIG" || fail 'fresh installs must default Cloud activity transfer to OFF'
grep -Fq 'activity-cloud-sync-upgrade-enable' "$ROOT_DIR/Makefile" || fail 'package upgrade must preserve pre-r19 implicit Cloud sync' if grep -Fq 'activity-cloud-sync-upgrade-enable' "$ROOT_DIR/Makefile"; then
grep -Fq "smartsafehub.activity.cloud_sync_enabled='1'" "$ROOT_DIR/Makefile" || fail 'postinst must restore Cloud sync for pre-r19 upgrades' fail 'package install scripts must not retain the retired pre-r19 implicit Cloud-sync migration marker'
fi
if grep -Fq "smartsafehub.activity.cloud_sync_enabled='1'" "$ROOT_DIR/Makefile"; then
fail 'package install scripts must not force-enable Cloud activity sync during upgrades'
fi
grep -Fq "set smartsafehub.activity.cloud_sync_enabled='0'" "$INIT_SYNC" || fail 'dynamically created activity sections must default Cloud activity transfer to OFF' grep -Fq "set smartsafehub.activity.cloud_sync_enabled='0'" "$INIT_SYNC" || fail 'dynamically created activity sections must default Cloud activity transfer to OFF'
grep -Fq "1|true|on|yes|'') return 0" "$SYNC_BIN" || fail 'missing pre-r19 Cloud preference must preserve legacy enabled behavior on upgrade' grep -Fq "1|true|on|yes) return 0" "$SYNC_BIN" || fail 'activity sync must enable Cloud upload only for explicit positive values'
grep -Fq "1|true|on|yes) return 0" "$EVENTS_BIN" || fail 'event outbox creation must require an explicit positive Cloud preference'
grep -Fq "1|true|on|yes) return 0" "$LICENSE_BIN" || fail 'license credential caching must require an explicit positive Cloud preference'
grep -Fq "return bool_config(ctx.get('smartsafehub', 'activity', 'cloud_sync_enabled'), false);" "$ACTIVITY_RPC" || \
fail 'activity RPC must interpret a missing Cloud preference as OFF'
if grep -Fq '$base/licenses/status' "$SYNC_BIN" || grep -Fq '$base/licenses/resolve' "$SYNC_BIN" || grep -Fq 'post_json()' "$SYNC_BIN"; then if grep -Fq '$base/licenses/status' "$SYNC_BIN" || grep -Fq '$base/licenses/resolve' "$SYNC_BIN" || grep -Fq 'post_json()' "$SYNC_BIN"; then
fail 'activity sync must not duplicate license API calls; smartsafehub-license owns license status and activity credential acquisition' fail 'activity sync must not duplicate license API calls; smartsafehub-license owns license status and activity credential acquisition'
fi fi
@@ -203,8 +203,8 @@ grep -Fq 'activityHistory?: ActivityHistory;' "$API" || \
fail 'frontend must tolerate a pre-reload status response without activityHistory' fail 'frontend must tolerate a pre-reload status response without activityHistory'
grep -Fq 'const activity = status.activityHistory ?? emptyActivityHistory();' "$API" || \ grep -Fq 'const activity = status.activityHistory ?? emptyActivityHistory();' "$API" || \
fail 'frontend must render an empty activity state while an older in-memory RPC handler is draining' fail 'frontend must render an empty activity state while an older in-memory RPC handler is draining'
grep -Fq 'enabled: activity.cloud.enabled ?? true' "$API" || \ grep -Fq 'enabled: activity.cloud.enabled ?? false' "$API" || \
fail 'pre-r19 activity responses must retain the legacy always-enabled Cloud behavior during rolling upgrades' fail 'activity responses without an explicit Cloud toggle must fail closed to OFF'
grep -Fq 'const ACTIVITY_REFRESH_INTERVAL_MS = 60_000;' "$HOOK" || \ grep -Fq 'const ACTIVITY_REFRESH_INTERVAL_MS = 60_000;' "$HOOK" || \
fail 'recent activity may refresh at a lightweight one-minute interval' fail 'recent activity may refresh at a lightweight one-minute interval'
grep -Fq 'refreshOnFocus: true' "$HOOK" || \ grep -Fq 'refreshOnFocus: true' "$HOOK" || \
@@ -82,6 +82,20 @@ grep -Fq 'autoComplete="current-password"' "$SETTINGS_PAGE" || \
fail 'current administrator password must expose password-manager autocomplete semantics' fail 'current administrator password must expose password-manager autocomplete semantics'
grep -Fq 'autoComplete="new-password"' "$SETTINGS_PAGE" || \ grep -Fq 'autoComplete="new-password"' "$SETTINGS_PAGE" || \
fail 'new administrator password fields must expose new-password autocomplete semantics' fail 'new administrator password fields must expose new-password autocomplete semantics'
awk '
/aria-label="새 비밀번호 요구 사항"/ { in_requirements = 1 }
in_requirements && /<div class="pt-2">/ { has_spacing = 1 }
in_requirements && /id="smartsafehub-confirm-admin-password"/ { reached_confirmation = 1 }
END { exit !(has_spacing && reached_confirmation) }
' "$SETTINGS_PAGE" || \
fail 'administrator password confirmation must keep extra spacing below the password requirement guidance'
awk '
/새 비밀번호 확인 값이 일치하지 않습니다/ { after_confirmation_status = 1 }
after_confirmation_status && /<div class="pt-2">/ { has_notice_spacing = 1 }
after_confirmation_status && /비밀번호는 현재 공유기에 직접 적용되며 외부 서버로 전송되지 않습니다/ { reached_local_notice = 1 }
END { exit !(has_notice_spacing && reached_local_notice) }
' "$SETTINGS_PAGE" || \
fail 'administrator password status feedback must keep extra spacing before the local-only notice'
grep -Fq 'await changeRootPassword(currentPassword, newPassword);' "$SETTINGS_PAGE" || \ grep -Fq 'await changeRootPassword(currentPassword, newPassword);' "$SETTINGS_PAGE" || \
fail 'administrator password card must call the dedicated frontend API' fail 'administrator password card must call the dedicated frontend API'
grep -Fq 'await logoutLuciSession();' "$SETTINGS_PAGE" || \ grep -Fq 'await logoutLuciSession();' "$SETTINGS_PAGE" || \
@@ -4,6 +4,7 @@ set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)" ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
HELPER="$ROOT_DIR/root/usr/libexec/smartsafehub-backup" HELPER="$ROOT_DIR/root/usr/libexec/smartsafehub-backup"
KEEP_FILE="$ROOT_DIR/root/lib/upgrade/keep.d/smartsafehub"
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
@@ -14,11 +15,24 @@ fail() {
make_backup() { make_backup() {
archive="$1" archive="$1"
device_code="${2:-iptime-ax3000sm}"
build_id="${3:-20260901T010203Z-oldbuild}"
root="$TMP_DIR/archive" root="$TMP_DIR/archive"
rm -rf "$root" rm -rf "$root"
mkdir -p "$root/etc/config" "$root/etc/dropbear" mkdir -p "$root/etc/config" "$root/etc/dropbear" "$root/usr/share/smartsafehub"
printf "config system 'system'\n\toption hostname 'SmartSafeHub'\n" > "$root/etc/config/system" printf "config system 'system'\n\toption hostname 'SmartSafeHub'\n" > "$root/etc/config/system"
printf 'root:x:0:0:root:/root:/bin/ash\n' > "$root/etc/passwd" printf 'root:x:0:0:root:/root:/bin/ash\n' > "$root/etc/passwd"
printf '{"schema":1,"device_code":"%s","build_id":"%s","channel":"stable"}\n' \
"$device_code" "$build_id" > "$root/usr/share/smartsafehub/firmware.json"
tar -czf "$archive" -C "$root" etc usr
}
make_legacy_backup() {
archive="$1"
root="$TMP_DIR/legacy"
rm -rf "$root"
mkdir -p "$root/etc/config"
printf "config system 'system'\n\toption hostname 'Legacy'\n" > "$root/etc/config/system"
tar -czf "$archive" -C "$root" etc tar -czf "$archive" -C "$root" etc
} }
@@ -44,7 +58,7 @@ SYSUPGRADE_BIN="$TMP_DIR/sysupgrade"
REBOOT_BIN="$TMP_DIR/reboot" REBOOT_BIN="$TMP_DIR/reboot"
UCI_BIN="$TMP_DIR/uci" UCI_BIN="$TMP_DIR/uci"
JSONFILTER_BIN="$TMP_DIR/jsonfilter" JSONFILTER_BIN="$TMP_DIR/jsonfilter"
FIRMWARE_METADATA_FILE="$TMP_DIR/firmware.json" FIRMWARE_METADATA_FILE="$TMP_DIR/installed/usr/share/smartsafehub/firmware.json"
UPDATE_STATE_FILE="$TMP_DIR/updates.state" UPDATE_STATE_FILE="$TMP_DIR/updates.state"
FIRMWARE_STATE_FILE="$TMP_DIR/firmware.state" FIRMWARE_STATE_FILE="$TMP_DIR/firmware.state"
UPDATE_LOCK_DIR="$TMP_DIR/updater.lock" UPDATE_LOCK_DIR="$TMP_DIR/updater.lock"
@@ -53,10 +67,19 @@ FIRMWARE_IMAGE_FILE="$TMP_DIR/firmware.bin"
SYSUPGRADE_LOG="$TMP_DIR/sysupgrade.log" SYSUPGRADE_LOG="$TMP_DIR/sysupgrade.log"
REBOOT_MARKER="$TMP_DIR/rebooted" REBOOT_MARKER="$TMP_DIR/rebooted"
UCI_LOG="$TMP_DIR/uci.log" UCI_LOG="$TMP_DIR/uci.log"
CURRENT_BUILD_ID='20260929T010203Z-currentbuild'
mkdir -p "$(dirname "$FIRMWARE_METADATA_FILE")"
printf '{"schema":1,"device_code":"iptime-ax3000sm","build_id":"%s","channel":"stable"}\n' \
"$CURRENT_BUILD_ID" > "$FIRMWARE_METADATA_FILE"
cat > "$SYSUPGRADE_BIN" <<EOF_SYSUPGRADE cat > "$SYSUPGRADE_BIN" <<EOF_SYSUPGRADE
#!/bin/sh #!/bin/sh
printf '%s\\n' "\$*" >> "$SYSUPGRADE_LOG" printf '%s\n' "\$*" >> "$SYSUPGRADE_LOG"
if [ "\${1:-}" = '--restore-backup' ]; then
tar -xOzf "\$2" usr/share/smartsafehub/firmware.json > \
"\$SMARTSAFEHUB_BACKUP_FIRMWARE_METADATA_FILE"
fi
exit 0 exit 0
EOF_SYSUPGRADE EOF_SYSUPGRADE
cat > "$REBOOT_BIN" <<EOF_REBOOT cat > "$REBOOT_BIN" <<EOF_REBOOT
@@ -65,43 +88,80 @@ touch "$REBOOT_MARKER"
EOF_REBOOT EOF_REBOOT
cat > "$UCI_BIN" <<EOF_UCI cat > "$UCI_BIN" <<EOF_UCI
#!/bin/sh #!/bin/sh
printf '%s\\n' "\$*" >> "$UCI_LOG" printf '%s\n' "\$*" >> "$UCI_LOG"
exit 0 exit 0
EOF_UCI EOF_UCI
cat > "$JSONFILTER_BIN" <<'EOF_JSONFILTER' cat > "$JSONFILTER_BIN" <<'EOF_JSONFILTER'
#!/bin/sh #!/bin/sh
printf '%s\n' '20260915T010203Z-abcd1234' file=''
expr=''
while [ "$#" -gt 0 ]; do
case "$1" in
-i) file="$2"; shift 2 ;;
-e) expr="$2"; shift 2 ;;
*) shift ;;
esac
done
[ -f "$file" ] || exit 0
case "$expr" in
'@.device_code') sed -n 's/.*"device_code"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$file" ;;
'@.build_id') sed -n 's/.*"build_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$file" ;;
esac
EOF_JSONFILTER EOF_JSONFILTER
chmod +x "$SYSUPGRADE_BIN" "$REBOOT_BIN" "$UCI_BIN" "$JSONFILTER_BIN" chmod +x "$SYSUPGRADE_BIN" "$REBOOT_BIN" "$UCI_BIN" "$JSONFILTER_BIN"
[ -f "$KEEP_FILE" ] || fail 'SmartSafeHub sysupgrade keep.d file must exist'
grep -Fxq '/usr/share/smartsafehub/firmware.json' "$KEEP_FILE" || \
fail 'standard OpenWrt backups must carry the existing SmartSafeHub firmware metadata'
make_backup "$BACKUP_FILE" make_backup "$BACKUP_FILE"
run_helper validate || fail 'a normal OpenWrt-style configuration archive must validate' run_helper validate || fail 'a same-device OpenWrt-style configuration archive must validate'
make_backup "$BACKUP_FILE" 'xiaomi-ax3000t'
set +e
run_helper validate >/dev/null 2>&1
status=$?
set -e
[ "$status" -eq 65 ] || fail 'a backup from another device must fail with device mismatch status 65'
make_legacy_backup "$BACKUP_FILE"
set +e
run_helper validate >/dev/null 2>&1
status=$?
set -e
[ "$status" -eq 66 ] || fail 'a backup without firmware device metadata must fail closed with status 66'
printf 'not-a-gzip' > "$BACKUP_FILE" printf 'not-a-gzip' > "$BACKUP_FILE"
if run_helper validate >/dev/null 2>&1; then if run_helper validate >/dev/null 2>&1; then
fail 'non-gzip input must be rejected' fail 'non-gzip input must be rejected'
fi fi
mkdir -p "$TMP_DIR/no-config/etc" mkdir -p "$TMP_DIR/no-config/usr/share/smartsafehub" "$TMP_DIR/no-config/etc"
printf '{"device_code":"iptime-ax3000sm"}\n' > "$TMP_DIR/no-config/usr/share/smartsafehub/firmware.json"
printf 'invalid\n' > "$TMP_DIR/no-config/etc/banner" printf 'invalid\n' > "$TMP_DIR/no-config/etc/banner"
tar -czf "$BACKUP_FILE" -C "$TMP_DIR/no-config" etc tar -czf "$BACKUP_FILE" -C "$TMP_DIR/no-config" etc usr
if run_helper validate >/dev/null 2>&1; then if run_helper validate >/dev/null 2>&1; then
fail 'an archive without /etc/config content must be rejected' fail 'an archive without /etc/config content must be rejected'
fi fi
mkdir -p "$TMP_DIR/empty-config/etc/config" mkdir -p "$TMP_DIR/empty-config/etc/config" "$TMP_DIR/empty-config/usr/share/smartsafehub"
tar -czf "$BACKUP_FILE" -C "$TMP_DIR/empty-config" etc printf '{"device_code":"iptime-ax3000sm"}\n' > "$TMP_DIR/empty-config/usr/share/smartsafehub/firmware.json"
tar -czf "$BACKUP_FILE" -C "$TMP_DIR/empty-config" etc usr
if run_helper validate >/dev/null 2>&1; then if run_helper validate >/dev/null 2>&1; then
fail 'an archive with only an empty /etc/config directory must be rejected' fail 'an archive with only an empty /etc/config directory must be rejected'
fi fi
make_backup "$BACKUP_FILE" make_backup "$BACKUP_FILE" 'iptime-ax3000sm' '20260901T010203Z-oldbuild'
printf '{"build_id":"20260915T010203Z-abcd1234"}\n' > "$FIRMWARE_METADATA_FILE" run_helper restore || fail 'validated same-device backup must restore successfully'
run_helper restore || fail 'validated backup must restore successfully'
grep -Fxq -- "--restore-backup $BACKUP_FILE" "$SYSUPGRADE_LOG" || \ grep -Fxq -- "--restore-backup $BACKUP_FILE" "$SYSUPGRADE_LOG" || \
fail 'restore must delegate to sysupgrade --restore-backup' fail 'restore must delegate to sysupgrade --restore-backup'
[ ! -e "$BACKUP_FILE" ] || fail 'successful restore must delete the uploaded archive' [ ! -e "$BACKUP_FILE" ] || fail 'successful restore must delete the uploaded archive'
grep -Fq 'set smartsafehub.firmware.current_build_id=20260915T010203Z-abcd1234' "$UCI_LOG" || \ grep -Fq "\"build_id\":\"$CURRENT_BUILD_ID\"" "$FIRMWARE_METADATA_FILE" || \
fail 'restore must keep firmware.json from the currently installed image, not the backup source image'
if grep -Fq '20260901T010203Z-oldbuild' "$FIRMWARE_METADATA_FILE"; then
fail 'backup source build metadata must not replace the installed firmware metadata'
fi
grep -Fq "set smartsafehub.firmware.current_build_id=$CURRENT_BUILD_ID" "$UCI_LOG" || \
fail 'restore must re-sync firmware build identity from the installed image metadata' fail 'restore must re-sync firmware build identity from the installed image metadata'
grep -Fq 'commit smartsafehub' "$UCI_LOG" || \ grep -Fq 'commit smartsafehub' "$UCI_LOG" || \
fail 'firmware build identity re-sync must be committed' fail 'firmware build identity re-sync must be committed'
@@ -113,7 +173,7 @@ done
[ -e "$REBOOT_MARKER" ] || fail 'successful restore must schedule a reboot' [ -e "$REBOOT_MARKER" ] || fail 'successful restore must schedule a reboot'
: > "$SYSUPGRADE_LOG" : > "$SYSUPGRADE_LOG"
rm -f "$REBOOT_MARKER" "$FIRMWARE_METADATA_FILE" rm -f "$REBOOT_MARKER"
make_backup "$BACKUP_FILE" make_backup "$BACKUP_FILE"
touch "$FIRMWARE_IMAGE_FILE" touch "$FIRMWARE_IMAGE_FILE"
set +e set +e
@@ -125,8 +185,11 @@ set -e
[ -e "$BACKUP_FILE" ] || fail 'busy restore must keep the validated archive for retry' [ -e "$BACKUP_FILE" ] || fail 'busy restore must keep the validated archive for retry'
rm -f "$FIRMWARE_IMAGE_FILE" rm -f "$FIRMWARE_IMAGE_FILE"
cat > "$SYSUPGRADE_BIN" <<'EOF_SYSUPGRADE_FAIL' cat > "$SYSUPGRADE_BIN" <<EOF_SYSUPGRADE_FAIL
#!/bin/sh #!/bin/sh
# Simulate a partially applied restore before sysupgrade reports failure.
tar -xOzf "\$2" usr/share/smartsafehub/firmware.json > \
"\$SMARTSAFEHUB_BACKUP_FIRMWARE_METADATA_FILE"
exit 1 exit 1
EOF_SYSUPGRADE_FAIL EOF_SYSUPGRADE_FAIL
chmod +x "$SYSUPGRADE_BIN" chmod +x "$SYSUPGRADE_BIN"
@@ -136,6 +199,8 @@ status=$?
set -e set -e
[ "$status" -ne 0 ] || fail 'sysupgrade restore failure must propagate as an error' [ "$status" -ne 0 ] || fail 'sysupgrade restore failure must propagate as an error'
[ -e "$BACKUP_FILE" ] || fail 'failed restore must keep the archive for inspection or retry' [ -e "$BACKUP_FILE" ] || fail 'failed restore must keep the archive for inspection or retry'
grep -Fq "\"build_id\":\"$CURRENT_BUILD_ID\"" "$FIRMWARE_METADATA_FILE" || \
fail 'failed restore must also restore the installed firmware metadata after a partial write'
[ ! -e "$REBOOT_MARKER" ] || fail 'failed restore must never reboot the device' [ ! -e "$REBOOT_MARKER" ] || fail 'failed restore must never reboot the device'
run_helper discard || fail 'discard command must succeed' run_helper discard || fail 'discard command must succeed'
@@ -144,4 +209,4 @@ run_helper discard || fail 'discard command must succeed'
grep -Fq '/* | .. | ../* | */.. | */../*' "$HELPER" || \ grep -Fq '/* | .. | ../* | */.. | */../*' "$HELPER" || \
fail 'backup validation must reject path traversal entries before restore' fail 'backup validation must reject path traversal entries before restore'
echo 'PASS: configuration backup validation, restore safety, firmware identity sync and reboot contract are consistent' echo 'PASS: configuration backup validation, same-device enforcement, metadata preservation and reboot safety are consistent'
@@ -66,6 +66,7 @@ require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-maintenance"
require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-health" require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-health"
require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-license" require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-license"
require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-backup" require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-backup"
require_file "$ROOT_DIR/root/lib/upgrade/keep.d/smartsafehub"
require_file "$ROOT_DIR/root/usr/libexec/smartsafehub-root-entry" require_file "$ROOT_DIR/root/usr/libexec/smartsafehub-root-entry"
require_executable "$ROOT_DIR/root/etc/uci-defaults/90-smartsafehub-system-defaults" require_executable "$ROOT_DIR/root/etc/uci-defaults/90-smartsafehub-system-defaults"
require_executable "$ROOT_DIR/root/etc/uci-defaults/99-smartsafehub-root-entry" require_executable "$ROOT_DIR/root/etc/uci-defaults/99-smartsafehub-root-entry"
@@ -225,6 +225,16 @@ grep -Fq '"$SYSUPGRADE_BIN" --restore-backup "$BACKUP_FILE"' "$BACKUP_HELPER" ||
fail 'configuration restore helper must use the OpenWrt sysupgrade restore path' fail 'configuration restore helper must use the OpenWrt sysupgrade restore path'
grep -Fq 'restore_is_busy && return 75' "$BACKUP_HELPER" || \ grep -Fq 'restore_is_busy && return 75' "$BACKUP_HELPER" || \
fail 'configuration restore must not run while firmware or management update work is active' fail 'configuration restore must not run while firmware or management update work is active'
grep -Fq 'BACKUP_DEVICE_MISMATCH=65' "$BACKUP_HELPER" || \
fail 'configuration restore must distinguish backups created for another SmartSafeHub device'
grep -Fq 'FIRMWARE_METADATA_ARCHIVE_PATH="usr/share/smartsafehub/firmware.json"' "$BACKUP_HELPER" || \
fail 'configuration backup validation must reuse firmware.json as the source device identity'
grep -Fq 'restore_installed_firmware_metadata "$saved_metadata"' "$BACKUP_HELPER" || \
fail 'configuration restore must restore the currently installed firmware metadata after sysupgrade extraction'
grep -Fq 'SYSTEM_BACKUP_DEVICE_MISMATCH' "$BACKUP_MODULE" || \
fail 'configuration restore RPC must expose a same-device mismatch error'
grep -Fq 'SYSTEM_BACKUP_DEVICE_UNVERIFIED' "$BACKUP_MODULE" || \
fail 'configuration restore RPC must fail closed when source device identity cannot be verified'
grep -Fq 'current_build_id="$build_id"' "$BACKUP_HELPER" || \ grep -Fq 'current_build_id="$build_id"' "$BACKUP_HELPER" || \
fail 'configuration restore must re-sync current firmware identity after applying an older backup' fail 'configuration restore must re-sync current firmware identity after applying an older backup'
@@ -13,17 +13,17 @@ fail() {
[ -x "$DEFAULTS" ] || fail 'SmartSafeHub system defaults must be executable' [ -x "$DEFAULTS" ] || fail 'SmartSafeHub system defaults must be executable'
grep -Fq "uci -q set system.@system[0].hostname='SmartRouter'" "$DEFAULTS" || \ grep -Fq "uci -q set system.@system[0].hostname='SmartRouter'" "$DEFAULTS" || \
fail 'system defaults must set the SmartSafeHub hostname' fail 'system defaults must set the SmartSafeHub hostname on fresh OpenWrt defaults'
grep -Fq "uci -q set system.@system[0].zonename='Asia/Seoul'" "$DEFAULTS" || \ grep -Fq "uci -q set system.@system[0].zonename='Asia/Seoul'" "$DEFAULTS" || \
fail 'system defaults must set the IANA timezone name' fail 'system defaults must set the IANA timezone name on fresh OpenWrt defaults'
grep -Fq "uci -q set system.@system[0].timezone='UTC+9'" "$DEFAULTS" || \ grep -Fq "uci -q set system.@system[0].timezone='UTC+9'" "$DEFAULTS" || \
fail 'system defaults must keep the existing SmartSafeHub POSIX timezone default' fail 'system defaults must keep the existing SmartSafeHub POSIX timezone default'
grep -Fq "uci -q set system.@system[0].log_size='64'" "$DEFAULTS" || \ grep -Fq "set_if_missing system.@system[0].log_size '64'" "$DEFAULTS" || \
fail 'system defaults must keep the configured log size' fail 'system defaults must only fill a missing log size'
grep -Fq "uci -q set system.ntp='timeserver'" "$DEFAULTS" || \ grep -Fq "uci -q set system.ntp='timeserver'" "$DEFAULTS" || \
fail 'system defaults must create the named NTP section when it is missing' fail 'system defaults must create the named NTP section when it is missing'
grep -Fq "uci -q add_list system.ntp.server='3.openwrt.pool.ntp.org'" "$DEFAULTS" || \ grep -Fq "uci -q add_list system.ntp.server='3.openwrt.pool.ntp.org'" "$DEFAULTS" || \
fail 'system defaults must configure the OpenWrt NTP pool' fail 'system defaults must configure the OpenWrt NTP pool only when no server list exists'
grep -Fq 'uci -q commit system' "$DEFAULTS" || \ grep -Fq 'uci -q commit system' "$DEFAULTS" || \
fail 'system defaults must commit the system UCI package' fail 'system defaults must commit the system UCI package'
@@ -40,9 +40,25 @@ trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
cat > "$TMP_DIR/uci" <<'MOCK' cat > "$TMP_DIR/uci" <<'MOCK'
#!/bin/sh #!/bin/sh
printf '%s\n' "$*" >> "$MOCK_UCI_LOG" printf '%s\n' "$*" >> "$MOCK_UCI_LOG"
mock_value() {
value="$1"
[ "$value" != '__MISSING__' ] || return 1
printf '%s\n' "$value"
}
case "$*" in case "$*" in
'-q get system.@system[0]') exit "${MOCK_SYSTEM_GET_RC:-0}" ;; '-q get system.@system[0]') exit "${MOCK_SYSTEM_GET_RC:-0}" ;;
'-q get system.ntp') exit "${MOCK_NTP_GET_RC:-0}" ;; '-q get system.ntp') exit "${MOCK_NTP_GET_RC:-0}" ;;
'-q get system.@system[0].hostname') mock_value "${MOCK_HOSTNAME:-__MISSING__}" ;;
'-q get system.@system[0].zonename') mock_value "${MOCK_ZONENAME:-__MISSING__}" ;;
'-q get system.@system[0].timezone') mock_value "${MOCK_TIMEZONE:-__MISSING__}" ;;
'-q get system.@system[0].ttylogin') mock_value "${MOCK_TTYLOGIN:-__MISSING__}" ;;
'-q get system.@system[0].log_size') mock_value "${MOCK_LOG_SIZE:-__MISSING__}" ;;
'-q get system.@system[0].urandom_seed') mock_value "${MOCK_URANDOM_SEED:-__MISSING__}" ;;
'-q get system.ntp.enabled') mock_value "${MOCK_NTP_ENABLED:-__MISSING__}" ;;
'-q get system.ntp.enable_server') mock_value "${MOCK_NTP_ENABLE_SERVER:-__MISSING__}" ;;
'-q get system.ntp.server') mock_value "${MOCK_NTP_SERVERS:-__MISSING__}" ;;
esac esac
exit 0 exit 0
MOCK MOCK
@@ -53,6 +69,15 @@ PATH="$TMP_DIR:$PATH" \
MOCK_UCI_LOG="$LOG_EXISTING" \ MOCK_UCI_LOG="$LOG_EXISTING" \
MOCK_SYSTEM_GET_RC=0 \ MOCK_SYSTEM_GET_RC=0 \
MOCK_NTP_GET_RC=0 \ MOCK_NTP_GET_RC=0 \
MOCK_HOSTNAME='HomeGateway' \
MOCK_ZONENAME='America/New_York' \
MOCK_TIMEZONE='EST5EDT,M3.2.0,M11.1.0' \
MOCK_TTYLOGIN='1' \
MOCK_LOG_SIZE='128' \
MOCK_URANDOM_SEED='1' \
MOCK_NTP_ENABLED='0' \
MOCK_NTP_ENABLE_SERVER='1' \
MOCK_NTP_SERVERS='time.example.net' \
/bin/sh "$DEFAULTS" /bin/sh "$DEFAULTS"
if grep -Fq -- '-q add system system' "$LOG_EXISTING"; then if grep -Fq -- '-q add system system' "$LOG_EXISTING"; then
@@ -61,10 +86,49 @@ fi
if grep -Fq -- '-q set system.ntp=timeserver' "$LOG_EXISTING"; then if grep -Fq -- '-q set system.ntp=timeserver' "$LOG_EXISTING"; then
fail 'existing NTP section must not be recreated' fail 'existing NTP section must not be recreated'
fi fi
for forbidden in \
'-q set system.@system[0].hostname=' \
'-q set system.@system[0].zonename=' \
'-q set system.@system[0].timezone=' \
'-q set system.@system[0].ttylogin=' \
'-q set system.@system[0].log_size=' \
'-q set system.@system[0].urandom_seed=' \
'-q set system.ntp.enabled=' \
'-q set system.ntp.enable_server=' \
'-q add_list system.ntp.server='; do
if grep -Fq -- "$forbidden" "$LOG_EXISTING"; then
fail "existing user setting must be preserved: $forbidden"
fi
done
if grep -Fq 'compat_version' "$LOG_EXISTING"; then if grep -Fq 'compat_version' "$LOG_EXISTING"; then
fail 'runtime defaults must not mutate compat_version' fail 'runtime defaults must not mutate compat_version'
fi fi
LOG_FRESH="$TMP_DIR/fresh.log"
PATH="$TMP_DIR:$PATH" \
MOCK_UCI_LOG="$LOG_FRESH" \
MOCK_SYSTEM_GET_RC=0 \
MOCK_NTP_GET_RC=0 \
MOCK_HOSTNAME='OpenWrt' \
MOCK_ZONENAME='UTC' \
MOCK_TIMEZONE='UTC0' \
MOCK_TTYLOGIN='__MISSING__' \
MOCK_LOG_SIZE='__MISSING__' \
MOCK_URANDOM_SEED='__MISSING__' \
MOCK_NTP_ENABLED='__MISSING__' \
MOCK_NTP_ENABLE_SERVER='__MISSING__' \
MOCK_NTP_SERVERS='__MISSING__' \
/bin/sh "$DEFAULTS"
grep -Fq -- '-q set system.@system[0].hostname=SmartRouter' "$LOG_FRESH" || \
fail 'fresh OpenWrt hostname must receive the SmartSafeHub default'
grep -Fq -- '-q set system.@system[0].zonename=Asia/Seoul' "$LOG_FRESH" || \
fail 'fresh OpenWrt timezone must receive the SmartSafeHub zonename default'
grep -Fq -- '-q set system.@system[0].timezone=UTC+9' "$LOG_FRESH" || \
fail 'fresh OpenWrt timezone must receive the SmartSafeHub POSIX timezone default'
grep -Fq -- '-q add_list system.ntp.server=0.openwrt.pool.ntp.org' "$LOG_FRESH" || \
fail 'fresh OpenWrt NTP configuration must receive the default server list'
LOG_MISSING="$TMP_DIR/missing.log" LOG_MISSING="$TMP_DIR/missing.log"
PATH="$TMP_DIR:$PATH" \ PATH="$TMP_DIR:$PATH" \
MOCK_UCI_LOG="$LOG_MISSING" \ MOCK_UCI_LOG="$LOG_MISSING" \
@@ -77,4 +141,4 @@ grep -Fq -- '-q add system system' "$LOG_MISSING" || \
grep -Fq -- '-q set system.ntp=timeserver' "$LOG_MISSING" || \ grep -Fq -- '-q set system.ntp=timeserver' "$LOG_MISSING" || \
fail 'missing NTP section must be created defensively' fail 'missing NTP section must be created defensively'
echo 'PASS: SmartSafeHub system defaults preserve OpenWrt compatibility metadata' echo 'PASS: SmartSafeHub defaults preserve OpenWrt metadata and existing user system settings'
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Status Pages 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-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
PKG_RELEASE:=22 PKG_RELEASE:=23
PKG_BUILD_DEPENDS:=iwinfo PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
@@ -96,6 +96,13 @@ return baseclass.extend({
return value != null ? [Number(value), String(value)] : [-1, '-']; return value != null ? [Number(value), String(value)] : [-1, '-'];
} }
if (direction == 'total') { if (direction == 'total') {
const traffic = data?.traffic?.[mac?.toUpperCase()];
if (traffic?.upload != null && traffic?.download != null) {
const upload = Number(traffic.upload), download = Number(traffic.download);
return [upload + download, upload || download
? '%1024.2mB'.format(upload) + ' / ' + '%1024.2mB'.format(download)
: '%1024.2mB'.format(0)];
}
const value = data?.totals?.[mac?.toUpperCase()]; const value = data?.totals?.[mac?.toUpperCase()];
return value != null ? [Number(value), '%1024.2mB'.format(value)] : [-1, '-']; return value != null ? [Number(value), '%1024.2mB'.format(value)] : [-1, '-'];
} }
@@ -422,7 +429,7 @@ return baseclass.extend({
E('th', { 'class': 'th' }, _('MAC address')), E('th', { 'class': 'th' }, _('MAC address')),
E('th', { 'class': 'th' }, _('Upload')), E('th', { 'class': 'th' }, _('Upload')),
E('th', { 'class': 'th' }, _('Download')), E('th', { 'class': 'th' }, _('Download')),
E('th', { 'class': 'th', 'data-total-traffic': '1' }, _('Total traffic')), E('th', { 'class': 'th', 'data-total-traffic': '1' }, _('Total traffic') + ' (↑ / ↓)'),
E('th', { 'class': 'th' }, _('Connection count')), E('th', { 'class': 'th' }, _('Connection count')),
isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, _('Static Lease')) isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, _('Static Lease'))
]) ])
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=rpcd-mod-luci PKG_NAME:=rpcd-mod-luci
PKG_VERSION:=20240305 PKG_VERSION:=20240305
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io> PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
+13
View File
@@ -747,6 +747,19 @@ static int get_rates(struct ubus_context *ctx, struct ubus_object *obj,
} }
blobmsg_close_table(&result, totals); blobmsg_close_table(&result, totals);
void *traffic = blobmsg_open_table(&result, "traffic");
avl_for_each_element(&hosts, host, avl) {
if (!host->available && !sampled) continue;
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
host->mac[0], host->mac[1], host->mac[2], host->mac[3], host->mac[4], host->mac[5]);
void *entry = blobmsg_open_table(&result, mac);
blobmsg_add_u64(&result, "upload", host->total[0]);
blobmsg_add_u64(&result, "download", host->total[1]);
blobmsg_close_table(&result, entry);
}
blobmsg_close_table(&result, traffic);
void *connections = blobmsg_open_table(&result, "connections"); void *connections = blobmsg_open_table(&result, "connections");
if (sampled && !failure && now - sampled < 3 * SAMPLE_MS) { if (sampled && !failure && now - sampled < 3 * SAMPLE_MS) {
avl_for_each_element(&hosts, host, avl) { avl_for_each_element(&hosts, host, avl) {
+3 -1
View File
@@ -2,7 +2,9 @@
`luci.client-rates get` accepts up to 1024 IP addresses and returns cached upload `luci.client-rates get` accepts up to 1024 IP addresses and returns cached upload
and download bytes/second for each address, plus cumulative upload + download and download bytes/second for each address, plus cumulative upload + download
bytes keyed by MAC in `totals`. It reads `NETLINK_NETFILTER` connection bytes keyed by MAC in `totals`. The `traffic` table also exposes cumulative
`upload` and `download` bytes per MAC, each combining IPv4 and IPv6. The homepage
displays upload / download and sorts by their sum. It reads `NETLINK_NETFILTER` connection
counters; it never installs packet-filter rules or resets kernel counters. counters; it never installs packet-filter rules or resets kernel counters.
On QSDK, ECM's NSS/SFE/PPE statistics synchronization updates these conntrack On QSDK, ECM's NSS/SFE/PPE statistics synchronization updates these conntrack
+7
View File
@@ -33,6 +33,13 @@ assert.equal(view.rateValue(view.clientAddresses(lease,hints),totals,'total',mac
assert.equal(view.rateValue(['192.168.0.2'],totals,'total',mac)[0],123456); assert.equal(view.rateValue(['192.168.0.2'],totals,'total',mac)[0],123456);
assert.equal(view.rateValue([],totals,'total',mac.toLowerCase())[0],123456); assert.equal(view.rateValue([],totals,'total',mac.toLowerCase())[0],123456);
assert.equal(view.rateValue([],{},'total',mac)[1],'-'); assert.equal(view.rateValue([],{},'total',mac)[1],'-');
const split = {traffic:{[mac]:{upload:1200,download:12200}}};
assert.deepEqual(view.rateValue([], split, 'total', mac.toLowerCase()),
[13400, '%1024.2mB'.format(1200) + ' / ' + '%1024.2mB'.format(12200)]);
assert.equal(view.rateValue([], {traffic:{[mac]:{upload:0,download:0}}}, 'total', mac)[0], 0);
assert.equal(view.rateValue([], {traffic:{[mac]:{upload:0,download:50}}}, 'total', mac)[1],
'%1024.2mB'.format(0) + ' / ' + '%1024.2mB'.format(50));
assert.equal(view.rateValue([], {totals:{[mac]:0}}, 'total',mac)[0],0); assert.equal(view.rateValue([], {totals:{[mac]:0}}, 'total',mac)[0],0);
assert.deepEqual(view.rateValue([], {connections:{[mac]:12}}, 'connections', mac), [12,'12']); assert.deepEqual(view.rateValue([], {connections:{[mac]:12}}, 'connections', mac), [12,'12']);
assert.deepEqual(view.rateValue([], {connections:{[mac]:0}}, 'connections', mac), [0,'0']); assert.deepEqual(view.rateValue([], {connections:{[mac]:0}}, 'connections', mac), [0,'0']);