🌈 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 aea69d1f87
commit 89626fd57d
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
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).
ifdef SOURCE_DATE_EPOCH
@@ -8,9 +8,9 @@
* Pure Map helpers — safe to unit-test on host without LuCI.
*/
return baseclass.extend({
CACHE_MAX: 2000,
CACHE_MAX: 4000,
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. */
lruSet: function (map, key, value, max) {
@@ -293,13 +293,16 @@ function buildEmptyStateNodes(state, callbacks) {
nodes.push(
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(
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;
}
@@ -23,6 +23,10 @@
'require fwlive.render-policy as renderPolicy';
'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({
object: 'fwlive',
method: 'poll',
@@ -162,11 +166,13 @@ return view.extend({
hostnameFailed: null,
resolveInFlight: false,
resolveGeneration: 0,
/* Coalesce hostname-cache paints deferred while tablePaused. */
/* Track hostname paints queued or deferred until a visible forced paint. */
resolvePaintPending: false,
lastPollError: false,
lastPollErrorCode: null,
lastRulesError: null,
rulesRetryAttempt: 0,
nextRulesRetryAt: 0,
followLive: true,
rulesMap: {},
firewallBackend: 'nft',
@@ -216,7 +222,7 @@ return view.extend({
updateHash(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])));
if (this.rowLimit !== constants.DEFAULT_ROW_LIMIT)
parts.push('limit=%s'.format(encodeURIComponent(this.rowLimit)));
@@ -703,7 +709,7 @@ return view.extend({
this.rulesMap = (res && res.rules) || {};
this.firewallBackend = (res && res.backend) || 'nft';
/* 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);
this.refreshBufferedRuleLabels();
} catch (_e) {
@@ -712,11 +718,35 @@ return view.extend({
this.rulesMap = {};
this.firewallBackend = 'nft';
}
this.lastRulesError = 'rules_unavailable';
this.noteRulesMapOutcome('rules_unavailable');
}
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() {
if (this.firewallBackend === 'nft') return _('using fw4');
return '';
@@ -1185,7 +1215,13 @@ return view.extend({
/* A throw here is local UI work after a successful poll. Leave
* 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);
if (epoch !== this.currentPollEpoch() || this.viewDisposed) return;
}
@@ -2082,6 +2118,7 @@ return view.extend({
actionRowTintClass: (action) => this.actionRowTintClass(action)
}
);
if (force && !this.isTabHidden()) this.resolvePaintPending = false;
if (scroll) {
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 ""
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"
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"
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 ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
@@ -19,11 +19,11 @@ msgstr ""
msgid "%s: %s"
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"
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)"
msgstr ""
@@ -32,11 +32,11 @@ msgstr ""
msgid "Action"
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."
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."
msgstr ""
@@ -44,32 +44,32 @@ msgstr ""
msgid "Allow/deny rules, LAN logging, or anything else."
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"
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:894
#: 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:924
msgid "Another change is staged for the firewall; apply or revert it first."
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:910
#: 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:940
msgid ""
"Another change overwrote WAN logging after it was saved; check the current "
"state."
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"
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"
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:1434
#: 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:1470
msgid "Auto"
msgstr ""
@@ -77,7 +77,7 @@ msgstr ""
msgid "Before you enable logging"
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."
msgstr ""
@@ -85,11 +85,11 @@ msgstr ""
msgid "Changes:"
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)"
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"
msgstr ""
@@ -97,13 +97,13 @@ msgstr ""
msgid "Clear all"
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 ""
"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"
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 ""
"Click a row (Time or other non-link cells) to see the full log line (Simple "
"view)."
@@ -113,61 +113,61 @@ msgstr ""
msgid "Click a row for the full message"
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 ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
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"
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…"
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:899
#: 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:929
msgid "Could not acquire the logging lock."
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:905
#: 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:935
msgid "Could not clear the WAN zone log option."
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."
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."
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."
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."
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:907
#: 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:937
msgid "Could not save the firewall configuration."
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."
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:903
#: 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:933
msgid "Could not write the WAN zone log option."
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."
msgstr ""
@@ -175,11 +175,11 @@ msgstr ""
msgid "DPort"
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"
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)."
msgstr ""
@@ -188,7 +188,7 @@ msgstr ""
msgid "Destination"
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)"
msgstr ""
@@ -196,11 +196,11 @@ msgstr ""
msgid "Destination port"
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)"
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"
msgstr ""
@@ -212,15 +212,15 @@ msgstr ""
msgid "Disabling…"
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"
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"
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."
msgstr ""
@@ -244,7 +244,7 @@ msgstr ""
msgid "Enable logging"
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 ""
"Enable logging turns on WAN zone drop/reject logging only (same as Network → "
"Firewall). It does not add rules or log normal LAN browsing."
@@ -255,7 +255,7 @@ msgstr ""
msgid "Enabling…"
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"
msgstr ""
@@ -263,11 +263,11 @@ msgstr ""
msgid "Exclude instead"
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"
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 ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
@@ -283,7 +283,7 @@ msgstr ""
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
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
msgid "Firewall Live View"
msgstr ""
@@ -296,7 +296,7 @@ msgstr ""
msgid "Flow"
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 ""
"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."
@@ -310,25 +310,25 @@ msgstr ""
msgid "Grant access to firewall live log view"
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"
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"
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 ""
"High event rate — table refresh is throttled to protect the browser. The "
"buffer still updates; refresh will resume automatically."
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."
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"
msgstr ""
@@ -336,14 +336,14 @@ msgstr ""
msgid "IN"
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 ""
"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 "
"data leaves the device)."
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 ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
@@ -357,7 +357,13 @@ msgstr ""
msgid "Include instead"
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."
msgstr ""
@@ -366,12 +372,12 @@ msgstr ""
msgid "Interface"
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)"
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:365
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:368
msgid "I’ll configure this under Network → Firewall"
msgstr ""
@@ -379,59 +385,53 @@ msgstr ""
msgid "Kernel log modules missing"
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
msgid "Len"
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"
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"
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"
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:1435
#: 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:1471
msgid "Manual"
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"
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 ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
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):"
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"
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: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:2535
#: 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:2572
msgid "Message"
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"
msgstr ""
@@ -448,8 +448,8 @@ msgstr ""
msgid "No WAN zone found"
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:897
#: 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:927
msgid "No WAN zone found; cannot toggle logging without one."
msgstr ""
@@ -457,7 +457,7 @@ msgstr ""
msgid "Not now"
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."
msgstr ""
@@ -465,15 +465,23 @@ msgstr ""
msgid "OUT"
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"
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"
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 ""
"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/"
@@ -484,16 +492,16 @@ msgstr ""
msgid "Outbound"
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"
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:2482
#: 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:2519
msgid "Pause"
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"
msgstr ""
@@ -502,11 +510,11 @@ msgstr ""
msgid "Proto"
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"
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"
msgstr ""
@@ -514,22 +522,22 @@ msgstr ""
msgid "Remove filter"
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"
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"
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 ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either "
"way."
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 ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -539,15 +547,15 @@ msgstr ""
msgid "Rule"
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"
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"
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"
msgstr ""
@@ -559,40 +567,40 @@ msgstr ""
msgid "Search"
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."
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."
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."
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)."
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"
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"
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:2581
#: 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:2618
msgid "Show rows"
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."
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"
msgstr ""
@@ -601,7 +609,7 @@ msgstr ""
msgid "Source"
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)"
msgstr ""
@@ -609,61 +617,61 @@ msgstr ""
msgid "Source port"
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)"
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."
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"
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."
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 ""
"Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work."
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:901
#: 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:931
msgid "The firewall did not reload; saved and live logging may differ."
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 ""
"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 "
"impose this cap."
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 ""
"The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior."
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 ""
"The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast."
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"
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."
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 ""
"This router reported a logging blocker that Live View does not recognize yet."
msgstr ""
@@ -672,15 +680,15 @@ msgstr ""
msgid "Time"
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"
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"
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"
msgstr ""
@@ -688,7 +696,7 @@ msgstr ""
msgid "Turn it back off with the WAN logging control on the watch strip."
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 ""
"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 "
@@ -703,32 +711,32 @@ msgstr ""
msgid "Undo:"
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)."
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:2493
#: 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:2530
msgid "View"
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."
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 ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
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 ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
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."
msgstr ""
@@ -741,7 +749,7 @@ msgid "WAN logging on (%s). Click to disable."
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:307
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:310
msgid "WAN logging unavailable"
msgstr ""
@@ -753,46 +761,46 @@ msgstr ""
msgid "WAN logging unavailable: no WAN zone"
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"
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:2457
#: 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:2494
msgid "Watching"
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"
msgstr ""
#: 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"
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"
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"
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
msgid "contains"
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
msgid "does not contain"
msgstr ""
#: 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"
msgstr ""
@@ -800,16 +808,16 @@ msgstr ""
msgid "is"
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"
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…"
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:1460
#: 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:1496
msgid "none"
msgstr ""
@@ -817,98 +825,98 @@ msgstr ""
msgid "not"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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"
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…"
msgstr ""
#: 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"
msgstr ""
#: 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"
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)"
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)"
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"
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"
msgstr ""
#: 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"
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"
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_VERSION:=26.9.27
PKG_RELEASE:=299
PKG_RELEASE:=300
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -1779,6 +1779,9 @@ local function curl(url, file, ua, mode)
ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or 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
if mode == "direct" then
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", "")
end
luci.sys.call('rm -f "%s"' % cookie_file)
return return_code, http_code, header_str
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
### 개선
+2 -15
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-smartsafehub
PKG_VERSION:=0.2.24
PKG_RELEASE:=4
PKG_VERSION:=0.2.25
PKG_RELEASE:=5
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
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
exit 1
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
exit 0
endef
@@ -54,10 +45,6 @@ define Package/luci-app-smartsafehub/postinst
if [ -z "$${IPKG_INSTROOT}" ]; then
mkdir -p /tmp/smartsafehub
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
if [ -x /etc/init.d/smartsafehub-events ]; then
/etc/init.d/smartsafehub-events enable
+1 -1
View File
@@ -32,7 +32,7 @@ SmartSafeHub는 OpenWrt 공유기를 위한 통합 홈 게이트웨이 관리 UI
- **업데이트 관리**: SmartSafeHub 관리 소프트웨어와 펌웨어 업데이트를 확인하고 적용합니다.
- **장치 진단**: 로컬 진단, 진단 정보 다운로드와 사용자가 동의한 원격 Health Reporter를 제공합니다.
- **관리자 보안**: 최초 관리자 비밀번호 설정과 현재 비밀번호 확인을 거친 비밀번호 변경, 변경 후 재로그인을 제공합니다.
- **최근 활동**: 인터넷, 보호, 업데이트, 라이선스와 진단 상태 변화를 로컬 이벤트 타임라인으로 정규화합니다.
- **최근 활동**: 인터넷, 보호, 업데이트, 라이선스와 진단 상태 변화를 로컬 이벤트 타임라인으로 정규화하며, Cloud 활동 기록은 사용자가 명시적으로 켠 경우에만 전송합니다.
- **IPTV (Beta)**: SK Broadband와 LG U+ 환경을 위한 IGMP Proxy/Snooping 구성을 제공합니다.
기능별 동작과 현재 제약은 [docs/FEATURES.md](docs/FEATURES.md)에서 확인할 수 있습니다.
@@ -88,6 +88,7 @@ luci-app-smartsafehub/
│ ├── usr/libexec/smartsafehub-health
│ ├── usr/libexec/smartsafehub-license
│ ├── usr/libexec/smartsafehub-maintenance
│ ├── lib/upgrade/keep.d/smartsafehub
│ ├── usr/libexec/smartsafehub-backup
│ ├── usr/share/luci/menu.d/
│ ├── usr/share/rpcd/acl.d/
+4 -4
View File
@@ -187,8 +187,8 @@ README에는 제품을 빠르게 파악하는 데 필요한 핵심 기능만 유
- 진단 파일에 Wi-Fi 비밀번호와 SafeShield 라이선스 키를 포함하지 않음
- 진단 파일에는 호스트명, WAN IPv4와 Wi-Fi SSID가 포함될 수 있으므로 외부 전달 전 확인 필요
- OpenWrt 표준 `sysupgrade` 설정 백업을 SmartSafeHub에서 직접 다운로드하고, SmartSafeHub 또는 기본 LuCI에서 만든 `.tar.gz` 백업을 업로드·검증한 뒤 복원 가능
- 복원 archive는 16MB로 제한하고 gzip/tar 구조, `/etc/config` 포함 여부와 위험한 경로를 검사하며, 업데이트나 펌웨어 작업 중에는 복원을 차단
- 설정 복원 후 현재 펌웨어 이미지의 `firmware.json`을 기준으로 `current_build_id`를 다시 동기화하고 자동 재부팅해 이전 백업의 펌웨어 identity가 남지 않도록 처리
- 복원 archive는 16MB로 제한하고 gzip/tar 구조, `/etc/config` 포함 여부와 위험한 경로를 검사하며, 백업에 포함된 기존 `/usr/share/smartsafehub/firmware.json`의 `device_code`가 현재 장치와 일치할 때만 복원을 허용
- 백업의 `firmware.json`은 장치 식별에만 사용하고 복원 후에는 현재 설치된 펌웨어의 metadata를 다시 고정한 뒤 `current_build_id`를 동기화하므로 오래된 백업이 현재 firmware identity를 되돌리지 않음
- 설정 백업에는 Wi-Fi 비밀번호, 관리자 설정, VPN 키와 라이선스 정보 등 민감한 설정이 포함될 수 있으므로 안전한 위치에 보관해야 하며, 펌웨어 이미지와 설치 패키지 자체는 포함하지 않음
- 데스크톱의 시스템 관리 영역은 `관리자 비밀번호`와 `설정 백업 및 복원`을 1:1 두 열로 배치하고 `시스템 도구`는 그 아래 전체 폭으로 표시합니다. 모바일에서는 모든 카드를 한 열로 쌓습니다.
- 백업/복원 카드는 절반 폭에서도 읽기 쉽도록 `현재 설정 백업`과 `설정 복원`을 compact 세로 섹션으로 표시
@@ -202,7 +202,7 @@ README에는 제품을 빠르게 파악하는 데 필요한 핵심 기능만 유
설정 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가 필요할 때 디렉터리를 다시 생성합니다.
@@ -247,7 +247,7 @@ Health observer의 첫 주기는 WAN/Health 현재 상태를 baseline으로만
공유기 웹사이트는 기존 로그인 세션과 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`를 별도로 호출하지 않습니다.
+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 서버의 `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 상태를 복원합니다.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "luci-app-smartsafehub-frontend",
"version": "0.2.24",
"version": "0.2.25",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "luci-app-smartsafehub-frontend",
"version": "0.2.24",
"version": "0.2.25",
"dependencies": {
"chart.js": "4.5.1",
"preact": "10.29.8"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "luci-app-smartsafehub-frontend",
"private": true,
"version": "0.2.24",
"version": "0.2.25",
"type": "module",
"engines": {
"node": ">=24.0"
@@ -85,9 +85,9 @@ export async function fetchActivityHistory(): Promise<ActivityHistory> {
...activity,
cloud: {
...activity.cloud,
// Pre-r19 RPC responses have no explicit toggle and always used Cloud
// sync when eligible. Preserve that behavior during rolling upgrades.
enabled: activity.cloud.enabled ?? true,
// Cloud activity upload is explicit opt-in. Older or partial RPC
// responses without the toggle must render as OFF rather than opt in.
enabled: activity.cloud.enabled ?? false,
},
};
}
+1 -1
View File
@@ -44,7 +44,7 @@ function installBootstrap(sessionId: string, host: HTMLElement): void {
sessionId,
rpcUrl: luciUrl('/admin/ubus'),
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',
});
}
@@ -912,16 +912,18 @@ function AdministratorPasswordCard(props: { onChanged: () => void }) {
<PasswordRequirement label="숫자 포함" met={policy.number} />
</ul>
<PasswordInputField
autoComplete="new-password"
disabled={submitting}
id="smartsafehub-confirm-admin-password"
label="새 비밀번호 확인"
name="confirm-password"
onInput={setConfirmation}
placeholder="새 관리자 비밀번호 다시 입력"
value={confirmation}
/>
<div class="pt-2">
<PasswordInputField
autoComplete="new-password"
disabled={submitting}
id="smartsafehub-confirm-admin-password"
label="새 비밀번호 확인"
name="confirm-password"
onInput={setConfirmation}
placeholder="새 관리자 비밀번호 다시 입력"
value={confirmation}
/>
</div>
{confirmation.length > 0 && (
<p
@@ -943,9 +945,11 @@ function AdministratorPasswordCard(props: { onChanged: () => void }) {
</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 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
# SPDX-License-Identifier: GPL-3.0-or-later
# Keep OpenWrt-generated system metadata such as compat_version intact. Only
# apply SmartSafeHub product defaults to individual UCI options instead of
# replacing /etc/config/system from a common firmware overlay.
# Preserve OpenWrt-generated metadata (including compat_version) and user
# changes across firmware upgrades. SmartSafeHub only replaces values that are
# 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
uci -q add system system >/dev/null || exit 1
fi
uci -q set system.@system[0].hostname='SmartRouter' || exit 1
uci -q set system.@system[0].zonename='Asia/Seoul' || exit 1
uci -q set system.@system[0].timezone='UTC+9' || exit 1
uci -q set system.@system[0].ttylogin='0' || exit 1
uci -q set system.@system[0].log_size='64' || exit 1
uci -q set system.@system[0].urandom_seed='0' || exit 1
hostname="$(uci_get system.@system[0].hostname)"
case "$hostname" in
''|OpenWrt)
uci -q set system.@system[0].hostname='SmartRouter' || 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
uci -q set system.ntp='timeserver' || exit 1
fi
uci -q set system.ntp.enabled='1' || exit 1
uci -q set system.ntp.enable_server='0' || exit 1
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
uci -q add_list system.ntp.server='1.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
set_if_missing system.ntp.enabled '1'
set_if_missing system.ntp.enable_server '0'
if [ -z "$(uci_get system.ntp.server)" ]; then
uci -q add_list system.ntp.server='0.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='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
exit 0
@@ -0,0 +1 @@
/usr/share/smartsafehub/firmware.json
@@ -54,9 +54,8 @@ cloud_sync_enabled() {
local value
value="$(uci_get cloud_sync_enabled)"
case "$value" in
0|false|off|no) return 1 ;;
1|true|on|yes|'') return 0 ;;
*) return 0 ;;
1|true|on|yes) return 0 ;;
*) return 1 ;;
esac
}
@@ -12,11 +12,14 @@ REBOOT_DELAY_S="${SMARTSAFEHUB_BACKUP_REBOOT_DELAY_S:-2}"
UCI_BIN="${SMARTSAFEHUB_BACKUP_UCI_BIN:-/sbin/uci}"
JSONFILTER_BIN="${SMARTSAFEHUB_BACKUP_JSONFILTER_BIN:-/usr/bin/jsonfilter}"
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}"
FIRMWARE_STATE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_STATE_FILE:-$RUNTIME_DIR/firmware.state}"
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_IMAGE_FILE="${SMARTSAFEHUB_BACKUP_FIRMWARE_IMAGE_FILE:-$RUNTIME_DIR/firmware.bin}"
BACKUP_DEVICE_MISMATCH=65
BACKUP_DEVICE_UNVERIFIED=66
numeric_or_zero() {
case "${1:-}" in
@@ -31,6 +34,28 @@ file_size() {
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() {
local list_file="$1" entry normalized has_config=0
@@ -50,8 +75,54 @@ archive_paths_are_safe() {
[ "$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() {
local size list_file
local size list_file device_status=0
[ -f "$BACKUP_FILE" ] && [ ! -L "$BACKUP_FILE" ] || return 1
size="$(file_size "$BACKUP_FILE")"
@@ -68,7 +139,9 @@ validate_backup() {
return 1
fi
validate_backup_device "$list_file" || device_status=$?
rm -f "$list_file"
[ "$device_status" -eq 0 ] || return "$device_status"
return 0
}
@@ -121,7 +194,7 @@ sync_firmware_identity() {
[ -x "$JSONFILTER_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
'' | *[!A-Za-z0-9._~:+-]*) return 0 ;;
esac
@@ -130,17 +203,54 @@ sync_firmware_identity() {
"$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() {
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
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
}
if ! "$SYSUPGRADE_BIN" --restore-backup "$BACKUP_FILE" >/dev/null 2>&1; then
restore_status=1
fi
# An older backup may contain the previous firmware build ID. Keep the
# restored user configuration, but re-pin the runtime firmware identity to
# the image that is actually installed before rebooting.
# Even a failed restore may already have written some files. Re-pin
# firmware.json before returning so the running image identity stays true.
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
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)"
fi
case "$value" in
0|false|off|no) return 1 ;;
1|true|on|yes|'') return 0 ;;
*) return 0 ;;
1|true|on|yes) return 0 ;;
*) return 1 ;;
esac
}
@@ -69,9 +69,8 @@ activity_cloud_sync_enabled() {
local value
value="$("$UCI_BIN" -q get smartsafehub.activity.cloud_sync_enabled 2>/dev/null || true)"
case "$value" in
0|false|off|no) return 1 ;;
1|true|on|yes|'') return 0 ;;
*) return 0 ;;
1|true|on|yes) return 0 ;;
*) return 1 ;;
esac
}
@@ -98,13 +98,12 @@ function bool_config(value, fallback) {
function cloud_sync_enabled() {
const ctx = new_uci_cursor();
if (ctx == null) {
return true;
return false;
}
// Releases before r19 had no explicit toggle and always uploaded when paid.
// Treat a missing option as enabled for upgrade compatibility, while fresh
// r19 installations ship cloud_sync_enabled=0.
return bool_config(ctx.get('smartsafehub', 'activity', 'cloud_sync_enabled'), true);
// Cloud activity upload is explicit opt-in. Missing or malformed values
// must fail closed so local activity never starts uploading unexpectedly.
return bool_config(ctx.get('smartsafehub', 'activity', 'cloud_sync_enabled'), false);
}
function license_cloud_entitlement() {
@@ -13,6 +13,8 @@ import {
const BACKUP_FILE = '/tmp/smartsafehub/config-backup.tar.gz';
const BACKUP_HELPER = '/usr/libexec/smartsafehub-backup';
const MAX_BACKUP_BYTES = 16777216;
const BACKUP_DEVICE_MISMATCH = 65;
const BACKUP_DEVICE_UNVERIFIED = 66;
function safe_upload_filename(value) {
const filename = string_value(value, 'backup.tar.gz');
@@ -21,6 +23,35 @@ function safe_upload_filename(value) {
: '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) {
const stat = fs.stat(BACKUP_FILE);
if (stat == null || stat?.size <= 0) {
@@ -31,12 +62,10 @@ export function validate_uploaded_backup(request) {
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);
return failure(
'SYSTEM_BACKUP_INVALID',
'올바른 OpenWrt 설정 백업 파일이 아닙니다. 같은 장치에서 생성한 백업인지 확인해 주세요.'
);
return backup_validation_failure(status);
}
return success({
@@ -51,24 +80,20 @@ export function restore_uploaded_backup(request) {
return failure('SYSTEM_BACKUP_CONFIRM_REQUIRED', '설정 복원을 다시 확인해 주세요.');
}
let status;
try {
status = system([ BACKUP_HELPER, 'restore' ], 30000);
}
catch (e) {
status = -1;
}
const status = run_backup_helper('restore', 30000);
if (status == 75) {
return failure(
'SYSTEM_BACKUP_BUSY',
'업데이트 또는 펌웨어 작업이 진행 중입니다. 작업이 끝난 뒤 다시 복원해 주세요.'
);
}
if (status == BACKUP_DEVICE_MISMATCH || status == BACKUP_DEVICE_UNVERIFIED) {
return backup_validation_failure(status);
}
if (status != 0) {
return failure(
'SYSTEM_BACKUP_RESTORE_FAILED',
'설정 백업을 복원하지 못했습니다. 백업 파일과 장치 호환성을 확인해 주세요.'
'설정 백업을 복원하지 못했습니다. 백업 파일이 손상되지 않았는지 확인해 주세요.'
);
}
@@ -10,16 +10,16 @@
<meta name="robots" content="noindex,nofollow,noarchive">
<meta name="referrer" content="same-origin">
<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>
<body style="margin:0;background:#f8fafc;min-height:100vh;">
<div
id="smartsafehub-entry-root"
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;"
></div>
<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>
</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(`
`):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"
SAFE_ADAPTER="$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/safeshield-management.uc"
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"
INIT_SYNC="$ROOT_DIR/root/etc/init.d/smartsafehub-activity-sync"
TMP_DIR="$(mktemp -d)"
@@ -18,7 +19,7 @@ fail() {
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"
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 '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 'activity-cloud-sync-upgrade-enable' "$ROOT_DIR/Makefile" || fail 'package upgrade must preserve pre-r19 implicit Cloud sync'
grep -Fq "smartsafehub.activity.cloud_sync_enabled='1'" "$ROOT_DIR/Makefile" || fail 'postinst must restore Cloud sync for pre-r19 upgrades'
if grep -Fq 'activity-cloud-sync-upgrade-enable' "$ROOT_DIR/Makefile"; then
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 "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
fail 'activity sync must not duplicate license API calls; smartsafehub-license owns license status and activity credential acquisition'
fi
@@ -203,8 +203,8 @@ grep -Fq 'activityHistory?: ActivityHistory;' "$API" || \
fail 'frontend must tolerate a pre-reload status response without activityHistory'
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'
grep -Fq 'enabled: activity.cloud.enabled ?? true' "$API" || \
fail 'pre-r19 activity responses must retain the legacy always-enabled Cloud behavior during rolling upgrades'
grep -Fq 'enabled: activity.cloud.enabled ?? false' "$API" || \
fail 'activity responses without an explicit Cloud toggle must fail closed to OFF'
grep -Fq 'const ACTIVITY_REFRESH_INTERVAL_MS = 60_000;' "$HOOK" || \
fail 'recent activity may refresh at a lightweight one-minute interval'
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'
grep -Fq 'autoComplete="new-password"' "$SETTINGS_PAGE" || \
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" || \
fail 'administrator password card must call the dedicated frontend API'
grep -Fq 'await logoutLuciSession();' "$SETTINGS_PAGE" || \
@@ -4,6 +4,7 @@ set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
HELPER="$ROOT_DIR/root/usr/libexec/smartsafehub-backup"
KEEP_FILE="$ROOT_DIR/root/lib/upgrade/keep.d/smartsafehub"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
@@ -14,11 +15,24 @@ fail() {
make_backup() {
archive="$1"
device_code="${2:-iptime-ax3000sm}"
build_id="${3:-20260901T010203Z-oldbuild}"
root="$TMP_DIR/archive"
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 '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
}
@@ -44,7 +58,7 @@ SYSUPGRADE_BIN="$TMP_DIR/sysupgrade"
REBOOT_BIN="$TMP_DIR/reboot"
UCI_BIN="$TMP_DIR/uci"
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"
FIRMWARE_STATE_FILE="$TMP_DIR/firmware.state"
UPDATE_LOCK_DIR="$TMP_DIR/updater.lock"
@@ -53,10 +67,19 @@ FIRMWARE_IMAGE_FILE="$TMP_DIR/firmware.bin"
SYSUPGRADE_LOG="$TMP_DIR/sysupgrade.log"
REBOOT_MARKER="$TMP_DIR/rebooted"
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
#!/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
EOF_SYSUPGRADE
cat > "$REBOOT_BIN" <<EOF_REBOOT
@@ -65,43 +88,80 @@ touch "$REBOOT_MARKER"
EOF_REBOOT
cat > "$UCI_BIN" <<EOF_UCI
#!/bin/sh
printf '%s\\n' "\$*" >> "$UCI_LOG"
printf '%s\n' "\$*" >> "$UCI_LOG"
exit 0
EOF_UCI
cat > "$JSONFILTER_BIN" <<'EOF_JSONFILTER'
#!/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
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"
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"
if run_helper validate >/dev/null 2>&1; then
fail 'non-gzip input must be rejected'
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"
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
fail 'an archive without /etc/config content must be rejected'
fi
mkdir -p "$TMP_DIR/empty-config/etc/config"
tar -czf "$BACKUP_FILE" -C "$TMP_DIR/empty-config" etc
mkdir -p "$TMP_DIR/empty-config/etc/config" "$TMP_DIR/empty-config/usr/share/smartsafehub"
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
fail 'an archive with only an empty /etc/config directory must be rejected'
fi
make_backup "$BACKUP_FILE"
printf '{"build_id":"20260915T010203Z-abcd1234"}\n' > "$FIRMWARE_METADATA_FILE"
run_helper restore || fail 'validated backup must restore successfully'
make_backup "$BACKUP_FILE" 'iptime-ax3000sm' '20260901T010203Z-oldbuild'
run_helper restore || fail 'validated same-device backup must restore successfully'
grep -Fxq -- "--restore-backup $BACKUP_FILE" "$SYSUPGRADE_LOG" || \
fail 'restore must delegate to sysupgrade --restore-backup'
[ ! -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'
grep -Fq 'commit smartsafehub' "$UCI_LOG" || \
fail 'firmware build identity re-sync must be committed'
@@ -113,7 +173,7 @@ done
[ -e "$REBOOT_MARKER" ] || fail 'successful restore must schedule a reboot'
: > "$SYSUPGRADE_LOG"
rm -f "$REBOOT_MARKER" "$FIRMWARE_METADATA_FILE"
rm -f "$REBOOT_MARKER"
make_backup "$BACKUP_FILE"
touch "$FIRMWARE_IMAGE_FILE"
set +e
@@ -125,8 +185,11 @@ set -e
[ -e "$BACKUP_FILE" ] || fail 'busy restore must keep the validated archive for retry'
rm -f "$FIRMWARE_IMAGE_FILE"
cat > "$SYSUPGRADE_BIN" <<'EOF_SYSUPGRADE_FAIL'
cat > "$SYSUPGRADE_BIN" <<EOF_SYSUPGRADE_FAIL
#!/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
EOF_SYSUPGRADE_FAIL
chmod +x "$SYSUPGRADE_BIN"
@@ -136,6 +199,8 @@ status=$?
set -e
[ "$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'
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'
run_helper discard || fail 'discard command must succeed'
@@ -144,4 +209,4 @@ run_helper discard || fail 'discard command must succeed'
grep -Fq '/* | .. | ../* | */.. | */../*' "$HELPER" || \
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-license"
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_executable "$ROOT_DIR/root/etc/uci-defaults/90-smartsafehub-system-defaults"
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'
grep -Fq 'restore_is_busy && return 75' "$BACKUP_HELPER" || \
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" || \
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'
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" || \
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" || \
fail 'system defaults must keep the existing SmartSafeHub POSIX timezone default'
grep -Fq "uci -q set system.@system[0].log_size='64'" "$DEFAULTS" || \
fail 'system defaults must keep the configured log size'
grep -Fq "set_if_missing system.@system[0].log_size '64'" "$DEFAULTS" || \
fail 'system defaults must only fill a missing log size'
grep -Fq "uci -q set system.ntp='timeserver'" "$DEFAULTS" || \
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" || \
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" || \
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'
#!/bin/sh
printf '%s\n' "$*" >> "$MOCK_UCI_LOG"
mock_value() {
value="$1"
[ "$value" != '__MISSING__' ] || return 1
printf '%s\n' "$value"
}
case "$*" in
'-q get system.@system[0]') exit "${MOCK_SYSTEM_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
exit 0
MOCK
@@ -53,6 +69,15 @@ PATH="$TMP_DIR:$PATH" \
MOCK_UCI_LOG="$LOG_EXISTING" \
MOCK_SYSTEM_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"
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
fail 'existing NTP section must not be recreated'
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
fail 'runtime defaults must not mutate compat_version'
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"
PATH="$TMP_DIR:$PATH" \
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" || \
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_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_LICENSE:=Apache-2.0
@@ -96,6 +96,13 @@ return baseclass.extend({
return value != null ? [Number(value), String(value)] : [-1, '-'];
}
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()];
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' }, _('Upload')),
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')),
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_VERSION:=20240305
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
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);
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");
if (sampled && !failure && now - sampled < 3 * SAMPLE_MS) {
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
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.
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([],totals,'total',mac.toLowerCase())[0],123456);
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.deepEqual(view.rateValue([], {connections:{[mac]:12}}, 'connections', mac), [12,'12']);
assert.deepEqual(view.rateValue([], {connections:{[mac]:0}}, 'connections', mac), [0,'0']);