🤞 Sync 2026-10-17 10:45:37

This commit is contained in:
github-actions[bot] committed 2026-10-17 10:45:37 +08:00
1 parent 1849cc6156
commit 315f273687
107 files changed
+3810 -1215

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all
PKG_VERSION:=0.1.53
PKG_RELEASE:=68
PKG_RELEASE:=69
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -11,5 +11,5 @@
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
*/
return baseclass.extend({
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
});
@@ -361,6 +361,9 @@
font-weight: 500;
}
.fwlive-rule-link:hover { text-decoration: underline; }
.fwlive-rule-target {
opacity: 0.65;
}
.fwlive-iface-badge {
display: inline-block;
padding: 1px 6px;
@@ -195,10 +195,16 @@ function buildColumnCell(col, row, state, callbacks) {
actionChildren.push(' ', rowExpandButton(row, state, callbacks));
return E('td', { 'class': log.actionRowClass(row.action) }, actionChildren);
}
case 'rule':
return E('td', { 'class': columnCellClass(col) }, [
case 'rule': {
const ruleChildren = [
links.ruleAdminLink(row.rule_hint, row.rule_label, onFilterClick)
]);
];
if (typeof row.rule_target === 'string' && row.rule_target) {
ruleChildren.push(' · ');
ruleChildren.push(E('span', { 'class': 'fwlive-rule-target' }, [row.rule_target]));
}
return E('td', { 'class': columnCellClass(col) }, ruleChildren);
}
case 'iface':
return E('td', { 'class': columnCellClass(col) }, [
links.ifaceLink(row.interface, onFilterClick)
@@ -313,6 +319,7 @@ function rowRenderKey(row, state, columns) {
row.action,
row.rule_hint,
row.rule_label,
row.rule_target,
row.interface_in,
row.interface_out,
row.direction,
@@ -207,6 +207,7 @@ return view.extend({
nextRulesRetryAt: 0,
followLive: true,
rulesMap: {},
ruleTargets: {},
firewallBackend: 'nft',
viewMode: 'simple',
expandedRowId: null,
@@ -749,6 +750,10 @@ return view.extend({
const res = await callFwliveRules();
if (!this.isCurrentPollEpoch(epoch)) return;
this.rulesMap = (res && res.rules) || {};
this.ruleTargets =
res && res.targets && typeof res.targets === 'object' && !Array.isArray(res.targets)
? res.targets
: {};
this.rulesMapTruncated = !!(res && res.truncated === true);
this.rulesMapLoaded = true;
this.firewallBackend = (res && res.backend) || 'nft';
@@ -1106,8 +1111,23 @@ return view.extend({
return log.formatRuleLabel(hint);
},
resolveRuleTarget(hint) {
if (!hint) return '';
const own = Object.prototype.hasOwnProperty;
if (own.call(this.ruleTargets, hint) && typeof this.ruleTargets[hint] === 'string')
return this.ruleTargets[hint];
const slug = hint.toLowerCase();
if (own.call(this.ruleTargets, slug) && typeof this.ruleTargets[slug] === 'string')
return this.ruleTargets[slug];
return '';
},
enrichEntry(row) {
row.rule_label = this.resolveRuleLabel(row.rule_hint);
row.rule_target = this.resolveRuleTarget(row.rule_hint);
return row;
},
@@ -1116,10 +1136,15 @@ return view.extend({
for (let i = 0; i < this.entries.length; i++) {
const row = this.entries[i];
const label = this.resolveRuleLabel(row.rule_hint);
const target = this.resolveRuleTarget(row.rule_hint);
if (row.rule_label !== label) {
row.rule_label = label;
changed = true;
}
if (row.rule_target !== target) {
row.rule_target = target;
changed = true;
}
}
if (changed) this.invalidateFilteredRows();
+162 -162
View File
@@ -12,32 +12,32 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791
msgid "%d matching · %d/%d stored"
msgstr "%d Treffer · %d/%d gespeichert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756
msgid "%dd"
msgstr "%dd"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755
msgid "%dh"
msgstr "%dh"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754
msgid "%dm"
msgstr "%dm"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753
msgid "%ds"
msgstr "%ds"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr ""
"%s · bis zu %d Rohzeilen angefordert · %d Firewall-Meldungen zurückgegeben"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
@@ -49,17 +49,17 @@ msgstr ""
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793
msgid "0 matching · %d/%d stored"
msgstr "0 Treffer · %d/%d gespeichert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487
msgid "Accessible (teal/orange)"
msgstr "Barrierefrei (teal/orange)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Action"
msgstr "Aktion"
@@ -70,12 +70,12 @@ msgstr ""
"Aktivieren Sie die Nachrichtenschaltfläche oder klicken Sie auf eine Zeile, "
"um die vollständige Meldung ein- oder auszublenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998
msgid "Administrator access is required to disable logging."
msgstr ""
"Zum Deaktivieren der Protokollierung sind Administratorrechte erforderlich."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982
msgid "Administrator access is required to enable logging."
msgstr ""
"Zum Aktivieren der Protokollierung sind Administratorrechte erforderlich."
@@ -84,7 +84,7 @@ msgstr ""
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "Allow/Deny-Regeln, LAN-Protokollierung oder irgendetwas anderes."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004
msgid "Also seen"
msgstr "Weitere"
@@ -102,16 +102,16 @@ msgstr ""
"Eine andere Änderung hat die WAN-Protokollierung nach dem Speichern "
"überschrieben; prüfen Sie den aktuellen Zustand."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899
msgid "Any action"
msgstr "Beliebige Aktion"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997
msgid "Any protocol"
msgstr "Beliebiges Protokoll"
#: 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:1545
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570
msgid "Auto"
msgstr "Auto"
@@ -129,20 +129,20 @@ msgstr ""
msgid "Changes:"
msgstr "Änderungen:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
msgid "Classic (green/red)"
msgstr "Klassisch (grün/rot)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
msgid "Clear all"
msgstr "Alle löschen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972
msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, activate the message button or click a "
@@ -153,7 +153,7 @@ msgstr ""
"Nachrichtenschaltfläche aktivieren oder auf eine Zeile klicken, um die "
"vollständige Meldung ein- oder auszublenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
@@ -161,7 +161,7 @@ msgstr ""
"Klicken Sie auf eine IP, Aktion oder ein Protokoll, um zu filtern; nutzen "
"Sie das Protokoll-Menü (oder ≠ auf einem Chip) zum Ausschließen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998
msgid "Common"
msgstr "Häufig"
@@ -174,7 +174,7 @@ msgstr ""
"Versionen, die diese Option unterstützen; Kernel/logd können das "
"Gesamtvolumen weiterhin begrenzen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804
msgid "Connection lost — retrying…"
msgstr "Verbindung verloren — neuer Versuch…"
@@ -186,53 +186,53 @@ msgstr "Die Protokollierungssperre konnte nicht erworben werden."
msgid "Could not clear the WAN zone log option."
msgstr "Die WAN-Zonen-Log-Option konnte nicht gelöscht werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804
msgid "Could not create a temporary file for rule names."
msgstr "Für die Regelnamen konnte keine temporäre Datei erstellt werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
msgid "Could not disable logging."
msgstr "Protokollierung konnte nicht deaktiviert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971
msgid "Could not enable logging."
msgstr "Protokollierung konnte nicht aktiviert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907
msgid "Could not load logging status."
msgstr "Protokollstatus konnte nicht geladen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810
msgid "Could not load rule names."
msgstr "Die Regelnamen konnten nicht geladen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806
msgid "Could not process firewall rule names."
msgstr "Die Namen der Firewall-Regeln konnten nicht verarbeitet werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999
msgid "Could not reach the router; check the current logging state."
msgstr ""
"Der Router konnte nicht erreicht werden; prüfen Sie den aktuellen "
"Protokollstatus."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Could not read the active firewall rules."
msgstr "Die aktiven Firewall-Regeln konnten nicht gelesen werden."
#: 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:906
msgid "Could not refresh logging status; showing the last known state."
msgstr ""
"Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt "
"bekannten Zustand."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815
msgid "Could not refresh rule names."
msgstr "Die Regelnamen konnten nicht aktualisiert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr ""
"Die Regelnamen konnten nicht aktualisiert werden; die zuvor geladenen Namen "
@@ -256,7 +256,7 @@ msgstr ""
msgid "Could not write the WAN zone log option."
msgstr "Die WAN-Zonen-Log-Option konnte nicht geschrieben werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr ""
"Eigenes Protokoll (! zum Ausschließen). Überschreibt das Menü, wenn gesetzt."
@@ -270,7 +270,7 @@ msgstr "Ziel-Port"
msgid "Destination"
msgstr "Ziel"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957
msgid "Destination IP contains (! to exclude)"
msgstr "Ziel-IP enthält (! zum Ausschließen)"
@@ -278,15 +278,15 @@ msgstr "Ziel-IP enthält (! zum Ausschließen)"
msgid "Destination port"
msgstr "Ziel-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962
msgid "Destination port (! to exclude)"
msgstr "Ziel-Port (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
msgid "Detail"
msgstr "Detail"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Details"
msgstr "Details"
@@ -298,7 +298,7 @@ msgstr "Richtung"
msgid "Disabling…"
msgstr "Wird deaktiviert…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
msgid ""
"Display limited to %d rows on this device; the larger Limit still buffers "
"rows."
@@ -306,11 +306,11 @@ msgstr ""
"Anzeige auf diesem Gerät auf %d Zeilen begrenzt; das größere Limit puffert "
"weiterhin Zeilen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545
msgid "Display options"
msgstr "Anzeigeoptionen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
msgid ""
"Display options set Limit, Fetch budget, Maximum raw lines, row tint, "
"palette, and hostnames."
@@ -336,7 +336,7 @@ msgstr ""
msgid "Enable logging"
msgstr "Protokollierung aktivieren"
#: 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:2633
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."
@@ -349,20 +349,20 @@ msgstr ""
msgid "Enabling…"
msgstr "Wird aktiviert…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011
msgid "Exclude"
msgstr "Ausschließen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Exclude instead"
msgstr "Stattdessen ausschließen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559
msgid "Fetch budget"
msgstr "Abrufbudget"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
@@ -370,10 +370,10 @@ msgstr ""
"Das Abrufbudget steuert die Rohzeilen pro Abfrage. Auto leitet es aus dem "
"Limit ab; Manuell wählt ein begrenztes Maximum."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Filter by %s"
msgstr "Filtern nach %s"
@@ -383,12 +383,12 @@ msgstr ""
"Logs nach Regel filtern (Hinweis: %s). Strg+Klick öffnet die Firewall-"
"Einstellungen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Firewall-Live-Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr ""
"Im Firewall-Protokoll sind derzeit keine Einträge; angezeigt werden Zeilen, "
@@ -402,7 +402,7 @@ msgstr "Flags"
msgid "Flow"
msgstr "Fluss"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658
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."
@@ -419,7 +419,7 @@ msgstr "Weitergeleitet"
msgid "Grant access to firewall live log view"
msgstr "Zugriff auf die Live-Ansicht der Firewall-Protokolle gewähren"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624
msgid "Help"
msgstr "Hilfe"
@@ -427,15 +427,15 @@ msgstr "Hilfe"
msgid "Hide full message"
msgstr "Vollständige Nachricht ausblenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Hide rows"
msgstr "Zeilen ausblenden"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543
msgid "Hostname lookup paused while the router is under load."
msgstr "Hostnamenauflösung pausiert, solange der Router ausgelastet ist."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -443,7 +443,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "EINGANG"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695
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 "
@@ -461,7 +461,7 @@ msgstr ""
"Wenn das WAN ruhig ist, warten Sie auf Probes oder nutzen Sie den optionalen "
"Ping-Test in der Hilfe / der Anleitung zum Aktivieren der Protokollierung."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679
msgid ""
"In Simple view, activate the message button or click a row to show or hide "
"the full message."
@@ -474,8 +474,8 @@ msgstr ""
msgid "Inbound"
msgstr "Eingehend"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Include instead"
msgstr "Stattdessen einschließen"
@@ -487,7 +487,7 @@ msgstr ""
"Installieren Sie kmod-nf-log und kmod-nf-log6 mit dem Befehl für Ihre "
"OpenWrt-Version und laden Sie dann die Firewall neu."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu."
@@ -496,7 +496,7 @@ msgstr "Installation unvollständig. Installieren Sie luci-app-fwlive neu."
msgid "Interface"
msgstr "Schnittstelle"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942
msgid "Interface (prefix ! to exclude)"
msgstr "Schnittstelle (! voranstellen zum Ausschließen)"
@@ -513,11 +513,11 @@ msgstr "Kernel-Logmodule fehlen"
msgid "Len"
msgstr "Länge"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Limit"
msgstr "Limit"
#: 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:861
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr ""
"Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle "
@@ -527,16 +527,16 @@ msgstr ""
msgid "Logging is off on this router"
msgstr "Protokollierung ist auf diesem Router deaktiviert"
#: 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:1546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
msgid "Manual"
msgstr "Manuell"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577
msgid "Manual still uses server protection and poll cadence"
msgstr "Manuell nutzt weiterhin Serverschutz und Abfrageintervall"
#: 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:2653
msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
@@ -548,18 +548,18 @@ msgstr ""
msgid "Manual test (System → Terminal):"
msgstr "Manueller Test (System → Terminal):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571
msgid "Maximum raw lines"
msgstr "Maximale Rohzeilen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826
msgid "Message"
msgstr "Nachricht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
msgid "More filters"
msgstr "Weitere Filter"
@@ -608,7 +608,7 @@ msgstr "Nichts ändert sich, bis Sie auf „Protokollierung aktivieren“ klicke
msgid "OUT"
msgstr "AUSGANG"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "One line"
msgstr "Einzeilig"
@@ -638,20 +638,20 @@ msgstr ""
msgid "Outbound"
msgstr "Ausgehend"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598
msgid "Palette"
msgstr "Palette"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
msgid "Pause"
msgstr "Pause"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
msgid "Paused"
msgstr "Pausiert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530
msgid ""
"Polling slowed: the router is responding slowly, so polls run less often."
msgstr ""
@@ -663,11 +663,11 @@ msgstr ""
msgid "Proto"
msgstr "Protokoll"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918
msgid "Protocol — common values"
msgstr "Protokoll — häufige Werte"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889
msgid "Quick search"
msgstr "Schnellsuche"
@@ -675,21 +675,21 @@ msgstr "Schnellsuche"
msgid "Rate limit:"
msgstr "Ratenbegrenzung:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
msgid "Remove filter"
msgstr "Filter entfernen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
msgid "Resume"
msgstr "Fortsetzen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588
msgid "Row tint"
msgstr "Zeileneinfärbung"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
msgid ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either "
@@ -699,7 +699,7 @@ msgstr ""
"Klassisch (grün/rot, Standard) oder Barrierefrei (teal/orange). Aktionstext "
"bleibt in beiden Fällen farbig."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -711,19 +711,19 @@ msgstr ""
msgid "Rule"
msgstr "Regel"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Rule name details"
msgstr "Details zu Regelnamen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832
msgid "Rule-name lookup does not change your firewall rules."
msgstr "Das Laden der Regelnamen verändert Ihre Firewall-Regeln nicht."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817
msgid "Rule-name lookup failed."
msgstr "Das Laden der Regelnamen ist fehlgeschlagen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824
msgid "Rule-name lookup reached a safety limit."
msgstr "Beim Laden der Regelnamen wurde eine Schutzgrenze erreicht."
@@ -735,11 +735,11 @@ msgstr "Quell-Port"
msgid "Search"
msgstr "Suche"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527
msgid "Server adaptive protection is disabled."
msgstr "Adaptiver Serverschutz ist deaktiviert."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535
msgid ""
"Server limited fetch: the router is under load; at most %d log lines per "
"poll."
@@ -747,14 +747,14 @@ msgstr ""
"Abruf vom Server begrenzt: Der Router ist ausgelastet; höchstens %d "
"Protokollzeilen pro Abfrage."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid ""
"Server limited fetch: this poll returned fewer lines to protect the router."
msgstr ""
"Abruf vom Server begrenzt: Diese Abfrage lieferte weniger Zeilen, um den "
"Router zu schonen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "Server protection state is unknown."
msgstr "Serverschutz-Status unbekannt."
@@ -762,28 +762,28 @@ msgstr "Serverschutz-Status unbekannt."
msgid "Show full message"
msgstr "Vollständige Nachricht anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615
msgid "Show hostnames"
msgstr "Hostnamen anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586
msgid "Show pass/deny row background colors"
msgstr "Pass/Deny-Zeilenhintergrundfarben anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872
msgid "Show rows"
msgstr "Zeilen anzeigen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605
msgid "Showing a compact summary of the top of the shown sample."
msgstr "Eine kompakte Zusammenfassung des Anfangs der angezeigten Stichprobe."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "Simple"
msgstr "Einfach"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835
msgid ""
"Some entries may show names derived from their log prefixes. Searching by a "
"friendly rule name may miss those entries."
@@ -792,7 +792,7 @@ msgstr ""
"angezeigt. Bei der Suche nach einem lesbaren Regelnamen werden diese "
"Einträge möglicherweise nicht gefunden."
#: 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:855
msgid "Some rule names may be missing"
msgstr "Einige Regelnamen fehlen möglicherweise"
@@ -801,7 +801,7 @@ msgstr "Einige Regelnamen fehlen möglicherweise"
msgid "Source"
msgstr "Quelle"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
msgid "Source IP contains (! to exclude)"
msgstr "Quell-IP enthält (! zum Ausschließen)"
@@ -809,21 +809,21 @@ msgstr "Quell-IP enthält (! zum Ausschließen)"
msgid "Source port"
msgstr "Quell-Port"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952
msgid "Source port (! to exclude)"
msgstr "Quell-Port (! zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
msgid "Summary data is unavailable; show rows to inspect the sample."
msgstr ""
"Zusammenfassungsdaten nicht verfügbar; Zeilen anzeigen, um die Stichprobe zu "
"prüfen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863
msgid "Summary mode"
msgstr "Zusammenfassungsmodus"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
msgid ""
"Summary mode: when the router responds slowly, a compact summary replaces "
"the table. Use Show rows to see the table; it returns automatically after "
@@ -833,13 +833,13 @@ msgstr ""
"kompakte Zusammenfassung die Tabelle. Mit „Zeilen anzeigen“ sehen Sie die "
"Tabelle; sie kehrt nach drei schnellen Abfragen automatisch zurück."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609
msgid "Summary was bounded before all categories could be included."
msgstr ""
"Die Zusammenfassung wurde begrenzt, bevor alle Kategorien aufgenommen werden "
"konnten."
#: 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:2663
msgid ""
"Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work."
@@ -848,7 +848,7 @@ msgstr ""
"eine einmalige Aufholabfrage. Hostnamen sind standardmäßig deaktiviert, da "
"Auflösungen zusätzliche Arbeit verursachen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700
msgid ""
"Table refresh throttled: the event rate is high, so the table repaints less "
"often to protect the browser. The buffer still updates; refresh resumes "
@@ -864,7 +864,7 @@ msgstr ""
"Die Firewall wurde nicht neu geladen; gespeicherte und aktive "
"Protokollierung können voneinander abweichen."
#: 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:2673
msgid ""
"The rate shown for WAN logging is the configured WAN zone log_limit, not "
"proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 "
@@ -876,11 +876,11 @@ msgstr ""
"mit OpenWrt 23.05.5 gelieferte fw4 ignoriert diese Option. Kernel/logd "
"können das Gesamtvolumen weiterhin begrenzen; fwlive setzt kein Limit."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803
msgid "The router could not read the firewall log — retrying…"
msgstr "Der Router konnte das Firewall-Protokoll nicht lesen — neuer Versuch…"
#: 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:2668
msgid ""
"The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior."
@@ -889,7 +889,7 @@ msgstr ""
"fwlive sie liest. fwlive kann verdrängte Einträge nicht wiederherstellen und "
"ändert das Weiterleitungsverhalten nicht."
#: 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:2628
msgid ""
"The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast."
@@ -897,7 +897,7 @@ msgstr ""
"Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr "
"protokolliert. Pause nutzen, wenn es zu schnell wird."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
msgid "Theme tint fallback"
msgstr "Theme-Farb-Fallback"
@@ -916,15 +916,15 @@ msgstr ""
msgid "Time"
msgstr "Zeit"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614
msgid "Top drops"
msgstr "Häufigste Drops"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617
msgid "Top rules"
msgstr "Häufigste Regeln"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611
msgid "Top talkers"
msgstr "Häufigste Absender"
@@ -952,17 +952,17 @@ msgstr ""
msgid "Undo:"
msgstr "Rückgängig:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr ""
"Verwenden Sie „Detail“ für alle Spalten (Flags, Länge, rohe Nachricht)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784
msgid "View"
msgstr "Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
msgid "WAN drop/reject logging is off."
msgstr "WAN-Drop/Reject-Protokollierung ist aus."
@@ -974,7 +974,7 @@ msgstr ""
"WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender WAN-Verkehr "
"erscheint hier. Normales LAN-Surfen nicht."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
@@ -982,7 +982,7 @@ msgstr ""
"WAN-Drop/Reject-Protokollierung ist an. Blockierter eingehender Verkehr "
"sollte hier in Echtzeit erscheinen — nicht normales LAN-Surfen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "WAN logging is already enabled."
msgstr "WAN-Protokollierung ist bereits aktiviert."
@@ -1011,25 +1011,25 @@ msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone"
msgid "Waiting for firewall events"
msgstr "Warte auf Firewall-Ereignisse"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736
msgid "Watching"
msgstr "Beobachtet"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838
msgid "Wrap"
msgstr "Umbrechen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901
msgid "block"
msgstr "blockiert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348
msgid "buffer filling"
msgstr "Puffer füllt sich"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352
msgid "buffer full"
msgstr "Puffer voll"
@@ -1041,7 +1041,7 @@ msgstr "konfiguriertes log_limit: %s"
msgid "contains"
msgstr "enthält"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357
msgid "display limited to %d rows"
msgstr "Anzeige auf %d Zeilen begrenzt"
@@ -1050,11 +1050,11 @@ msgid "does not contain"
msgstr "enthält nicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
msgid "drop"
msgstr "verworfen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361
msgid "hostname lookup paused"
msgstr "Hostnamenauflösung pausiert"
@@ -1062,11 +1062,11 @@ msgstr "Hostnamenauflösung pausiert"
msgid "is"
msgstr "ist"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347
msgid "loading buffer"
msgstr "Lade Puffer"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789
msgid "loading…"
msgstr "lädt…"
@@ -1074,8 +1074,8 @@ msgstr "lädt…"
msgid "no WAN rate limit configured"
msgstr "Kein WAN-Ratenlimit konfiguriert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596
msgid "none"
msgstr "keine"
@@ -1083,98 +1083,98 @@ msgstr "keine"
msgid "not"
msgstr "nicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019
msgid "not AH"
msgstr "nicht AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018
msgid "not ESP"
msgstr "nicht ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017
msgid "not GRE"
msgstr "nicht GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014
msgid "not ICMP"
msgstr "nicht ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015
msgid "not ICMPv6"
msgstr "nicht ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016
msgid "not IGMP"
msgstr "nicht IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020
msgid "not SCTP"
msgstr "nicht SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012
msgid "not TCP"
msgstr "nicht TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013
msgid "not UDP"
msgstr "nicht UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907
msgid "not block"
msgstr "nicht blockieren"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906
msgid "not drop"
msgstr "nicht verwerfen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905
msgid "not pass"
msgstr "nicht erlauben"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908
msgid "not reject"
msgstr "nicht zurückweisen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909
msgid "not unknown"
msgstr "nicht unbekannt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925
msgid "or type…"
msgstr "oder tippen…"
#: 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:2875
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
msgid "pass"
msgstr "erlaubt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359
msgid "polling slowed"
msgstr "Abfragen verlangsamt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903
msgid "reject"
msgstr "abgelehnt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363
msgid "scroll frozen — scroll to top to follow live"
msgstr "Scrollen eingefroren — nach oben scrollen für Live-Ansicht"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360
msgid "server limited fetch"
msgstr "Abruf vom Server begrenzt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354
msgid "table refresh throttled"
msgstr "Tabellenaktualisierung gedrosselt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904
msgid "unknown"
msgstr "unbekannt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "using fw4"
msgstr "mit fw4"
+162 -162
View File
@@ -13,31 +13,31 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791
msgid "%d matching · %d/%d stored"
msgstr "%d совпад. · сохранено %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756
msgid "%dd"
msgstr "%dд"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755
msgid "%dh"
msgstr "%dч"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754
msgid "%dm"
msgstr "%dм"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753
msgid "%ds"
msgstr "%dс"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "%s · запрошено до %d сырых строк · возвращено %d сообщений firewall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
@@ -49,17 +49,17 @@ msgstr ""
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793
msgid "0 matching · %d/%d stored"
msgstr "0 совпад. · сохранено %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487
msgid "Accessible (teal/orange)"
msgstr "Доступный (бирюзовый/оранжевый)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Action"
msgstr "Действие"
@@ -70,11 +70,11 @@ msgstr ""
"Активируйте кнопку сообщения или нажмите на строку, чтобы показать или "
"скрыть полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998
msgid "Administrator access is required to disable logging."
msgstr "Для отключения журналирования требуются права администратора."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982
msgid "Administrator access is required to enable logging."
msgstr "Для включения журналирования требуются права администратора."
@@ -82,7 +82,7 @@ msgstr "Для включения журналирования требуютс
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "Правила allow/deny, журналирование LAN и любые другие настройки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004
msgid "Also seen"
msgstr "Также встречаются"
@@ -100,16 +100,16 @@ msgstr ""
"Другое изменение перезаписало журналирование WAN после сохранения; проверьте "
"текущее состояние."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899
msgid "Any action"
msgstr "Любое действие"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997
msgid "Any protocol"
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:1545
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570
msgid "Auto"
msgstr "Авто"
@@ -126,20 +126,20 @@ msgstr ""
msgid "Changes:"
msgstr "Изменения:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
msgid "Classic (green/red)"
msgstr "Классический (зелёный/красный)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "Классический — зелёный/красный; доступный — бирюзовый/оранжевый"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
msgid "Clear all"
msgstr "Очистить всё"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972
msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, activate the message button or click a "
@@ -149,7 +149,7 @@ msgstr ""
"правилу для настроек МЭ · в Простом виде активируйте кнопку сообщения или "
"нажмите на строку, чтобы показать или скрыть полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
@@ -157,7 +157,7 @@ msgstr ""
"Щёлкните по IP, действию или протоколу для фильтрации; для исключения "
"используйте меню «Протокол» (или ≠ на чипе)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998
msgid "Common"
msgstr "Частые"
@@ -170,7 +170,7 @@ msgstr ""
"fw4 с поддержкой этой опции; ядро/logd всё ещё могут ограничивать общий "
"объём журналов."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804
msgid "Connection lost — retrying…"
msgstr "Соединение потеряно — повторная попытка…"
@@ -182,53 +182,53 @@ msgstr "Не удалось получить блокировку журнали
msgid "Could not clear the WAN zone log option."
msgstr "Не удалось очистить параметр журнала зоны WAN."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804
msgid "Could not create a temporary file for rule names."
msgstr "Не удалось создать временный файл для имён правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
msgid "Could not disable logging."
msgstr "Не удалось отключить журналирование."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971
msgid "Could not enable logging."
msgstr "Не удалось включить журналирование."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907
msgid "Could not load logging status."
msgstr "Не удалось загрузить состояние журналирования."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810
msgid "Could not load rule names."
msgstr "Не удалось загрузить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806
msgid "Could not process firewall rule names."
msgstr "Не удалось обработать имена правил межсетевого экрана."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999
msgid "Could not reach the router; check the current logging state."
msgstr ""
"Не удалось связаться с маршрутизатором; проверьте текущее состояние "
"журналирования."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Could not read the active firewall rules."
msgstr "Не удалось прочитать действующие правила межсетевого экрана."
#: 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:906
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:810
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815
msgid "Could not refresh rule names."
msgstr "Не удалось обновить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr ""
"Не удалось обновить имена правил; отображаются ранее загруженные имена."
@@ -251,7 +251,7 @@ msgstr ""
msgid "Could not write the WAN zone log option."
msgstr "Не удалось записать параметр журнала зоны WAN."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "Свой протокол (! для исключения). Переопределяет меню, если задан."
@@ -264,7 +264,7 @@ msgstr "Порт назн."
msgid "Destination"
msgstr "Назначение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957
msgid "Destination IP contains (! to exclude)"
msgstr "IP назначения содержит (! для исключения)"
@@ -272,15 +272,15 @@ msgstr "IP назначения содержит (! для исключения)
msgid "Destination port"
msgstr "Порт назначения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962
msgid "Destination port (! to exclude)"
msgstr "Порт назначения (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
msgid "Detail"
msgstr "Подробно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Details"
msgstr "Подробности"
@@ -292,7 +292,7 @@ msgstr "Напр."
msgid "Disabling…"
msgstr "Отключение…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
msgid ""
"Display limited to %d rows on this device; the larger Limit still buffers "
"rows."
@@ -300,11 +300,11 @@ msgstr ""
"Отображение ограничено %d строками на этом устройстве; больший лимит по-"
"прежнему буферизует строки."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545
msgid "Display options"
msgstr "Параметры отображения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
msgid ""
"Display options set Limit, Fetch budget, Maximum raw lines, row tint, "
"palette, and hostnames."
@@ -330,7 +330,7 @@ msgstr ""
msgid "Enable logging"
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:2633
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."
@@ -343,20 +343,20 @@ msgstr ""
msgid "Enabling…"
msgstr "Включение…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011
msgid "Exclude"
msgstr "Исключить"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Exclude instead"
msgstr "Исключить вместо этого"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559
msgid "Fetch budget"
msgstr "Бюджет выборки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
@@ -364,10 +364,10 @@ msgstr ""
"Бюджет выборки задаёт число сырых строк журнала за опрос. «Авто» выводится "
"из лимита; «Вручную» задаёт ограниченный максимум."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Filter by %s"
msgstr "Фильтр по %s"
@@ -377,12 +377,12 @@ msgstr ""
"Фильтр журналов по правилу (подсказка: %s). Ctrl+клик открывает настройки "
"файрвола."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "Просмотр файрвола в реальном времени"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr ""
"В журнале межсетевого экрана сейчас нет записей; показаны строки, полученные "
@@ -396,7 +396,7 @@ msgstr "Флаги"
msgid "Flow"
msgstr "Поток"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658
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."
@@ -413,7 +413,7 @@ msgstr "Транзитный"
msgid "Grant access to firewall live log view"
msgstr "Предоставить доступ к живому просмотру журнала межсетевого экрана"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624
msgid "Help"
msgstr "Справка"
@@ -421,15 +421,15 @@ msgstr "Справка"
msgid "Hide full message"
msgstr "Скрыть полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Hide rows"
msgstr "Скрыть строки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543
msgid "Hostname lookup paused while the router is under load."
msgstr "Разрешение имён приостановлено из-за нагрузки на маршрутизатор."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -437,7 +437,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "ВХОД"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695
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 "
@@ -455,7 +455,7 @@ msgstr ""
"Если WAN неактивен, дождитесь проб или используйте необязательную проверку "
"ping в Справке / руководстве по включению журналов."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679
msgid ""
"In Simple view, activate the message button or click a row to show or hide "
"the full message."
@@ -467,8 +467,8 @@ msgstr ""
msgid "Inbound"
msgstr "Входящий"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Include instead"
msgstr "Включить вместо этого"
@@ -480,7 +480,7 @@ msgstr ""
"Установите kmod-nf-log и kmod-nf-log6 командой для вашей версии OpenWrt, "
"затем перезагрузите файрвол."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "Установка не завершена. Переустановите luci-app-fwlive."
@@ -489,7 +489,7 @@ msgstr "Установка не завершена. Переустановите
msgid "Interface"
msgstr "Интерфейс"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942
msgid "Interface (prefix ! to exclude)"
msgstr "Интерфейс (! в начале для исключения)"
@@ -506,11 +506,11 @@ msgstr "Отсутствуют модули журналирования ядр
msgid "Len"
msgstr "Длина"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Limit"
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:861
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr ""
"Живой просмотр может быть неполным — зарегистрирована устаревшая таблица "
@@ -520,16 +520,16 @@ msgstr ""
msgid "Logging is off on this router"
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:1546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
msgid "Manual"
msgstr "Вручную"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577
msgid "Manual still uses server protection and poll cadence"
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:2653
msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
@@ -541,18 +541,18 @@ msgstr ""
msgid "Manual test (System → Terminal):"
msgstr "Ручной тест (Система → Терминал):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571
msgid "Maximum raw lines"
msgstr "Максимум сырых строк"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826
msgid "Message"
msgstr "Сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
msgid "More filters"
msgstr "Больше фильтров"
@@ -599,7 +599,7 @@ msgstr "Ничего не изменится, пока вы не нажмёте
msgid "OUT"
msgstr "ВЫХОД"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "One line"
msgstr "Одна строка"
@@ -629,20 +629,20 @@ msgstr ""
msgid "Outbound"
msgstr "Исходящий"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598
msgid "Palette"
msgstr "Палитра"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
msgid "Pause"
msgstr "Пауза"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
msgid "Paused"
msgstr "Пауза"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530
msgid ""
"Polling slowed: the router is responding slowly, so polls run less often."
msgstr ""
@@ -654,11 +654,11 @@ msgstr ""
msgid "Proto"
msgstr "Протокол"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918
msgid "Protocol — common values"
msgstr "Протокол — частые значения"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889
msgid "Quick search"
msgstr "Быстрый поиск"
@@ -666,21 +666,21 @@ msgstr "Быстрый поиск"
msgid "Rate limit:"
msgstr "Ограничение частоты:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
msgid "Remove filter"
msgstr "Удалить фильтр"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
msgid "Resume"
msgstr "Продолжить"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588
msgid "Row tint"
msgstr "Цвет строк"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
msgid ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either "
@@ -690,7 +690,7 @@ msgstr ""
"классический (зелёный/красный, по умолчанию) или доступный (бирюзовый/"
"оранжевый). Цвет текста действия сохраняется в любом случае."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -702,19 +702,19 @@ msgstr ""
msgid "Rule"
msgstr "Правило"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Rule name details"
msgstr "Подробности об именах правил"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832
msgid "Rule-name lookup does not change your firewall rules."
msgstr "Получение имён правил не изменяет правила межсетевого экрана."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817
msgid "Rule-name lookup failed."
msgstr "Не удалось получить имена правил."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824
msgid "Rule-name lookup reached a safety limit."
msgstr "При загрузке имён правил достигнут защитный лимит."
@@ -726,11 +726,11 @@ msgstr "Порт ист."
msgid "Search"
msgstr "Поиск"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527
msgid "Server adaptive protection is disabled."
msgstr "Адаптивная защита сервера отключена."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535
msgid ""
"Server limited fetch: the router is under load; at most %d log lines per "
"poll."
@@ -738,14 +738,14 @@ msgstr ""
"Сервер ограничил выборку: маршрутизатор нагружен; не более %d строк журнала "
"за опрос."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid ""
"Server limited fetch: this poll returned fewer lines to protect the router."
msgstr ""
"Сервер ограничил выборку: этот опрос вернул меньше строк для защиты "
"маршрутизатора."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "Server protection state is unknown."
msgstr "Состояние защиты сервера неизвестно."
@@ -753,28 +753,28 @@ msgstr "Состояние защиты сервера неизвестно."
msgid "Show full message"
msgstr "Показать полное сообщение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615
msgid "Show hostnames"
msgstr "Показывать имена узлов"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586
msgid "Show pass/deny row background colors"
msgstr "Показывать цвета фона строк pass/deny"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872
msgid "Show rows"
msgstr "Показать строки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605
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:2771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "Simple"
msgstr "Простой"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835
msgid ""
"Some entries may show names derived from their log prefixes. Searching by a "
"friendly rule name may miss those entries."
@@ -782,7 +782,7 @@ 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:855
msgid "Some rule names may be missing"
msgstr "Некоторые имена правил могут отсутствовать"
@@ -791,7 +791,7 @@ msgstr "Некоторые имена правил могут отсутство
msgid "Source"
msgstr "Источник"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
msgid "Source IP contains (! to exclude)"
msgstr "IP источника содержит (! для исключения)"
@@ -799,19 +799,19 @@ msgstr "IP источника содержит (! для исключения)"
msgid "Source port"
msgstr "Порт источника"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952
msgid "Source port (! to exclude)"
msgstr "Порт источника (! для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
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:2838
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863
msgid "Summary mode"
msgstr "Режим сводки"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
msgid ""
"Summary mode: when the router responds slowly, a compact summary replaces "
"the table. Use Show rows to see the table; it returns automatically after "
@@ -821,11 +821,11 @@ msgstr ""
"показывается краткая сводка. Нажмите «Показать строки», чтобы увидеть "
"таблицу; она вернётся автоматически после трёх быстрых опросов."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609
msgid "Summary was bounded before all categories could be included."
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:2663
msgid ""
"Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work."
@@ -834,7 +834,7 @@ msgstr ""
"один догоняющий опрос. Имена хостов по умолчанию отключены, поскольку их "
"разрешение создаёт дополнительную нагрузку."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700
msgid ""
"Table refresh throttled: the event rate is high, so the table repaints less "
"often to protect the browser. The buffer still updates; refresh resumes "
@@ -850,7 +850,7 @@ 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:2673
msgid ""
"The rate shown for WAN logging is the configured WAN zone log_limit, not "
"proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 "
@@ -862,13 +862,13 @@ msgstr ""
"OpenWrt 23.05.5 эта опция игнорируется. Ядро/logd всё ещё могут ограничивать "
"общий объём журналов; fwlive не задаёт ограничение."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803
msgid "The router could not read the firewall log — retrying…"
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:2668
msgid ""
"The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior."
@@ -877,7 +877,7 @@ msgstr ""
"fwlive их прочитает. fwlive не может восстановить вытесненные записи и не "
"изменяет пересылку."
#: 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:2628
msgid ""
"The table updates automatically when your firewall logs traffic. Use Pause "
"if it moves too fast."
@@ -885,7 +885,7 @@ msgstr ""
"Таблица обновляется автоматически, когда файрвол пишет в журнал. Используйте "
"Паузу, если слишком быстро."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2723
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
msgid "Theme tint fallback"
msgstr "Запасной цвет темы"
@@ -904,15 +904,15 @@ msgstr ""
msgid "Time"
msgstr "Время"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614
msgid "Top drops"
msgstr "Частые сбросы"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617
msgid "Top rules"
msgstr "Частые правила"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611
msgid "Top talkers"
msgstr "Частые источники"
@@ -939,17 +939,17 @@ msgstr ""
msgid "Undo:"
msgstr "Отмена:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr ""
"Используйте «Подробно» для всех столбцов (флаги, длина, исходное сообщение)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784
msgid "View"
msgstr "Вид"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
msgid "WAN drop/reject logging is off."
msgstr "Журналирование drop/reject для WAN отключено."
@@ -961,7 +961,7 @@ msgstr ""
"Журналирование drop/reject для WAN включено. Заблокированный входящий WAN-"
"трафик будет отображаться здесь. Обычный LAN-трафик — нет."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
@@ -970,7 +970,7 @@ msgstr ""
"должен появляться здесь по мере поступления — обычный LAN-трафик не "
"отображается."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "WAN logging is already enabled."
msgstr "WAN-журналирование уже включено."
@@ -999,25 +999,25 @@ msgstr "WAN-журналирование недоступно: нет зоны W
msgid "Waiting for firewall events"
msgstr "Ожидание событий межсетевого экрана"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736
msgid "Watching"
msgstr "Наблюдение"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838
msgid "Wrap"
msgstr "Перенос"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901
msgid "block"
msgstr "заблокирован"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348
msgid "buffer filling"
msgstr "буфер заполняется"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352
msgid "buffer full"
msgstr "буфер заполнен"
@@ -1029,7 +1029,7 @@ msgstr "настроенный log_limit: %s"
msgid "contains"
msgstr "содержит"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357
msgid "display limited to %d rows"
msgstr "отображение ограничено %d строками"
@@ -1038,11 +1038,11 @@ msgid "does not contain"
msgstr "не содержит"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
msgid "drop"
msgstr "отброшен"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361
msgid "hostname lookup paused"
msgstr "разрешение имён приостановлено"
@@ -1050,11 +1050,11 @@ msgstr "разрешение имён приостановлено"
msgid "is"
msgstr "есть"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347
msgid "loading buffer"
msgstr "загрузка буфера"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789
msgid "loading…"
msgstr "загрузка…"
@@ -1062,8 +1062,8 @@ msgstr "загрузка…"
msgid "no WAN rate limit configured"
msgstr "Ограничение частоты WAN не настроено"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596
msgid "none"
msgstr "нет"
@@ -1071,99 +1071,99 @@ msgstr "нет"
msgid "not"
msgstr "не"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019
msgid "not AH"
msgstr "не AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018
msgid "not ESP"
msgstr "не ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017
msgid "not GRE"
msgstr "не GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014
msgid "not ICMP"
msgstr "не ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015
msgid "not ICMPv6"
msgstr "не ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016
msgid "not IGMP"
msgstr "не IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020
msgid "not SCTP"
msgstr "не SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012
msgid "not TCP"
msgstr "не TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013
msgid "not UDP"
msgstr "не UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907
msgid "not block"
msgstr "не блокировать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906
msgid "not drop"
msgstr "не отбрасывать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905
msgid "not pass"
msgstr "не пропускать"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908
msgid "not reject"
msgstr "не отклонять"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909
msgid "not unknown"
msgstr "не неизвестно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925
msgid "or type…"
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:2875
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
msgid "pass"
msgstr "разрешён"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359
msgid "polling slowed"
msgstr "опрос замедлен"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903
msgid "reject"
msgstr "отклонён"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363
msgid "scroll frozen — scroll to top to follow live"
msgstr ""
"прокрутка заморожена — прокрутите вверх, чтобы следить в реальном времени"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360
msgid "server limited fetch"
msgstr "сервер ограничил выборку"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354
msgid "table refresh throttled"
msgstr "обновление таблицы ограничено"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904
msgid "unknown"
msgstr "неизвестно"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "using fw4"
msgstr "используется fw4"
+162 -162
View File
@@ -1,31 +1,31 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791
msgid "%d matching · %d/%d stored"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756
msgid "%dd"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755
msgid "%dh"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754
msgid "%dm"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753
msgid "%ds"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
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:1482
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
@@ -35,17 +35,17 @@ msgstr ""
msgid "%s: %s"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793
msgid "0 matching · %d/%d stored"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487
msgid "Accessible (teal/orange)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Action"
msgstr ""
@@ -54,11 +54,11 @@ msgid ""
"Activate the message button or click a row to show or hide the full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998
msgid "Administrator access is required to disable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982
msgid "Administrator access is required to enable logging."
msgstr ""
@@ -66,7 +66,7 @@ msgstr ""
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004
msgid "Also seen"
msgstr ""
@@ -80,16 +80,16 @@ msgid ""
"state."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899
msgid "Any action"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997
msgid "Any protocol"
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:1545
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570
msgid "Auto"
msgstr ""
@@ -105,33 +105,33 @@ msgstr ""
msgid "Changes:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
msgid "Classic (green/red)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
msgid "Clear all"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972
msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, activate the message button or click a "
"row to show or hide the full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
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:1973
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998
msgid "Common"
msgstr ""
@@ -141,7 +141,7 @@ msgid ""
"it; kernel/logd may still constrain total log volume."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804
msgid "Connection lost — retrying…"
msgstr ""
@@ -153,49 +153,49 @@ msgstr ""
msgid "Could not clear the WAN zone log option."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804
msgid "Could not create a temporary file for rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
msgid "Could not disable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971
msgid "Could not enable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907
msgid "Could not load logging status."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810
msgid "Could not load rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806
msgid "Could not process firewall rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999
msgid "Could not reach the router; check the current logging state."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Could not read the active firewall rules."
msgstr ""
#: 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:906
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:810
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815
msgid "Could not refresh rule names."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr ""
@@ -215,7 +215,7 @@ msgstr ""
msgid "Could not write the WAN zone log option."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr ""
@@ -228,7 +228,7 @@ msgstr ""
msgid "Destination"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957
msgid "Destination IP contains (! to exclude)"
msgstr ""
@@ -236,15 +236,15 @@ msgstr ""
msgid "Destination port"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962
msgid "Destination port (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
msgid "Detail"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Details"
msgstr ""
@@ -256,17 +256,17 @@ msgstr ""
msgid "Disabling…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
msgid ""
"Display limited to %d rows on this device; the larger Limit still buffers "
"rows."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545
msgid "Display options"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
msgid ""
"Display options set Limit, Fetch budget, Maximum raw lines, row tint, "
"palette, and hostnames."
@@ -288,7 +288,7 @@ msgstr ""
msgid "Enable logging"
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:2633
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."
@@ -298,29 +298,29 @@ msgstr ""
msgid "Enabling…"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011
msgid "Exclude"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Exclude instead"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559
msgid "Fetch budget"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Filter by %s"
msgstr ""
@@ -328,12 +328,12 @@ 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:2697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr ""
@@ -345,7 +345,7 @@ msgstr ""
msgid "Flow"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658
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."
@@ -359,7 +359,7 @@ msgstr ""
msgid "Grant access to firewall live log view"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624
msgid "Help"
msgstr ""
@@ -367,15 +367,15 @@ msgstr ""
msgid "Hide full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Hide rows"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543
msgid "Hostname lookup paused while the router is under load."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002
msgid "ICMPv6"
msgstr ""
@@ -383,7 +383,7 @@ msgstr ""
msgid "IN"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695
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 "
@@ -396,7 +396,7 @@ msgid ""
"Help / the enabling-logs guide."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679
msgid ""
"In Simple view, activate the message button or click a row to show or hide "
"the full message."
@@ -406,8 +406,8 @@ msgstr ""
msgid "Inbound"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Include instead"
msgstr ""
@@ -417,7 +417,7 @@ msgid ""
"release, then reload the firewall."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr ""
@@ -426,7 +426,7 @@ msgstr ""
msgid "Interface"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942
msgid "Interface (prefix ! to exclude)"
msgstr ""
@@ -443,11 +443,11 @@ msgstr ""
msgid "Len"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Limit"
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:861
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr ""
@@ -455,16 +455,16 @@ msgstr ""
msgid "Logging is off on this router"
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:1546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
msgid "Manual"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577
msgid "Manual still uses server protection and poll cadence"
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:2653
msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
@@ -474,18 +474,18 @@ msgstr ""
msgid "Manual test (System → Terminal):"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571
msgid "Maximum raw lines"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826
msgid "Message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
msgid "More filters"
msgstr ""
@@ -528,7 +528,7 @@ msgstr ""
msgid "OUT"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "One line"
msgstr ""
@@ -555,20 +555,20 @@ msgstr ""
msgid "Outbound"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598
msgid "Palette"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
msgid "Pause"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
msgid "Paused"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530
msgid ""
"Polling slowed: the router is responding slowly, so polls run less often."
msgstr ""
@@ -578,11 +578,11 @@ msgstr ""
msgid "Proto"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918
msgid "Protocol — common values"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889
msgid "Quick search"
msgstr ""
@@ -590,28 +590,28 @@ msgstr ""
msgid "Rate limit:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
msgid "Remove filter"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
msgid "Resume"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588
msgid "Row tint"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
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:2720
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -621,19 +621,19 @@ msgstr ""
msgid "Rule"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Rule name details"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832
msgid "Rule-name lookup does not change your firewall rules."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817
msgid "Rule-name lookup failed."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824
msgid "Rule-name lookup reached a safety limit."
msgstr ""
@@ -645,22 +645,22 @@ msgstr ""
msgid "Search"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527
msgid "Server adaptive protection is disabled."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535
msgid ""
"Server limited fetch: the router is under load; at most %d log lines per "
"poll."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid ""
"Server limited fetch: this poll returned fewer lines to protect the router."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "Server protection state is unknown."
msgstr ""
@@ -668,34 +668,34 @@ msgstr ""
msgid "Show full message"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615
msgid "Show hostnames"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586
msgid "Show pass/deny row background colors"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872
msgid "Show rows"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605
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:2771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "Simple"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835
msgid ""
"Some entries may show names derived from their log prefixes. Searching by a "
"friendly rule name may miss those entries."
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:855
msgid "Some rule names may be missing"
msgstr ""
@@ -704,7 +704,7 @@ msgstr ""
msgid "Source"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
msgid "Source IP contains (! to exclude)"
msgstr ""
@@ -712,36 +712,36 @@ msgstr ""
msgid "Source port"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952
msgid "Source port (! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
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:2838
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863
msgid "Summary mode"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
msgid ""
"Summary mode: when the router responds slowly, a compact summary replaces "
"the table. Use Show rows to see the table; it returns automatically after "
"three fast polls."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609
msgid "Summary was bounded before all categories could be included."
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:2663
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:1675
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700
msgid ""
"Table refresh throttled: the event rate is high, so the table repaints less "
"often to protect the browser. The buffer still updates; refresh resumes "
@@ -752,7 +752,7 @@ msgstr ""
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:2648
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid ""
"The rate shown for WAN logging is the configured WAN zone log_limit, not "
"proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 "
@@ -760,23 +760,23 @@ msgid ""
"fwlive does not impose a limit."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803
msgid "The router could not read the firewall log — retrying…"
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:2668
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:2603
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2628
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:2723
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
msgid "Theme tint fallback"
msgstr ""
@@ -793,15 +793,15 @@ msgstr ""
msgid "Time"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614
msgid "Top drops"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617
msgid "Top rules"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611
msgid "Top talkers"
msgstr ""
@@ -823,16 +823,16 @@ msgstr ""
msgid "Undo:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784
msgid "View"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
msgid "WAN drop/reject logging is off."
msgstr ""
@@ -842,13 +842,13 @@ msgid ""
"here. Normal LAN browsing will not."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
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:972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "WAN logging is already enabled."
msgstr ""
@@ -877,25 +877,25 @@ msgstr ""
msgid "Waiting for firewall events"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736
msgid "Watching"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838
msgid "Wrap"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901
msgid "block"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348
msgid "buffer filling"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352
msgid "buffer full"
msgstr ""
@@ -907,7 +907,7 @@ msgstr ""
msgid "contains"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357
msgid "display limited to %d rows"
msgstr ""
@@ -916,11 +916,11 @@ msgid "does not contain"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
msgid "drop"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361
msgid "hostname lookup paused"
msgstr ""
@@ -928,11 +928,11 @@ msgstr ""
msgid "is"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347
msgid "loading buffer"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789
msgid "loading…"
msgstr ""
@@ -940,8 +940,8 @@ msgstr ""
msgid "no WAN rate limit configured"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596
msgid "none"
msgstr ""
@@ -949,98 +949,98 @@ msgstr ""
msgid "not"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019
msgid "not AH"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018
msgid "not ESP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017
msgid "not GRE"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014
msgid "not ICMP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015
msgid "not ICMPv6"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016
msgid "not IGMP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020
msgid "not SCTP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012
msgid "not TCP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013
msgid "not UDP"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907
msgid "not block"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906
msgid "not drop"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905
msgid "not pass"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908
msgid "not reject"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909
msgid "not unknown"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925
msgid "or type…"
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:2875
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
msgid "pass"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359
msgid "polling slowed"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903
msgid "reject"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363
msgid "scroll frozen — scroll to top to follow live"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360
msgid "server limited fetch"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354
msgid "table refresh throttled"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904
msgid "unknown"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "using fw4"
msgstr ""
+162 -162
View File
@@ -12,31 +12,31 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1766
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1791
msgid "%d matching · %d/%d stored"
msgstr "%d 条匹配 · 已存 %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1731
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1756
msgid "%dd"
msgstr "%d天"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1730
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1755
msgid "%dh"
msgstr "%d小时"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1729
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1754
msgid "%dm"
msgstr "%d分钟"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1728
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1753
msgid "%ds"
msgstr "%d秒"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1492
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1517
msgid "%s · requested up to %d raw lines · %d firewall messages returned"
msgstr "%s · 请求最多 %d 行原始日志 · 返回 %d 条防火墙消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1482
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1507
msgid ""
"%s · requested up to %d raw lines · server limited fetch to %d · %d firewall "
"messages returned"
@@ -47,17 +47,17 @@ msgstr ""
msgid "%s: %s"
msgstr "%s: %s"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1768
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1793
msgid "0 matching · %d/%d stored"
msgstr "0 条匹配 · 已存 %d/%d"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:487
msgid "Accessible (teal/orange)"
msgstr "无障碍(青绿/橙色)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:440
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:38
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Action"
msgstr "动作"
@@ -66,11 +66,11 @@ msgid ""
"Activate the message button or click a row to show or hide the full message"
msgstr "激活消息按钮或单击行即可显示或隐藏完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:998
msgid "Administrator access is required to disable logging."
msgstr "需要管理员权限才能禁用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:982
msgid "Administrator access is required to enable logging."
msgstr "需要管理员权限才能启用日志记录。"
@@ -78,7 +78,7 @@ msgstr "需要管理员权限才能启用日志记录。"
msgid "Allow/deny rules, LAN logging, or anything else."
msgstr "允许/拒绝规则、LAN 日志记录或任何其他设置。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1979
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2004
msgid "Also seen"
msgstr "其他常见"
@@ -92,16 +92,16 @@ msgid ""
"state."
msgstr "保存后另有更改覆盖了 WAN 日志设置;请检查当前状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2874
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2899
msgid "Any action"
msgstr "所有动作"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1997
msgid "Any protocol"
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:1545
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1570
msgid "Auto"
msgstr "自动"
@@ -117,20 +117,20 @@ msgstr "无法启用日志记录,直到内核日志模块已安装。"
msgid "Changes:"
msgstr "变更:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:485
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:486
msgid "Classic (green/red)"
msgstr "经典(绿/红)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2579
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2604
msgid "Classic uses green/red; Accessible uses teal/orange"
msgstr "经典使用绿/红;无障碍使用青绿/橙色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:94
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:274
msgid "Clear all"
msgstr "全部清除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2972
msgid ""
"Click a cell to filter · ≠ on a chip to exclude · Ctrl+click a rule for "
"firewall settings · in Simple view, activate the message button or click a "
@@ -139,14 +139,14 @@ msgstr ""
"点击单元格筛选 · 芯片上的 ≠ 排除 · Ctrl+点击规则打开防火墙设置 · 在简洁视图"
"中,激活消息按钮或单击行即可显示或隐藏完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2659
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2684
msgid ""
"Click an IP, action, or protocol to filter; use the Protocol menu (or ≠ on a "
"chip) to exclude."
msgstr ""
"点击 IP、动作或协议进行筛选;使用协议菜单(或筛选标签上的 ≠)进行排除。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1973
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1998
msgid "Common"
msgstr "常用"
@@ -158,7 +158,7 @@ msgstr ""
"已配置的 WAN 区域 log_limit:%s。只有支持该选项的 fw4 构建才会实施此限制;内"
"核/logd 仍可能限制日志总量。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1779
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1804
msgid "Connection lost — retrying…"
msgstr "连接已断开 — 正在重试…"
@@ -170,49 +170,49 @@ msgstr "无法获取日志记录锁。"
msgid "Could not clear the WAN zone log option."
msgstr "无法清除 WAN 区域日志选项。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:799
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:804
msgid "Could not create a temporary file for rule names."
msgstr "无法为规则名称创建临时文件。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:993
msgid "Could not disable logging."
msgstr "无法禁用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:966
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:971
msgid "Could not enable logging."
msgstr "无法启用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:907
msgid "Could not load logging status."
msgstr "无法加载日志状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:805
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:810
msgid "Could not load rule names."
msgstr "无法加载规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:806
msgid "Could not process firewall rule names."
msgstr "无法处理防火墙规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:945
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:978
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:950
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:983
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:999
msgid "Could not reach the router; check the current logging state."
msgstr "无法连接路由器;请检查当前日志状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:803
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
msgid "Could not read the active firewall rules."
msgstr "无法读取当前生效的防火墙规则。"
#: 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:906
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:810
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:815
msgid "Could not refresh rule names."
msgstr "无法刷新规则名称。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:808
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:813
msgid "Could not refresh rule names; previously loaded names are still shown."
msgstr "无法刷新规则名称;仍显示之前加载的名称。"
@@ -232,7 +232,7 @@ msgstr "无法安全跟踪日志记录的更改;日志记录未更改。"
msgid "Could not write the WAN zone log option."
msgstr "无法写入 WAN 区域日志选项。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
msgid "Custom protocol (prefix ! to exclude). Overrides the menu when set."
msgstr "自定义协议(! 排除)。有内容时覆盖菜单。"
@@ -245,7 +245,7 @@ msgstr "目的端口"
msgid "Destination"
msgstr "目的地址"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2932
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2957
msgid "Destination IP contains (! to exclude)"
msgstr "目的 IP 包含(! 表示排除)"
@@ -253,15 +253,15 @@ msgstr "目的 IP 包含(! 表示排除)"
msgid "Destination port"
msgstr "目标端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2962
msgid "Destination port (! to exclude)"
msgstr "目的端口(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2782
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2807
msgid "Detail"
msgstr "详细"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Details"
msgstr "详情"
@@ -273,17 +273,17 @@ msgstr "方向"
msgid "Disabling…"
msgstr "正在禁用…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1522
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1547
msgid ""
"Display limited to %d rows on this device; the larger Limit still buffers "
"rows."
msgstr "显示上限为 %d 行(本设备);较大的行数限制仍会缓冲这些行。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2520
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2545
msgid "Display options"
msgstr "显示选项"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2613
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2638
msgid ""
"Display options set Limit, Fetch budget, Maximum raw lines, row tint, "
"palette, and hostnames."
@@ -305,7 +305,7 @@ msgstr "启用 WAN 区域的丢弃/拒绝日志记录(同 网络 → 防火墙
msgid "Enable logging"
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:2633
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."
@@ -317,20 +317,20 @@ msgstr ""
msgid "Enabling…"
msgstr "正在启用…"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1986
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2011
msgid "Exclude"
msgstr "排除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Exclude instead"
msgstr "改为排除"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2534
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2559
msgid "Fetch budget"
msgstr "抓取配额"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2623
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2648
msgid ""
"Fetch budget controls raw log lines per poll. Auto derives from Limit; "
"Manual selects a bounded maximum."
@@ -338,10 +338,10 @@ msgstr ""
"抓取配额控制每次轮询的原始日志行数。「自动」由行数限制推导;「手动」可选择一"
"个有上限的最大值。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:63
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:84
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2871
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/links.js:149
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2896
msgid "Filter by %s"
msgstr "按 %s 筛选"
@@ -349,12 +349,12 @@ msgstr "按 %s 筛选"
msgid "Filter logs by rule (hint: %s). Ctrl+click to open firewall settings."
msgstr "按规则筛选日志(提示:%s)。Ctrl+点击可打开防火墙设置。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2697
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2722
#: openwrt-feed/luci-app-fwlive/root/usr/share/luci/menu.d/luci-app-fwlive.json:3
msgid "Firewall Live View"
msgstr "防火墙实时视图"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1746
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1771
msgid "Firewall log has no current entries; showing rows last received %s ago."
msgstr "防火墙日志当前没有条目;显示的是 %s 前收到的日志行。"
@@ -366,7 +366,7 @@ msgstr "标志"
msgid "Flow"
msgstr "流向"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2633
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2658
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."
@@ -382,7 +382,7 @@ msgstr "已转发"
msgid "Grant access to firewall live log view"
msgstr "授予防火墙实时日志视图的访问权限"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2599
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2624
msgid "Help"
msgstr "帮助"
@@ -390,15 +390,15 @@ msgstr "帮助"
msgid "Hide full message"
msgstr "隐藏完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
msgid "Hide rows"
msgstr "隐藏行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1518
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1543
msgid "Hostname lookup paused while the router is under load."
msgstr "主机名解析已暂停:路由器负载较高。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1977
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2002
msgid "ICMPv6"
msgstr "ICMPv6"
@@ -406,7 +406,7 @@ msgstr "ICMPv6"
msgid "IN"
msgstr "IN"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2670
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2695
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 "
@@ -423,7 +423,7 @@ msgstr ""
"如果 WAN 侧较安静,请等待探测报文,或使用「帮助」/启用日志指南中的可选 ping "
"检查。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2654
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2679
msgid ""
"In Simple view, activate the message button or click a row to show or hide "
"the full message."
@@ -433,8 +433,8 @@ msgstr "在简洁视图中,激活消息按钮或单击行即可显示或隐藏
msgid "Inbound"
msgstr "入站"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:92
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:190
msgid "Include instead"
msgstr "改为包含"
@@ -446,7 +446,7 @@ msgstr ""
"请使用适用于您 OpenWrt 版本的命令安装 kmod-nf-log 和 kmod-nf-log6,然后重新加"
"载防火墙。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1777
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1802
msgid "Installation is incomplete. Reinstall luci-app-fwlive."
msgstr "安装不完整。请重新安装 luci-app-fwlive。"
@@ -455,7 +455,7 @@ msgstr "安装不完整。请重新安装 luci-app-fwlive。"
msgid "Interface"
msgstr "接口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2917
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2942
msgid "Interface (prefix ! to exclude)"
msgstr "接口(前缀 ! 表示排除)"
@@ -472,11 +472,11 @@ msgstr "缺少内核日志模块"
msgid "Len"
msgstr "长度"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2523
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2548
msgid "Limit"
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:861
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
@@ -484,16 +484,16 @@ msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
msgid "Logging is off on this router"
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:1546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1498
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
msgid "Manual"
msgstr "手动"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2552
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2577
msgid "Manual still uses server protection and poll cadence"
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:2653
msgid ""
"Manual still uses server protection and poll cadence; it changes the fetch "
"budget, not the polling interval."
@@ -504,18 +504,18 @@ msgstr ""
msgid "Manual test (System → Terminal):"
msgstr "手动测试(系统 → 终端):"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2546
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2571
msgid "Maximum raw lines"
msgstr "最大原始行数"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:370
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:52
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2801
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2794
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:377
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2826
msgid "Message"
msgstr "消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2912
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2937
msgid "More filters"
msgstr "更多筛选"
@@ -559,7 +559,7 @@ msgstr "点击「启用日志」之前不会有任何更改。"
msgid "OUT"
msgstr "出接口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2824
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2849
msgid "One line"
msgstr "单行"
@@ -588,20 +588,20 @@ msgstr ""
msgid "Outbound"
msgstr "出站"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2573
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2598
msgid "Palette"
msgstr "配色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2773
msgid "Pause"
msgstr "暂停"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
msgid "Paused"
msgstr "已暂停"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1505
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1530
msgid ""
"Polling slowed: the router is responding slowly, so polls run less often."
msgstr "轮询已放慢:路由器响应较慢,因此降低轮询频率。"
@@ -611,11 +611,11 @@ msgstr "轮询已放慢:路由器响应较慢,因此降低轮询频率。"
msgid "Proto"
msgstr "协议"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2893
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2918
msgid "Protocol — common values"
msgstr "协议 — 常用值"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2889
msgid "Quick search"
msgstr "快速搜索"
@@ -623,21 +623,21 @@ msgstr "快速搜索"
msgid "Rate limit:"
msgstr "速率限制:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:229
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:230
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:93
msgid "Remove filter"
msgstr "移除筛选"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1840
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1865
msgid "Resume"
msgstr "继续"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2563
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2588
msgid "Row tint"
msgstr "行着色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2664
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2689
msgid ""
"Row tint shows pass/deny row backgrounds when checked. Choose Classic (green/"
"red, default) or Accessible (teal/orange). Action text stays colored either "
@@ -646,7 +646,7 @@ msgstr ""
"勾选后行着色显示放行/拒绝行背景。可选经典(绿/红,默认)或无障碍(青绿/橙)。"
"操作文本在两种模式下均保持着色。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2720
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2745
msgid ""
"Row tint used a local color fallback because the active LuCI theme did not "
"apply pass/deny backgrounds."
@@ -656,19 +656,19 @@ msgstr "由于当前 LuCI 主题未应用放行/拒绝背景色,行着色使
msgid "Rule"
msgstr "规则"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2735
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2760
msgid "Rule name details"
msgstr "规则名称详情"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:827
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:832
msgid "Rule-name lookup does not change your firewall rules."
msgstr "规则名称查询不会更改您的防火墙规则。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:812
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:817
msgid "Rule-name lookup failed."
msgstr "规则名称查询失败。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:819
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:824
msgid "Rule-name lookup reached a safety limit."
msgstr "规则名称查询已达到安全限制。"
@@ -680,22 +680,22 @@ msgstr "源端口"
msgid "Search"
msgstr "搜索"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1502
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1527
msgid "Server adaptive protection is disabled."
msgstr "服务器自适应保护已禁用。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1510
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1535
msgid ""
"Server limited fetch: the router is under load; at most %d log lines per "
"poll."
msgstr "服务器已限制抓取:路由器负载较高;每次轮询最多 %d 行日志。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1515
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1540
msgid ""
"Server limited fetch: this poll returned fewer lines to protect the router."
msgstr "服务器已限制抓取:本次轮询返回的行数较少,以保护路由器。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1500
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1525
msgid "Server protection state is unknown."
msgstr "服务器保护状态未知。"
@@ -703,28 +703,28 @@ msgstr "服务器保护状态未知。"
msgid "Show full message"
msgstr "显示完整消息"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2590
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2615
msgid "Show hostnames"
msgstr "显示主机名"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2561
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2586
msgid "Show pass/deny row background colors"
msgstr "显示放行/拒绝行背景色"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1620
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2847
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1645
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2872
msgid "Show rows"
msgstr "显示行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1580
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1605
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:2771
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2796
msgid "Simple"
msgstr "简洁"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:830
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:835
msgid ""
"Some entries may show names derived from their log prefixes. Searching by a "
"friendly rule name may miss those entries."
@@ -732,7 +732,7 @@ 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:855
msgid "Some rule names may be missing"
msgstr "部分规则名称可能缺失"
@@ -741,7 +741,7 @@ msgstr "部分规则名称可能缺失"
msgid "Source"
msgstr "源地址"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2922
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2947
msgid "Source IP contains (! to exclude)"
msgstr "源 IP 包含(! 表示排除)"
@@ -749,19 +749,19 @@ msgstr "源 IP 包含(! 表示排除)"
msgid "Source port"
msgstr "源端口"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2927
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2952
msgid "Source port (! to exclude)"
msgstr "源端口(! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1582
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1607
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:2838
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2863
msgid "Summary mode"
msgstr "摘要模式"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2618
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2643
msgid ""
"Summary mode: when the router responds slowly, a compact summary replaces "
"the table. Use Show rows to see the table; it returns automatically after "
@@ -770,11 +770,11 @@ msgstr ""
"摘要模式:路由器响应较慢时,会以简要摘要代替表格。点击「显示行」可查看表格;"
"连续三次快速轮询后会自动恢复。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1584
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1609
msgid "Summary was bounded before all categories could be included."
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:2663
msgid ""
"Switching tabs pauses polling; returning performs one catch-up poll. "
"Hostnames are off by default because lookups add work."
@@ -782,7 +782,7 @@ msgstr ""
"切换到其他标签页会暂停轮询;返回时执行一次追赶轮询。主机名默认关闭,因为解析"
"会增加工作量。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1675
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1700
msgid ""
"Table refresh throttled: the event rate is high, so the table repaints less "
"often to protect the browser. The buffer still updates; refresh resumes "
@@ -795,7 +795,7 @@ msgstr ""
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:2648
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2673
msgid ""
"The rate shown for WAN logging is the configured WAN zone log_limit, not "
"proof the running fw4 supports it. The fw4 bundled with OpenWrt 23.05.5 "
@@ -806,11 +806,11 @@ msgstr ""
"该选项。OpenWrt 23.05.5 附带的 fw4 会忽略此选项。内核/logd 仍可能限制日志总"
"量;fwlive 不会施加限制。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1803
msgid "The router could not read the firewall log — retrying…"
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:2668
msgid ""
"The router log ring may evict older events before fwlive reads them. fwlive "
"cannot recover evicted entries or change forwarding behavior."
@@ -818,13 +818,13 @@ msgstr ""
"路由器日志环形缓冲区可能在 fwlive 读取前淘汰较旧事件。fwlive 无法恢复已淘汰的"
"条目,也不会改变转发行为。"
#: 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:2628
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:2723
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2748
msgid "Theme tint fallback"
msgstr "主题着色回退"
@@ -841,15 +841,15 @@ msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
msgid "Time"
msgstr "时间"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1589
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1614
msgid "Top drops"
msgstr "主要丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1592
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1617
msgid "Top rules"
msgstr "主要规则"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1586
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1611
msgid "Top talkers"
msgstr "主要通信方"
@@ -872,16 +872,16 @@ msgstr "开启 WAN 防火墙区域的日志记录并重新加载防火墙。"
msgid "Undo:"
msgstr "撤销:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2667
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2692
msgid "Use Detail for all columns (flags, length, raw message)."
msgstr "使用「详细」可查看全部列(标志、长度、原始消息)。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2759
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2753
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2778
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2784
msgid "View"
msgstr "视图"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:994
msgid "WAN drop/reject logging is off."
msgstr "WAN 丢弃/拒绝日志记录已关闭。"
@@ -893,7 +893,7 @@ msgstr ""
"WAN 丢弃/拒绝日志记录已开启。被阻止的 WAN 入站流量会显示在这里;正常的 LAN 浏"
"览不会。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:970
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:975
msgid ""
"WAN drop/reject logging is on. Blocked inbound traffic should appear here as "
"it happens — not normal LAN browsing."
@@ -901,7 +901,7 @@ msgstr ""
"WAN 丢弃/拒绝日志记录已开启。被阻止的入站流量会实时显示在这里 — 正常的 LAN 浏"
"览不会。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:972
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:977
msgid "WAN logging is already enabled."
msgstr "WAN 日志记录已处于启用状态。"
@@ -930,25 +930,25 @@ msgstr "WAN 日志不可用:无 WAN 区域"
msgid "Waiting for firewall events"
msgstr "正在等待防火墙事件"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1839
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2711
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1864
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2736
msgid "Watching"
msgstr "监视中"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2813
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2838
msgid "Wrap"
msgstr "自动换行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:74
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2876
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2901
msgid "block"
msgstr "拦截"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1323
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1348
msgid "buffer filling"
msgstr "缓冲区填充中"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1327
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1352
msgid "buffer full"
msgstr "缓冲区已满"
@@ -960,7 +960,7 @@ msgstr "已配置的 log_limit:%s"
msgid "contains"
msgstr "包含"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1332
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1357
msgid "display limited to %d rows"
msgstr "显示上限为 %d 行"
@@ -969,11 +969,11 @@ msgid "does not contain"
msgstr "不包含"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:75
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2877
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2902
msgid "drop"
msgstr "丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1336
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1361
msgid "hostname lookup paused"
msgstr "主机名解析已暂停"
@@ -981,11 +981,11 @@ msgstr "主机名解析已暂停"
msgid "is"
msgstr "是"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1322
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1347
msgid "loading buffer"
msgstr "正在加载缓冲区"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1764
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1789
msgid "loading…"
msgstr "加载中…"
@@ -993,8 +993,8 @@ msgstr "加载中…"
msgid "no WAN rate limit configured"
msgstr "未配置 WAN 速率限制"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1562
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1571
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1587
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1596
msgid "none"
msgstr "无"
@@ -1002,98 +1002,98 @@ msgstr "无"
msgid "not"
msgstr "非"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1994
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2019
msgid "not AH"
msgstr "非 AH"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1993
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2018
msgid "not ESP"
msgstr "非 ESP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1992
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2017
msgid "not GRE"
msgstr "非 GRE"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1989
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2014
msgid "not ICMP"
msgstr "非 ICMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1990
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2015
msgid "not ICMPv6"
msgstr "非 ICMPv6"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1991
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2016
msgid "not IGMP"
msgstr "非 IGMP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1995
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2020
msgid "not SCTP"
msgstr "非 SCTP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1987
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2012
msgid "not TCP"
msgstr "非 TCP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1988
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2013
msgid "not UDP"
msgstr "非 UDP"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2882
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2907
msgid "not block"
msgstr "非阻止"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2881
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2906
msgid "not drop"
msgstr "非丢弃"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2880
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2905
msgid "not pass"
msgstr "非放行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2883
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2908
msgid "not reject"
msgstr "非拒绝"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2884
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2909
msgid "not unknown"
msgstr "非未知"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2925
msgid "or type…"
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:2875
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2900
msgid "pass"
msgstr "通过"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1334
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1359
msgid "polling slowed"
msgstr "轮询已放慢"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:76
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2878
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2903
msgid "reject"
msgstr "拒绝"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1338
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1363
msgid "scroll frozen — scroll to top to follow live"
msgstr "滚动已冻结 — 滚动到顶部以恢复跟随"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1335
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1360
msgid "server limited fetch"
msgstr "服务器已限制抓取"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1329
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1354
msgid "table refresh throttled"
msgstr "表格刷新已限流"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/table.js:65
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2879
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2904
msgid "unknown"
msgstr "未知"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:792
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:797
msgid "using fw4"
msgstr "使用 fw4"
+133 -9
View File
@@ -41,6 +41,11 @@ POLL_LINES_MAX=2000
# Cap rules JSON size so a huge ruleset cannot unbound an rpcd reply.
RULES_MAP_MAX_KEYS=512
RULES_MAP_MAX_BYTES=65536
# Reserve the longest envelope (unknown backend, false, mktemp_failed)
# including both objects, commas and the closing brace. Map contents
# are counted separately after JSON escaping.
RULES_MAP_ENVELOPE='{"backend":"unknown","rules":{},"targets":{},"truncated":false,"error":"mktemp_failed"}'
RULES_MAP_MAX_PAYLOAD_BYTES=$(( RULES_MAP_MAX_BYTES - ${#RULES_MAP_ENVELOPE} ))
# First-wins dups never increment _map_keys, so they would otherwise walk
# the whole dump. Bound prepared prefix records independently.
RULES_MAP_MAX_LINES=512
@@ -54,6 +59,28 @@ json_object_append() {
OUT="${OUT}\"$1\":\"$2\""
}
# UCI target tokens use only the space left after all rule labels.
# Keys/values have passed is_uci_style_name, so their JSON form is unchanged.
targets_add() {
esc_key="$1"
esc_val="$2"
[ -n "$esc_key" ] && [ -n "$esc_val" ] || return 0
case "$TARGETS_OUT" in
*"\"$esc_key\":"*) return 0 ;;
esac
_pair_len=$(( ${#esc_key} + ${#esc_val} + 5 ))
_next_len=$(( ${#OUT} + ${#TARGETS_OUT} + _pair_len ))
[ -n "$TARGETS_OUT" ] && _next_len=$((_next_len + 1))
if [ "$_next_len" -gt "$RULES_MAP_MAX_PAYLOAD_BYTES" ]; then
_targets_overflow=1
return 0
fi
if [ -n "$TARGETS_OUT" ]; then
TARGETS_OUT="${TARGETS_OUT},"
fi
TARGETS_OUT="${TARGETS_OUT}\"$esc_key\":\"$esc_val\""
}
map_add() {
key="$1"
val="$2"
@@ -87,7 +114,7 @@ map_add_escaped() {
_next_len=$(( ${#OUT} + _pair_len ))
[ -n "$OUT" ] && _next_len=$((_next_len + 1))
if [ "${_map_keys:-0}" -ge "$RULES_MAP_MAX_KEYS" ] || \
[ "$_next_len" -gt "$RULES_MAP_MAX_BYTES" ]; then
[ "$_next_len" -gt "$RULES_MAP_MAX_PAYLOAD_BYTES" ]; then
_map_overflow=1
return 0
fi
@@ -98,6 +125,8 @@ map_add_escaped() {
uci_rule_names() {
# uci missing / empty firewall config is "no names", not fatal.
# Read the captured show output once; never fork one uci get per section.
# Emit name<TAB>slug<TAB>target (target may be empty). Buffer per section
# so name-then-target and target-then-name both pair.
_firewall_show=$(uci -q show firewall 2>/dev/null || true)
printf '%s\n' "$_firewall_show" |
awk -v max="$RULES_MAP_MAX_LINES" '
@@ -109,36 +138,96 @@ uci_rule_names() {
}
return out
}
function emit(value, quote) {
function strip_quotes(value, quote) {
quote = sprintf("%c", 39)
if (substr(value, 1, 1) == quote && substr(value, length(value), 1) == quote)
value = substr(value, 2, length(value) - 2)
return value
}
function emit_buffered( out_name, out_target) {
out_name = buf_name
out_target = buf_target
buf_name = ""
buf_target = ""
if (truncated) return
if (out_name == "") return
if (buf_kind == "named" && buf_is_rule != 1) return
if (count >= max) {
truncated = 1
exit 42
}
print value "\t" slug_uci(value)
print out_name "\t" slug_uci(out_name) "\t" out_target
count++
}
function new_section(id, kind, is_rule) {
emit_buffered()
buf_id = id
buf_kind = kind
buf_is_rule = 0
if (is_rule) buf_is_rule = 1
}
{
equal = index($0, "=")
if (!equal) next
key = substr($0, 1, equal - 1)
value = substr($0, equal + 1)
if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]$/) {
if (buf_id != key || buf_kind != "anon")
new_section(key, "anon", 1)
else
buf_is_rule = 1
next
}
if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]\.name$/) {
emit(value)
sid = key
sub(/\.name$/, "", sid)
if (buf_id != sid || buf_kind != "anon")
new_section(sid, "anon", 1)
buf_name = strip_quotes(value)
next
}
if (key ~ /^firewall\.@rule\[[0-9][0-9]*\]\.target$/) {
sid = key
sub(/\.target$/, "", sid)
if (buf_id != sid || buf_kind != "anon")
new_section(sid, "anon", 1)
buf_target = strip_quotes(value)
next
}
if (key ~ /^firewall\.[A-Za-z0-9_-]+$/) {
section = key
sub(/^firewall\./, "", section)
current_rule = (value == "rule")
if (buf_id == section && buf_kind == "named") {
buf_is_rule = 0
if (value == "rule") buf_is_rule = 1
} else {
new_section(section, "named", (value == "rule"))
}
next
}
if (key ~ /^firewall\.[A-Za-z0-9_-]+\.name$/) {
section = key
sub(/^firewall\./, "", section)
sub(/\.name$/, "", section)
if (buf_id != section || buf_kind != "named")
new_section(section, "named", 0)
buf_name = strip_quotes(value)
next
}
if (key ~ /^firewall\.[A-Za-z0-9_-]+\.target$/) {
section = key
sub(/^firewall\./, "", section)
sub(/\.target$/, "", section)
if (buf_id != section || buf_kind != "named")
new_section(section, "named", 0)
buf_target = strip_quotes(value)
next
}
if (current_rule && key == "firewall." section ".name")
emit(value)
}
END { if (truncated) exit 42 }
END {
if (truncated) exit 42
emit_buffered()
}
'
}
@@ -170,6 +259,7 @@ map_uci_rule_names() {
# are skipped; one shared line cap also bounds duplicate UCI names that
# do not trip the output key/byte caps.
# Keep the loop in this shell so map_add mutations reach OUT.
# TSV is name<TAB>slug<TAB>target; target may be empty.
if _uci_names="$(uci_rule_names)"; then
_uci_names_truncated=''
else
@@ -189,6 +279,7 @@ map_uci_rule_names() {
[ -n "$line" ] || continue
name="${line%%${_tab}*}"
slug="${line#*"$_tab"}"
slug="${slug%%${_tab}*}"
[ -n "$name" ] || continue
is_uci_style_name "$name" || continue
map_add_escaped "$name" "$name"
@@ -204,6 +295,33 @@ EOF
fi
}
# Reuse the captured UCI records after nft enrichment. Target truncation must
# neither consume rule-label space nor stop the labeled/unlabeled nft passes.
map_uci_rule_targets() {
_targets_overflow=''
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_targets_overflow" ] && break
name="${line%%${_tab}*}"
rest="${line#*"$_tab"}"
slug="${rest%%${_tab}*}"
target="${rest#*"$_tab"}"
is_uci_style_name "$name" && is_uci_style_name "$target" || continue
# Do not attach targets to UCI names excluded by the rule-map caps.
case "$OUT" in
*"\"$name\":\"$name\""*) ;;
*) continue ;;
esac
targets_add "$name" "$target"
[ -n "$_targets_overflow" ] && break
if [ -n "$slug" ] && [ "$slug" != "$name" ]; then
targets_add "$slug" "$target"
fi
done <<EOF
$_uci_names
EOF
[ -z "$_targets_overflow" ] || _map_overflow=1
}
# One awk pass; output encoding depends on mode (default: raw). Raw mode emits
# prefix<TAB>comment with TSV escapes (tab as \t, backslash as \\). Production
# prepared mode emits kind<TAB>key<TAB>alias<TAB>value: kind is the unquoted
@@ -516,7 +634,11 @@ enrich_rules_dump() {
}
build_rules_map() {
# Shell string lengths must count UTF-8 bytes, not locale characters.
LC_ALL=C
export LC_ALL
OUT=''
TARGETS_OUT=''
_map_keys=0
_map_lines=0
_map_overflow=''
@@ -532,10 +654,12 @@ build_rules_map() {
enrich_rules_dump fwlive-nft
fi
map_uci_rule_targets
if [ "$backend" = unknown ] && [ -z "$_enrich_error" ]; then
_enrich_error='no_backend'
fi
_rules_json="{\"backend\":\"$backend\",\"rules\":{$OUT}"
_rules_json="{\"backend\":\"$backend\",\"rules\":{$OUT},\"targets\":{${TARGETS_OUT}}"
# Map limits and enrichment failures are independent outcomes.
_truncated=false
[ -n "$_map_overflow" ] && _truncated=true
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.336
PKG_RELEASE:=32
PKG_VERSION:=0.1.337
PKG_RELEASE:=33
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.16
PKG_RELEASE:=335
PKG_RELEASE:=336
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -1712,7 +1712,7 @@ acl_app() {
start() {
busybox pgrep -f ${TMP_PATH}/bin > /dev/null 2>&1 && {
logger -t PSW-RESTART "Upgrade or overload residue is detected, and the subprocess is being called to perform complete cleaning..."
(stop)
(stop "$1")
sleep 2
}
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
@@ -1765,7 +1765,7 @@ start() {
}
fi
start_crontab $1
start_crontab "$1"
echolog "运行完成!\n"
[ "$ENABLED" = 1 ] && [ "$1" = "boot" ] && {
@@ -1802,7 +1802,7 @@ stop() {
unset V2RAY_LOCATION_ASSET
unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab $1
stop_crontab "$1"
rm -rf $GLOBAL_DNSMASQ_CONF
rm -rf $GLOBAL_DNSMASQ_CONF_PATH
[ "1" = "1" ] && {
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.1
PKG_RELEASE:=143
PKG_RELEASE:=144
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -37,6 +37,7 @@ local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | aw
local singbox_version = api.get_app_version("sing-box"):match("[^v]+")
local version_ge_1_14_0 = api.compare_versions(singbox_version, ">=", "1.14.0")
local version_ge_1_15_0 = api.compare_versions(singbox_version, ">=", "1.15.0")
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("socks", "Socks")
@@ -68,6 +69,9 @@ end
if version_ge_1_14_0 then
o:value("snell", "Snell")
end
if version_ge_1_15_0 and singbox_tags:find("with_quic") then
o:value("masque", "MASQUE")
end
o:value("_urltest", translate("URLTest"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
@@ -197,6 +201,7 @@ o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "ssh" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
o = s:option(Value, "password", translate("Password"))
o.password = true
@@ -209,6 +214,34 @@ o:depends({ protocol = "tuic" })
o:depends({ protocol = "anytls" })
o:depends({ protocol = "ssh" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
-- [[ MASQUE ]] --
o = s:option(Value, "masque_path", translate("MASQUE Path"))
o:depends({ protocol = "masque" })
o = s:option(TextValue, "masque_headers", translate("Custom HTTP Headers"))
o.rows = 5
o.wrap = "off"
o:depends({ protocol = "masque" })
o.cfgvalue = function(self, section)
return (m:get(section, self.config_option) or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n")
local headers = {}
for line in value:gmatch("[^\n]+") do
if api.trim(line) ~= "" then
local key = line:match("^%s*([^:]+):")
key = key and api.trim(key):lower()
if not key or not key:match("^[!#$%%&'*+%.%^_`|~%w%-]+$") or headers[key] then
return nil, translate("Not true format, please re-enter!") .. " (" .. line .. ")"
end
headers[key] = true
end
end
return value:gsub("\n", "\\n")
end
if version_ge_1_14_0 then
-- snell
@@ -549,6 +582,7 @@ o:depends({ tls = true })
o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain"))
o:depends({ tls = true })
@@ -556,6 +590,7 @@ o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
@@ -563,12 +598,14 @@ o:depends({ tls = true })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ tls = true })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
@@ -579,6 +616,7 @@ o:depends({ protocol = "hysteria"})
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
o.default = ""
@@ -608,6 +646,7 @@ o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256")
o:depends({ tls = true })
o:depends({ protocol = "masque" })
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
@@ -616,6 +655,7 @@ o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "ech_config", translate("ECH Config"))
o.default = ""
@@ -812,6 +852,7 @@ o:depends({ transport = "http" })
o:depends({ transport = "ws" })
o:depends({ transport = "httpupgrade" })
o:depends({ protocol = "naive" })
o:depends({ protocol = "masque" })
-- [[ Mux ]]--
o = s:option(Flag, "mux", translate("Mux"))
@@ -31,8 +31,6 @@ local header_type_list = {
"none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns"
}
local xray_version = api.get_app_version("xray")
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("vmess", translate("Vmess"))
o:value("vless", translate("VLESS"))
@@ -41,12 +39,9 @@ o:value("socks", translate("Socks"))
o:value("shadowsocks", translate("Shadowsocks"))
o:value("trojan", translate("Trojan"))
o:value("wireguard", translate("WireGuard"))
if api.compare_versions(xray_version, ">=", "26.1.13") then
o:value("hysteria2", translate("Hysteria2"))
end
if api.compare_versions(xray_version, ">=", "1.8.12") then
o:value("_balancing", translate("Balancing"))
end
o:value("hysteria2", translate("Hysteria2"))
o:value("masque", "MASQUE")
o:value("_balancing", translate("Balancing"))
o:value("_shunt", translate("Shunt"))
o:value("_iface", translate("Custom Interface"))
@@ -260,6 +255,7 @@ o:depends({ protocol = "vless" })
o = s:option(Value, "username", translate("Username"))
o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "masque" })
o = s:option(Value, "password", translate("Password"))
o.password = true
@@ -267,6 +263,57 @@ o:depends({ protocol = "http" })
o:depends({ protocol = "socks" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "masque" })
-- [[ MASQUE ]] --
o = s:option(Flag, "masque_warp", "WARP")
o.default = "0"
o:depends({ protocol = "masque" })
for _, field in ipairs({
{ "masque_warp_private_key", translate("WARP Device Private Key") },
{ "masque_warp_public_key", translate("WARP Endpoint Public Key") }
}) do
o = s:option(TextValue, field[1], field[2])
o.rows = 3
o.wrap = "off"
o:depends({ protocol = "masque", masque_warp = "1" })
o.cfgvalue = function(self, section)
return (m:get(section, self.config_option) or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
return api.trim(value):gsub("\r\n?", "\n"):gsub("\n", "\\n")
end
end
o = s:option(DynamicList, "masque_warp_address", "WARP " .. translate("Local Address"))
o:depends({ protocol = "masque", masque_warp = "1" })
o = s:option(Value, "masque_path", translate("MASQUE Path"))
o:depends({ protocol = "masque" })
o = s:option(TextValue, "masque_headers", translate("Custom HTTP Headers"))
o.rows = 5
o.wrap = "off"
o:depends({ protocol = "masque" })
o.cfgvalue = function(self, section)
return (m:get(section, self.config_option) or ""):gsub("\\n", "\n")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("\r", "\n")
local headers = {}
for line in value:gmatch("[^\n]+") do
if api.trim(line) ~= "" then
local key = line:match("^%s*([^:]+):")
key = key and api.trim(key):lower()
if not key or not key:match("^[!#$%%&'*+%.%^_`|~%w%-]+$") or headers[key] or key == "host" or key == "capsule-protocol" then
return nil, translate("Not true format, please re-enter!") .. " (" .. line .. ")"
end
headers[key] = true
end
end
return value:gsub("\n", "\\n")
end
o = s:option(ListValue, "security", translate("Encrypt Method"))
for a, t in ipairs(security_list) do o:value(t) end
@@ -305,11 +352,7 @@ o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o = s:option(Flag, "hysteria2_realms", translate("Realms"))
o.default = "0"
if api.compare_versions(xray_version, ">", "26.5.9") then
o:depends({ protocol = "hysteria2"})
else
o:depends({ protocol = "__hide"})
end
o:depends({ protocol = "hysteria2"})
o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name")
o:depends({ hysteria2_realms = "1" })
@@ -400,6 +443,7 @@ o:value("http/1.1")
o:value("h2,http/1.1")
o:value("h3,h2,http/1.1")
o:depends({ tls = true, reality = false })
o:depends({ protocol = "masque" })
-- o = s:option(Value, "minversion", translate("minversion"))
-- o.default = "1.3"
@@ -409,21 +453,25 @@ o:depends({ tls = true, reality = false })
o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain"))
o:depends({ tls = true })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") ..
string.format("<a href='javascript:void(0)' onclick='javascript:fetchCertSha256(this)'>%s</a>", "→ " .. translate("Fetch Manually"))
o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name."))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format."))
o.default = ""
@@ -442,6 +490,7 @@ o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "ech_config", translate("ECH Config"))
o.default = ""
@@ -502,6 +551,7 @@ o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384")
o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256")
o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256")
o:depends({ tls = true, reality = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key"))
o.default = ""
@@ -541,10 +591,8 @@ o = s:option(Value, "wireguard_mtu", translate("MTU"))
o.default = "1420"
o:depends({ protocol = "wireguard" })
if api.compare_versions(xray_version, ">=", "1.8.0") then
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
end
o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings."))
o:depends({ protocol = "wireguard" })
o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive"))
o.default = "0"
@@ -701,6 +749,7 @@ o:depends({ transport = "ws" })
o:depends({ transport = "httpupgrade" })
o:depends({ transport = "xhttp" })
o:depends({ transport = "grpc" })
o:depends({ protocol = "masque" })
-- [[ Mux.Cool ]]--
o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool"))
@@ -731,6 +780,7 @@ o:depends({ protocol = "trojan" })
o:depends({ protocol = "shadowsocks" })
o:depends({ protocol = "wireguard" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "finalmask", " ")
o:depends({ use_finalmask = true })
@@ -31,6 +31,7 @@ local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | aw
local local_version = api.get_app_version("sing-box"):match("[^v]+")
local version_ge_1_14_0 = api.compare_versions(local_version, ">=", "1.14.0")
local version_ge_1_15_0 = api.compare_versions(local_version, ">=", "1.15.0")
local ss_method_list = {
"none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305",
@@ -246,6 +247,24 @@ o:value("", translate("Disable"))
o:value("xtls-rprx-vision")
o:depends({ protocol = "vless" , tls = true })
-- [[ MASQUE ]] --
if version_ge_1_15_0 and singbox_tags:find("with_quic") then
s.fields["protocol"]:value("masque", "MASQUE")
s.fields["users"]:depends({ protocol = "masque" })
o = s:option(Value, "masque_path", translate("MASQUE Path"))
o:depends({ protocol = "masque" })
o = s:option(DynamicList, "masque_address", translate("MASQUE Address"))
o.rmempty = false
o:depends({ protocol = "masque" })
o = s:option(Value, "masque_mtu", "MTU")
o.datatype = "uinteger"
o.default = "1280"
o:depends({ protocol = "masque" })
end
o = s:option(Flag, "tls", translate("TLS"))
o.default = 0
o.validate = function(self, value, t)
@@ -322,6 +341,7 @@ o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt"
@@ -331,6 +351,7 @@ o:depends({ protocol = "naive", tls_use_pem = false })
o:depends({ protocol = "hysteria", tls_use_pem = false })
o:depends({ protocol = "tuic", tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o:depends({ protocol = "masque", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -350,6 +371,7 @@ o:depends({ protocol = "naive", tls_use_pem = false })
o:depends({ protocol = "hysteria", tls_use_pem = false })
o:depends({ protocol = "tuic", tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o:depends({ protocol = "masque", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -361,7 +383,7 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -374,7 +396,7 @@ o.validate = function(self, value)
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -394,6 +416,7 @@ o:depends({ protocol = "naive" })
o:depends({ protocol = "hysteria" })
o:depends({ protocol = "tuic" })
o:depends({ protocol = "hysteria2", hysteria2_realms = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "ech_key", translate("ECH Key"))
o.default = ""
@@ -65,6 +65,7 @@ o:value("shadowsocks", "Shadowsocks")
o:value("trojan", "Trojan")
o:value("hysteria2", "Hysteria2")
o:value("wireguard", "WireGuard")
o:value("masque", "MASQUE")
o:value("tunnel", "Tunnel")
o:depends({ custom = false })
@@ -85,6 +86,7 @@ o:depends({ protocol = "vless" })
o:depends({ protocol = "trojan" })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "wireguard" })
o:depends({ protocol = "masque" })
o = s:option(ListValue, "d_protocol", translate("Destination protocol"))
o:value("tcp", "TCP")
@@ -187,6 +189,19 @@ o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps"))
o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false })
-- [[ MASQUE ]] --
o = s:option(Value, "masque_path", translate("MASQUE Path"))
o:depends({ protocol = "masque" })
o = s:option(DynamicList, "masque_address", translate("MASQUE Address"))
o.rmempty = false
o:depends({ protocol = "masque" })
o = s:option(Value, "masque_mtu", "MTU")
o.datatype = "uinteger"
o.default = "1280"
o:depends({ protocol = "masque" })
---- [[ TLS ]]
o = s:option(Flag, "tls", "TLS")
o.default = 0
@@ -274,12 +289,14 @@ end
o = s:option(Flag, "tls_use_pem", translate("Use PEM"), translate("Use certificate and private key PEM content."))
o:depends({ tls = true, reality = false })
o:depends({ protocol = "hysteria2" })
o:depends({ protocol = "masque" })
o = s:option(FileUpload, "tls_certificateFile", translate("Path to the certificate file"), translate("as:") .. "/etc/ssl/fullchain.crt")
o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. s.section .. ".crt"
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o:depends({ protocol = "masque", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -296,6 +313,7 @@ o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. s.section .
if o and o:formvalue(s.section) then o.default = o:formvalue(s.section) end
o:depends({ tls = true, reality = false, tls_use_pem = false })
o:depends({ protocol = "hysteria2", tls_use_pem = false })
o:depends({ protocol = "masque", tls_use_pem = false })
o.validate = function(self, value, t)
if value and value ~= "" then
if not api.fs.access(value) then
@@ -307,7 +325,7 @@ o.validate = function(self, value, t)
return nil
end
o = s:option(TextValue, "tls_certificate", "TLS Certificate (PEM)", translate("Full certificate (chain), PEM format."))
o = s:option(TextValue, "tls_certificate", translate("TLS Certificate (PEM)"), translate("Full certificate (chain), PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -320,7 +338,7 @@ o.validate = function(self, value)
return value:gsub("\n", "\\n")
end
o = s:option(TextValue, "tls_key", "TLS Private Key (PEM)", translate("Private key in PEM format."))
o = s:option(TextValue, "tls_key", translate("TLS Private Key (PEM)"), translate("Private key in PEM format."))
o.default = ""
o.rows = 5
o.wrap = "off"
@@ -336,6 +354,7 @@ end
o = s:option(Flag, "ech", translate("ECH"))
o.default = "0"
o:depends({ tls = true, reality = false })
o:depends({ protocol = "masque" })
o = s:option(TextValue, "ech_key", translate("ECH Key"))
o.default = ""
@@ -114,8 +114,8 @@ function gen_outbound(flag, node, tag, proxy_table)
local proxy_tag, fragment, record_fragment
if proxy_table ~= nil and type(proxy_table) == "table" then
proxy_tag = proxy_table.tag or nil
fragment = (proxy_table.fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
fragment = (proxy_table.fragment and node.protocol ~= "naive" and node.protocol ~= "masque" and not node.hysteria2_realms) and true or nil
record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and node.protocol ~= "masque" and not node.hysteria2_realms) and true or nil
end
if node.type ~= "sing-box" then
@@ -233,7 +233,14 @@ function gen_outbound(flag, node, tag, proxy_table)
end
local tls = nil
if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" then
if node.protocol == "masque" then
node.alpn = nil
node.utls = nil
node.reality = nil
node.transport = nil
node.mux = nil
end
if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" or node.protocol == "masque" then
node.tls = "1"
end
if node.tls == "1" then
@@ -254,7 +261,7 @@ function gen_outbound(flag, node, tag, proxy_table)
fragment = fragment,
record_fragment = record_fragment,
certificate = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and api.split(node.tls_certificate_pem:gsub("\\n", "\n"), "\n") or nil,
cipher_suites = (node.cipherSuites and node.cipherSuites ~= "") and api.split(node.cipherSuites, ":") or nil,
cipher_suites = (node.cipherSuites and node.cipherSuites ~= "") and (type(node.cipherSuites) == "table" and node.cipherSuites or api.split(node.cipherSuites, ":")) or nil,
ech = (node.ech == "1") and (function()
local function get_ech_domain(s) --兼容xray "域名+DNS" 格式ech
local domain, dns = s:match("^([^+]+)%+(.+)$")
@@ -690,6 +697,25 @@ function gen_outbound(flag, node, tag, proxy_table)
}
end
if node.protocol == "masque" then
local headers = {}
if node.user_agent and node.user_agent ~= "" then
headers["user-agent"] = { node.user_agent }
end
for line in (node.masque_headers or ""):gsub("\\n", "\n"):gmatch("[^\r\n]+") do
local key, value = line:match("^%s*([^:]+):%s*(.-)%s*$")
if key then headers[api.trim(key):lower()] = { value } end
end
protocol_table = {
type = "masque-client",
username = not headers.authorization and node.username or nil,
password = not headers.authorization and node.password or nil,
path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil,
headers = next(headers) and headers or nil,
tls = tls
}
end
if node.protocol == "snell" then
protocol_table = {
version = tonumber(node.snell_version),
@@ -719,6 +745,11 @@ function gen_config_server(node)
{ type = "direct", tag = "direct" }
}
if node.protocol == "masque" then
node.tls = "1"
node.reality = nil
end
local tls = {
enabled = true,
certificate_path = (node.tls_use_pem ~= "1") and node.tls_certificateFile or nil,
@@ -830,7 +861,7 @@ function gen_config_server(node)
local user = api.uci_get_s(v) or {}
if user[".type"] == "user" then
local u = {}
if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" then
if node.protocol == "mixed" or node.protocol == "socks" or node.protocol == "http" or node.protocol == "naive" or node.protocol == "masque" then
u.username = user.username
u.password = user.password
end
@@ -877,6 +908,18 @@ function gen_config_server(node)
local protocol_table = nil
if node.protocol == "masque" then
tls.alpn = nil
protocol_table = {
type = "masque-server",
users = users,
path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil,
address = node.masque_address,
mtu = tonumber(node.masque_mtu or 1280),
tls = tls
}
end
if node.protocol == "mixed" then
protocol_table = {
users = users,
@@ -1071,8 +1114,7 @@ function gen_config_server(node)
end
end
if node.protocol == "wireguard" then
inbound.listen = nil
if node.protocol == "wireguard" or node.protocol == "masque" then
table.insert(endpoints, inbound)
else
table.insert(inbounds, inbound)
@@ -1146,6 +1188,12 @@ function gen_config_server(node)
end
end
end
for i = #config.outbounds, 1, -1 do
if config.outbounds[i].type == "masque-client" then
endpoints[#endpoints + 1] = config.outbounds[i]
table.remove(config.outbounds, i)
end
end
return config
end
@@ -2341,6 +2389,9 @@ function gen_config(var)
}
endpoints[#endpoints + 1] = endpoint
table.remove(config.outbounds, i)
elseif value.type == "masque-client" then
endpoints[#endpoints + 1] = value
table.remove(config.outbounds, i)
end
end
if #endpoints > 0 then
@@ -145,6 +145,11 @@ function gen_outbound(flag, node, tag, proxy_table)
node.transport = "hysteria"
node.stream_security = "tls"
end
if node.protocol == "masque" then
node.transport = "masque"
node.stream_security = "tls"
node.utls = nil
end
if node.protocol == "http" and node.stream_security == "tls" then
node.transport = "raw"
@@ -162,12 +167,12 @@ function gen_outbound(flag, node, tag, proxy_table)
_flag_proxy_tag = proxy_tag,
tag = tag,
protocol = node.protocol,
mux = {
mux = (node.protocol ~= "masque") and {
enabled = (node.mux == "1") and true or false,
concurrency = (node.mux == "1" and ((node.mux_concurrency) and tonumber(node.mux_concurrency) or -1)) or nil,
xudpConcurrency = (node.mux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil
} or nil,
streamSettings = (node.streamSettings or dialer_proxy_tag or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria" or node.protocol == "http") and {
streamSettings = (node.streamSettings or dialer_proxy_tag or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan" or node.protocol == "hysteria" or node.protocol == "http" or node.protocol == "masque") and {
sockopt = {
mark = 255,
domainStrategy = node.domain_strategy or "UseIP",
@@ -277,6 +282,28 @@ function gen_outbound(flag, node, tag, proxy_table)
return api.cleanEmptyTables(extra)
end)()
} or nil,
masqueSettings = (node.protocol == "masque") and {
path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil,
warp = (node.masque_warp == "1") and {
privateKey = (node.masque_warp_private_key or ""):gsub("\\n", "\n"),
publicKey = (node.masque_warp_public_key or ""):gsub("\\n", "\n"),
address = node.masque_warp_address
} or nil,
headers = (function()
local headers = {}
if node.masque_warp ~= "1" and ((node.username and node.username ~= "") or (node.password and node.password ~= "")) then
headers["authorization"] = "Basic " .. api.base64Encode((node.username or "") .. ":" .. (node.password or ""))
end
if node.user_agent and node.user_agent ~= "" then
headers["user-agent"] = node.user_agent
end
for line in (node.masque_headers or ""):gsub("\\n", "\n"):gmatch("[^\r\n]+") do
local key, value = line:match("^%s*([^:]+):%s*(.-)%s*$")
if key then headers[api.trim(key):lower()] = value end
end
return next(headers) and headers or nil
end)()
} or nil,
hysteriaSettings = (node.transport == "hysteria") and {
version = 2,
auth = node.hysteria2_auth_password
@@ -521,6 +548,10 @@ function gen_config_server(node)
u.user = user.username
u.pass = user.password
end
if node.protocol == "masque" then
u.email = user.username
u.pass = user.password
end
if node.protocol == "shadowsocks" or node.protocol == "trojan" then
u.email = user.username
u.password = user.password
@@ -591,6 +622,18 @@ function gen_config_server(node)
version = 2,
users = users
}
elseif node.protocol == "masque" then
node.transport = "masque"
node.tls = "1"
node.reality = nil
node.alpn = nil
node.fallback = nil
node.finalmask = nil
settings = {
users = users,
address = node.masque_address,
mtu = tonumber(node.masque_mtu or 1280)
}
elseif node.protocol == "tunnel" then
settings = {
allowedNetwork = node.d_protocol,
@@ -698,6 +741,9 @@ function gen_config_server(node)
streamSettings = {
method = node.transport,
security = "none",
masqueSettings = (node.protocol == "masque") and {
path = (node.masque_path and node.masque_path ~= "") and node.masque_path or nil
} or nil,
tlsSettings = ("1" == node.tls) and {
disableSystemRoot = false,
certificates = {
+40 -10
View File
@@ -2252,11 +2252,26 @@ msgstr "ورودی %s:%s"
msgid "Node: %s:%s, Weight: %s"
msgstr "گره: %s:%s، وزن: %s"
msgid "[Rule update] instance is running; please try again later."
msgstr "نمونه [به‌روزرسانی قانون] در حال اجرا است؛ لطفاً بعداً دوباره امتحان کنید."
msgid "Unable to open the subscription/rule update lock. Exiting."
msgstr "باز کردن قفل به‌روزرسانی اشتراک/قوانین ممکن نشد. خروج."
msgid "[Subscription] instance is running; [Rule Update] queue and wait."
msgstr "نمونه [اشتراک] در حال اجرا است؛ [به‌روزرسانی قانون] در صف انتظار قرار می‌گیرد."
msgid "A subscription or rule update is already running. Skipping this request."
msgstr "به‌روزرسانی اشتراک یا قوانین در حال اجرا است. این درخواست نادیده گرفته می‌شود."
msgid "Unable to acquire the update lock. Scheduled task failed."
msgstr "دریافت قفل به‌روزرسانی ممکن نشد. وظیفه زمان‌بندی‌شده ناموفق بود."
msgid "Subscription"
msgstr "اشتراک"
msgid "Rule Update"
msgstr "به‌روزرسانی قوانین"
msgid "Subscription/Rule Update"
msgstr "به‌روزرسانی اشتراک/قوانین"
msgid "[%s] is running; [%s] is queued and waiting..."
msgstr "[%s] در حال اجرا است؛ [%s] در صف قرار گرفته و منتظر است..."
msgid "Start updating the rules..."
msgstr "شروع به‌روزرسانی قوانین..."
@@ -2414,11 +2429,11 @@ msgstr "بدون پروکسی"
msgid "Proxy"
msgstr "پروکسی"
msgid "[Subscription] instance is running; please try again later."
msgstr "نمونه [اشتراک] در حال اجرا است؛ لطفاً بعداً دوباره امتحان کنید."
msgid "Unable to acquire the stop lock. Exiting."
msgstr "دریافت قفل توقف ممکن نشد. خروج."
msgid "[Rule Update] instance is running; [Subscription] queue and wait."
msgstr "نمونه [به‌روزرسانی قانون] در حال اجرا است؛ [اشتراک] در صف انتظار قرار دارد."
msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled."
msgstr "انتظار برای به‌روزرسانی اشتراک/قوانین ناموفق بود یا مهلت آن به پایان رسید. راه‌اندازی مجدد لغو شد."
msgid "Start subscribing..."
msgstr "شروع اشتراک‌گیری..."
@@ -2432,8 +2447,8 @@ msgstr "اشتراک: [%s] بدون تغییر، نیازی به به‌روزر
msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]"
msgstr "[%s] اشتراک ناموفق بود. این می‌تواند به دلیل آدرس اشتراک نامعتبر یا مشکل شبکه باشد. لطفاً مشکل را بررسی کنید! [%s]"
msgid "Error, restoring service."
msgstr "خطا، بازیابی سرویس."
msgid "An error occurred during subscription."
msgstr "هنگام به‌روزرسانی اشتراک خطایی رخ داد."
msgid "Subscription complete..."
msgstr "اشتراک کامل شد..."
@@ -2470,3 +2485,18 @@ msgstr "فهرست مجموعه‌های رمز پشتیبانی‌شده را
msgid "This username already exists."
msgstr "این نام کاربری از قبل وجود دارد."
msgid "MASQUE Path"
msgstr "مسیر MASQUE"
msgid "Custom HTTP Headers"
msgstr "سرآیندهای سفارشی HTTP"
msgid "MASQUE Address"
msgstr "آدرس تونل MASQUE"
msgid "WARP Device Private Key"
msgstr "کلید خصوصی دستگاه WARP"
msgid "WARP Endpoint Public Key"
msgstr "کلید عمومی سرور WARP"
+40 -10
View File
@@ -2253,11 +2253,26 @@ msgstr "Вход %s:%s"
msgid "Node: %s:%s, Weight: %s"
msgstr "Сервер: %s:%s, вес: %s"
msgid "[Rule update] instance is running; please try again later."
msgstr "Экземпляр [Обновление правила] запущен; пожалуйста, попробуйте позже."
msgid "Unable to open the subscription/rule update lock. Exiting."
msgstr "Не удалось открыть файл блокировки обновления подписок и правил. Завершение работы."
msgid "[Subscription] instance is running; [Rule Update] queue and wait."
msgstr "Экземпляр [Подписка] запущен; [Обновление правила] поставить в очередь и ждать."
msgid "A subscription or rule update is already running. Skipping this request."
msgstr "Обновление подписки или правил уже выполняется. Этот запрос пропущен."
msgid "Unable to acquire the update lock. Scheduled task failed."
msgstr "Не удалось получить блокировку обновления. Запланированная задача не выполнена."
msgid "Subscription"
msgstr "Подписка"
msgid "Rule Update"
msgstr "Обновление правил"
msgid "Subscription/Rule Update"
msgstr "Обновление подписки/правил"
msgid "[%s] is running; [%s] is queued and waiting..."
msgstr "[%s] выполняется; [%s] находится в очереди и ожидает..."
msgid "Start updating the rules..."
msgstr "Начало обновления правил..."
@@ -2415,11 +2430,11 @@ msgstr "Без прокси"
msgid "Proxy"
msgstr "Прокси"
msgid "[Subscription] instance is running; please try again later."
msgstr "Экземпляр [Подписки] запущен; пожалуйста, попробуйте позже."
msgid "Unable to acquire the stop lock. Exiting."
msgstr "Не удалось получить блокировку остановки. Завершение работы."
msgid "[Rule Update] instance is running; [Subscription] queue and wait."
msgstr "Экземпляр [Обновление правила] запущен; [Подписка] ставится в очередь и ожидает."
msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled."
msgstr "Ошибка или превышено время ожидания обновления подписок/правил. Перезапуск отменён."
msgid "Start subscribing..."
msgstr "Начало подписки..."
@@ -2433,8 +2448,8 @@ msgstr "Подписка: [%s] изменений нет, обновление
msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]"
msgstr "[%s] Ошибка подписки. Возможно, адрес подписки неверен или есть проблема с сетью. Пожалуйста, проведите диагностику! [%s]"
msgid "Error, restoring service."
msgstr "Ошибка, выполняется восстановление службы."
msgid "An error occurred during subscription."
msgstr "При обновлении подписки произошла ошибка."
msgid "Subscription complete..."
msgstr "Подписка завершена..."
@@ -2471,3 +2486,18 @@ msgstr "Задает список поддерживаемых наборов ш
msgid "This username already exists."
msgstr "Это имя пользователя уже существует."
msgid "MASQUE Path"
msgstr "Путь MASQUE"
msgid "Custom HTTP Headers"
msgstr "Пользовательские HTTP-заголовки"
msgid "MASQUE Address"
msgstr "Адрес туннеля MASQUE"
msgid "WARP Device Private Key"
msgstr "Приватный ключ устройства WARP"
msgid "WARP Endpoint Public Key"
msgstr "Публичный ключ сервера WARP"
+40 -10
View File
@@ -2238,11 +2238,26 @@ msgstr "入口 %s:%s"
msgid "Node: %s:%s, Weight: %s"
msgstr "节点:%s:%s,权重:%s"
msgid "[Rule update] instance is running; please try again later."
msgstr "[规则更新]实例正在运行,请稍后再试..."
msgid "Unable to open the subscription/rule update lock. Exiting."
msgstr "无法打开订阅/规则更新锁,退出。"
msgid "[Subscription] instance is running; [Rule Update] queue and wait."
msgstr "[订阅]实例正在运行,[规则更新]进入队列等待..."
msgid "A subscription or rule update is already running. Skipping this request."
msgstr "订阅或规则更新已在运行,跳过本次请求。"
msgid "Unable to acquire the update lock. Scheduled task failed."
msgstr "无法获取更新锁,定时任务失败。"
msgid "Subscription"
msgstr "订阅"
msgid "Rule Update"
msgstr "规则更新"
msgid "Subscription/Rule Update"
msgstr "订阅/规则更新"
msgid "[%s] is running; [%s] is queued and waiting..."
msgstr "[%s]正在运行,[%s]已进入队列等待..."
msgid "Start updating the rules..."
msgstr "开始更新规则..."
@@ -2400,11 +2415,11 @@ msgstr "不代理"
msgid "Proxy"
msgstr "代理"
msgid "[Subscription] instance is running; please try again later."
msgstr "[订阅]实例正在运行,请稍后再试..."
msgid "Unable to acquire the stop lock. Exiting."
msgstr "无法获取停止锁,退出。"
msgid "[Rule Update] instance is running; [Subscription] queue and wait."
msgstr "[规则更新]实例正在运行,[订阅]进入队列等待..."
msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled."
msgstr "等待订阅/规则更新失败或超时,已取消重启。"
msgid "Start subscribing..."
msgstr "开始订阅..."
@@ -2418,8 +2433,8 @@ msgstr "订阅:【%s】没有变化,无需更新。"
msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]"
msgstr "【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断![%s]"
msgid "Error, restoring service."
msgstr "发生错误, 正在恢复服务。"
msgid "An error occurred during subscription."
msgstr "订阅过程中发生错误。"
msgid "Subscription complete..."
msgstr "订阅完毕..."
@@ -2460,3 +2475,18 @@ msgstr "该用户名已存在。"
msgid "Update Options"
msgstr "更新选项"
msgid "MASQUE Path"
msgstr "MASQUE 路径"
msgid "Custom HTTP Headers"
msgstr "自定义 HTTP 请求头"
msgid "MASQUE Address"
msgstr "MASQUE 隧道地址"
msgid "WARP Device Private Key"
msgstr "WARP 设备私钥"
msgid "WARP Endpoint Public Key"
msgstr "WARP 服务端公钥"
+40 -10
View File
@@ -2244,11 +2244,26 @@ msgstr "入口 %s:%s"
msgid "Node: %s:%s, Weight: %s"
msgstr "節點:%s:%s,權重:%s"
msgid "[Rule update] instance is running; please try again later."
msgstr "[規則更新]實例正在運行,請稍後再試..."
msgid "Unable to open the subscription/rule update lock. Exiting."
msgstr "無法開啟訂閱/規則更新鎖,退出。"
msgid "[Subscription] instance is running; [Rule Update] queue and wait."
msgstr "[訂閱]實例正在運行,[規則更新]進入隊列等待..."
msgid "A subscription or rule update is already running. Skipping this request."
msgstr "訂閱或規則更新已在執行,略過本次請求。"
msgid "Unable to acquire the update lock. Scheduled task failed."
msgstr "無法取得更新鎖,排程任務失敗。"
msgid "Subscription"
msgstr "訂閱"
msgid "Rule Update"
msgstr "規則更新"
msgid "Subscription/Rule Update"
msgstr "訂閱/規則更新"
msgid "[%s] is running; [%s] is queued and waiting..."
msgstr "[%s]正在執行,[%s]已進入佇列等待..."
msgid "Start updating the rules..."
msgstr "開始更新規則..."
@@ -2406,11 +2421,11 @@ msgstr "不代理"
msgid "Proxy"
msgstr "代理"
msgid "[Subscription] instance is running; please try again later."
msgstr "[訂閱]實例正在運行,請稍後再試..."
msgid "Unable to acquire the stop lock. Exiting."
msgstr "無法取得停止鎖,退出。"
msgid "[Rule Update] instance is running; [Subscription] queue and wait."
msgstr "[規則更新]實例正在運行,[訂閱]進入隊列等待..."
msgid "Failed or timed out waiting for subscription/rule updates. Restart cancelled."
msgstr "等待訂閱/規則更新失敗或逾時,已取消重新啟動。"
msgid "Start subscribing..."
msgstr "開始訂閱..."
@@ -2424,8 +2439,8 @@ msgstr "訂閱:【%s】沒有變化,無需更新。"
msgid "[%s] Subscription failed. This could be due to an invalid subscription address or a network issue. Please diagnose the problem! [%s]"
msgstr "【%s】訂閱失敗,可能是訂閱地址無效,或是網絡問題,請診斷![%s]"
msgid "Error, restoring service."
msgstr "發生錯誤, 正在恢復服務。"
msgid "An error occurred during subscription."
msgstr "訂閱過程中發生錯誤。"
msgid "Subscription complete..."
msgstr "訂閱完毕..."
@@ -2462,3 +2477,18 @@ msgstr "配置支援的密碼套件列表。"
msgid "This username already exists."
msgstr "該用戶名已存在。"
msgid "MASQUE Path"
msgstr "MASQUE 路徑"
msgid "Custom HTTP Headers"
msgstr "自訂 HTTP 請求標頭"
msgid "MASQUE Address"
msgstr "MASQUE 隧道位址"
msgid "WARP Device Private Key"
msgstr "WARP 裝置私鑰"
msgid "WARP Endpoint Public Key"
msgstr "WARP 伺服器公鑰"
+49 -38
View File
@@ -10,39 +10,30 @@ LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock
fi
set_lock() {
[ ! -d "$LOCK_PATH" ] && mkdir -p $LOCK_PATH
exec 999>"$LOCK_FILE"
flock -xn 999
mkdir -p "$LOCK_PATH" || return 1
command exec 9>"$LOCK_FILE" || return 1
flock -xn 9
}
set_lock_wait() {
mkdir -p "$LOCK_PATH" || return 1
command exec 9>"$LOCK_FILE" || return 1
flock -x 9
}
unset_lock() {
flock -u 999
rm -rf "$LOCK_FILE"
}
unlock() {
failcount=1
while [ "$failcount" -le 10 ]; do
if [ -f "$LOCK_FILE" ]; then
let "failcount++"
sleep 1s
[ "$failcount" -ge 10 ] && unset_lock
else
break
fi
done
flock -u 9
exec 9>&-
}
boot_func() {
rm -f ${LOCK_PATH}/${CONFIG}_ready.lock
rm -f ${LOCK_PATH}/${CONFIG}_rule_update.lock
rm -f ${LOCK_PATH}/${CONFIG}_subscribe.lock
local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1)
if [ "$delay" -gt 0 ]; then
log_i18n 0 "Start after a delay of %s seconds!" "${delay}"
sleep $delay
fi
restart boot
restart boot || return 1
touch ${LOCK_PATH}/${CONFIG}_ready.lock
}
@@ -51,35 +42,55 @@ boot() {
}
start() {
set_lock
[ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0
$APP_FILE start
local action=${2:-$1}
[ "$action" = "start" ] && action=
set_lock || { log_i18n 0 "The script is already running, do not run it again. Exit."; return 0; }
"$APP_FILE" start "$action" 9>&-
local status=$?
unset_lock
return "$status"
}
stop() {
unlock
set_lock
[ $? == 1 ] && log_i18n 0 "Stop the script and wait for a timeout, then exit without repeating the process." && exit 0
$APP_FILE stop
local action=${2:-$1}
[ "$action" = "stop" ] && action=
set_lock_wait || { log_i18n 0 "Unable to acquire the stop lock. Exiting."; return 1; }
"$APP_FILE" stop "$action" 9>&-
local status=$?
unset_lock
return "$status"
}
restart() {
set_lock
[ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0
rule_lock=${LOCK_PATH}/${CONFIG}_rule_update.lock
sub_lock=${LOCK_PATH}/${CONFIG}_subscribe.lock
local action=${2:-$1}
[ "$action" = "restart" ] && action=
set_lock || {
log_i18n 0 "The script is already running, do not run it again. Exit."
[ "$action" = "boot" ] && return 1
return 0
}
local update_status=1
for i in $(seq 1 300); do
if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then
break
fi
PASSWALL2_UPDATE_LOCK="${LOCK_PATH}/${CONFIG}_sub_rule.lock" lua -e '
local nixio = require "nixio"
local file = nixio.open(os.getenv("PASSWALL2_UPDATE_LOCK"), "a")
if not file then os.exit(2) end
os.exit(file:lock("tlock") and 0 or 1)
'
update_status=$?
[ "$update_status" -ne 1 ] && break
sleep 2
done
rm -f "$rule_lock" "$sub_lock"
$APP_FILE stop
$APP_FILE start $1
if [ "$update_status" -ne 0 ]; then
log_i18n 0 "Failed or timed out waiting for subscription/rule updates. Restart cancelled."
unset_lock
return 1
fi
"$APP_FILE" stop "$action" 9>&-
"$APP_FILE" start "$action" 9>&-
local status=$?
unset_lock
return "$status"
}
reload() {
@@ -591,7 +591,6 @@ start_socks() {
}
clean_crontab() {
[ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return
touch /etc/crontabs/root
#sed -i "/${CONFIG}/d" /etc/crontabs/root >/dev/null 2>&1
sed -i "/$(echo "/etc/init.d/${CONFIG}" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1
@@ -608,11 +607,10 @@ start_crontab() {
[ "$start_daemon" = "1" ] && { $APP_PATH/monitor.sh > /dev/null 2>&1 & }
fi
[ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && {
rm -rf "${LOCK_PATH}/${CONFIG}_cron.lock"
if [ "$1" = "cron" ]; then
log_i18n 0 "The task is currently running automatically as a scheduled task; no reconfiguration of the scheduled task is required."
return
}
fi
clean_crontab
@@ -628,7 +626,7 @@ start_crontab() {
stop_time_mm=$(echo $stop_time_mode | awk -F ':' '{print $2}')
local t="$stop_time_mm $stop_time_hh * * $stop_week_mode"
[ "$stop_week_mode" = "7" ] && t="$stop_time_mm $stop_time_hh * * *"
echo "$t /etc/init.d/$CONFIG stop > /dev/null 2>&1 &" >>/etc/crontabs/root
echo "$t /etc/init.d/$CONFIG stop cron > /dev/null 2>&1 &" >>/etc/crontabs/root
log_i18n 0 "Scheduled tasks: Auto stop service."
fi
@@ -639,7 +637,7 @@ start_crontab() {
start_time_mm=$(echo $start_time_mode | awk -F ':' '{print $2}')
local t="$start_time_mm $start_time_hh * * $start_week_mode"
[ "$start_week_mode" = "7" ] && t="$start_time_mm $start_time_hh * * *"
echo "$t /etc/init.d/$CONFIG start > /dev/null 2>&1 &" >>/etc/crontabs/root
echo "$t /etc/init.d/$CONFIG start cron > /dev/null 2>&1 &" >>/etc/crontabs/root
log_i18n 0 "Scheduled tasks: Auto start service."
fi
@@ -653,7 +651,7 @@ start_crontab() {
if [ "$restart_week_mode" = "8" ]; then
update_loop=1
else
echo "$t /etc/init.d/$CONFIG restart > /dev/null 2>&1 &" >>/etc/crontabs/root
echo "$t /etc/init.d/$CONFIG restart cron > /dev/null 2>&1 &" >>/etc/crontabs/root
fi
log_i18n 0 "Scheduled tasks: Auto restart service."
fi
@@ -716,7 +714,7 @@ start_crontab() {
}
stop_crontab() {
[ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return
[ "$1" = "cron" ] && return
clean_crontab
/etc/init.d/cron restart
#log_i18n 0 "Clear scheduled commands."
@@ -894,7 +892,7 @@ acl_node() {
start() {
busybox pgrep -f ${TMP_PATH}/bin > /dev/null 2>&1 && {
logger -t PW2-RESTART "Upgrade or overload residue is detected, and the subprocess is being called to perform complete cleaning..."
(stop)
(stop "$1")
sleep 2
}
mkdir -p /tmp/etc /tmp/log $TMP_PATH $TMP_BIN_PATH $TMP_SCRIPT_FUNC_PATH $TMP_ROUTE_PATH $TMP_ACL_PATH $TMP_PATH2
@@ -936,10 +934,7 @@ start() {
}
fi
run_process_queue
[ "$1" = "boot" ] && {
rm -f "${LOCK_PATH}/${CONFIG}_cron.lock"
}
start_crontab
start_crontab "$1"
log_i18n 0 "Running complete!"
echolog "\n"
@@ -977,7 +972,7 @@ stop() {
unset V2RAY_LOCATION_ASSET
unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab
stop_crontab "$1"
rm -rf $DEFAULT_DNSMASQ_CONF
rm -rf $DEFAULT_DNSMASQ_CONF_PATH
[ "1" = "1" ] && {
@@ -1004,12 +999,6 @@ stop() {
rm -f ${LOCK_PATH}/${CONFIG}_socks_auto_switch*
rm -f ${LOCK_PATH}/${CONFIG}_lease2hosts*
rm -f ${LOCK_PATH}/${CONFIG}_monitor*
if ! busybox pgrep -af "${CONFIG}/" | grep -q '/subscribe\.lua'; then
rm -f "${LOCK_PATH}/${CONFIG}_subscribe.lock"
fi
if ! busybox pgrep -af "${CONFIG}/" | grep -q '/rule_update\.lua'; then
rm -f "${LOCK_PATH}/${CONFIG}_rule_update.lock"
fi
log_i18n 0 "Clearing and closing related programs and cache complete."
exit 0
}
@@ -1099,9 +1088,9 @@ socks_node_switch)
socks_node_switch $@
;;
start)
start $@
start "$@"
;;
stop)
stop
stop "$@"
;;
esac
@@ -1,7 +1,6 @@
#!/usr/bin/lua
local api = require "luci.passwall2.api"
local appname = api.appname
local fs = api.fs
local log = api.log
local sys = api.sys
@@ -11,6 +10,35 @@ local arg1 = arg[1]
local arg2 = arg[2]
local arg3 = arg[3]
local update_lock = api.nixio.open(api.LOCK_PREFIX .. "_sub_rule.lock", api.nixio.open_flags("rdwr", "creat"))
if not update_lock then
log(0, api.i18n.translate("Unable to open the subscription/rule update lock. Exiting."))
os.exit(1)
end
if not update_lock:lock("tlock") then
if arg3 ~= "cron" then
log(0, api.i18n.translate("A subscription or rule update is already running. Skipping this request."))
update_lock:close()
os.exit(0)
end
local running_task = update_lock:read(1)
update_lock:seek(0, "set")
local running_name = running_task == "s" and api.i18n.translate("Subscription")
or running_task == "r" and api.i18n.translate("Rule Update")
or api.i18n.translate("Subscription/Rule Update")
log(0, api.i18n.translatef("[%s] is running; [%s] is queued and waiting...", running_name, api.i18n.translate("Rule Update")))
if not update_lock:lock("lock") then
log(0, api.i18n.translate("Unable to acquire the update lock. Scheduled task failed."))
update_lock:close()
os.exit(1)
end
end
update_lock:write("r")
update_lock:seek(0, "set")
uci:revert(api.c_config)
math.randomseed(os.time() + math.floor(os.clock() * 1000))
local reboot = 0
local geoip_update = "0"
local geosite_update = "0"
@@ -181,37 +209,10 @@ else
geosite_update = uci_get("@global_rules[0]", "geosite_update") or "1"
end
if geoip_update == "0" and geosite_update == "0" then
os.exit(0)
update_lock:close()
return
end
local function check_instance(action)
local rule_lock = "/var/lock/" .. appname .. "_rule_update.lock"
local sub_lock = "/var/lock/" .. appname .. "_subscribe.lock"
if action == "start" then
math.randomseed(os.time() + math.floor(os.clock() * 1000))
api.nixio.nanosleep(0, math.random(100, 1000) * 1000000)
if fs.access(rule_lock) then
log(0, api.i18n.translatef("[Rule update] instance is running; please try again later.") .. "\n")
os.exit(0)
else
luci.sys.call("touch " .. rule_lock)
end
elseif action == "end" then
luci.sys.call("rm -f " .. rule_lock)
return
end
if fs.access(sub_lock) then
log(0, api.i18n.translatef("[Subscription] instance is running; [Rule Update] queue and wait.") .. "\n")
end
while fs.access(sub_lock) do
api.nixio.nanosleep(2, 0)
end
end
check_instance("start")
log(0, api.i18n.translate("Start updating the rules..."))
local function safe_call(func, err_msg)
xpcall(func, function(e)
@@ -235,19 +236,16 @@ end
uci_set("@global_rules[0]", "geoip_update", geoip_update)
uci_set("@global_rules[0]", "geosite_update", geosite_update)
uci_save(true)
if reboot == 1 then
if arg3 == "cron" then
if not fs.access(api.LOCK_PREFIX .. ".lock") then
sys.call("touch %s_cron.lock" % api.LOCK_PREFIX)
end
end
log(1, api.i18n.translate("Restart the service and apply the new rules."))
uci_set("@global[0]", "flush_set", "1")
uci_save(true, true)
end
uci_save(true)
log(0, api.i18n.translate("The rules have been updated..."))
check_instance("end")
update_lock:close()
if reboot == 1 then
local action = arg3 == "cron" and " cron" or ""
sys.call("/etc/init.d/passwall2 restart" .. action .. " > /dev/null 2>&1 &")
end
@@ -23,6 +23,35 @@ local uci, uci_get, uci_set, uci_del, uci_foreach, uci_save = api.uci, api.uci_g
-- so caching them is worth the effort
local tinsert = table.insert
local ssub, slen, schar, sbyte, sformat, sgsub = string.sub, string.len, string.char, string.byte, string.format, string.gsub
-- Both update scripts use the same lockf lock; the kernel releases it on exit.
if not arg[1] then os.exit(0) end
local update_lock = api.nixio.open(api.LOCK_PREFIX .. "_sub_rule.lock", api.nixio.open_flags("rdwr", "creat"))
if not update_lock then
log(0, i18n.translate("Unable to open the subscription/rule update lock. Exiting."))
os.exit(1)
end
-- Scheduled updates wait for the lock; manual requests skip a busy lock.
if not update_lock:lock("tlock") then
if arg[3] ~= "cron" then
log(0, i18n.translate("A subscription or rule update is already running. Skipping this request."))
os.exit(0)
end
local running = ({s = i18n.translate("Subscription"), r = i18n.translate("Rule Update")})[update_lock:read(1)] or i18n.translate("Subscription/Rule Update")
update_lock:seek(0, "set")
log(0, i18n.translatef("[%s] is running; [%s] is queued and waiting...", running, i18n.translate("Subscription")))
if not update_lock:lock("lock") then
log(0, i18n.translate("Unable to acquire the update lock. Scheduled task failed."))
os.exit(1)
end
end
-- Record the running task in the same file: s = subscription, r = rule update.
update_lock:write("s")
update_lock:seek(0, "set")
uci:revert(c_config)
math.randomseed(os.time() + math.floor(os.clock() * 1000))
local restart_required = false
local lyaml = require "lyaml"
local has_ss_rust = api.is_finded("sslocal")
@@ -2487,15 +2516,7 @@ local function update_node(manual)
uci_save(true)
if arg[3] == "cron" then
if not fs.access(api.LOCK_PREFIX .. ".lock") then
luci.sys.call("touch %s_cron.lock" % api.LOCK_PREFIX)
end
end
if manual ~= 1 then
luci.sys.call("/etc/init.d/passwall2 restart > /dev/null 2>&1 &")
end
if manual ~= 1 then restart_required = true end
end
local function parse_link(raw, add_mode, group, sub_cfg)
@@ -2687,54 +2708,29 @@ local execute = function()
end
end
local function check_instance(action)
local sub_lock = api.LOCK_PREFIX .. "_subscribe.lock"
local rule_lock = api.LOCK_PREFIX .. "_rule_update.lock"
if action == "start" then
math.randomseed(os.time() + math.floor(os.clock() * 1000))
api.nixio.nanosleep(0, math.random(100, 1000) * 1000000)
if fs.access(sub_lock) then
log(0, i18n.translatef("[Subscription] instance is running; please try again later.") .. "\n")
os.exit(0)
else
luci.sys.call("touch " .. sub_lock)
uci:revert(c_config)
end
elseif action == "end" then
luci.sys.call("rm -f " .. sub_lock)
return
end
if fs.access(rule_lock) then
log(0, i18n.translatef("[Rule Update] instance is running; [Subscription] queue and wait.") .. "\n")
end
while fs.access(rule_lock) do
api.nixio.nanosleep(2, 0)
end
end
if arg[1] then
check_instance("start")
if arg[1] == "start" then
log(0, i18n.translatef("Start subscribing..."))
xpcall(execute, function(e)
log(1, e)
if arg[1] == "start" then
log(0, i18n.translate("Start subscribing..."))
xpcall(execute, function(e)
log(1, e)
if type(debug) == "table" and type(debug.traceback) == "function" then
log(1, debug.traceback())
log(1, i18n.translatef("Error, restoring service."))
end)
log(0, i18n.translatef("Subscription complete...") .. "\n")
elseif arg[1] == "add" then
local f = assert(io.open("/tmp/links.conf", 'r'))
local raw = f:read('*all')
f:close()
parse_link(raw, "1", arg[2])
update_node(1)
luci.sys.call("rm -f /tmp/links.conf")
elseif arg[1] == "truncate" then
truncate_nodes(arg[2])
end
end
log(1, i18n.translate("An error occurred during subscription."))
end)
log(0, i18n.translate("Subscription complete...") .. "\n")
elseif arg[1] == "add" then
local f = assert(io.open("/tmp/links.conf", 'r'))
local raw = f:read('*all')
f:close()
parse_link(raw, "1", arg[2])
update_node(1)
luci.sys.call("rm -f /tmp/links.conf")
elseif arg[1] == "truncate" then
truncate_nodes(arg[2])
end
check_instance("end")
update_lock:close()
if restart_required then
local action = arg[3] == "cron" and " cron" or ""
luci.sys.call("/etc/init.d/passwall2 restart" .. action .. " > /dev/null 2>&1 &")
end
@@ -22,7 +22,7 @@ do
restart_interval_mode=$(expr "$restart_interval_mode" \* 60)
if [ -n "$restart_week_mode" ]; then
[ "$restart_week_mode" = "8" ] && {
[ "$(expr "$CFG_UPDATE_INT" % "$restart_interval_mode")" -eq 0 ] && { /etc/init.d/$CONFIG restart > /dev/null 2>&1 & }
[ "$(expr "$CFG_UPDATE_INT" % "$restart_interval_mode")" -eq 0 ] && { /etc/init.d/$CONFIG restart cron > /dev/null 2>&1 & }
}
fi
+2
View File
@@ -27,3 +27,5 @@ frontend/.env.*.local
*.orig
*.rej
/*.patch
# frontend/demo-dist/ is intentionally committed for the Hub demo.
+150
View File
@@ -1,5 +1,155 @@
# 변경 기록
## [0.2.29-r1] - 2026-10-17
### 추가
- 추천 사용자 규칙을 적용하는 기능을 제공합니다.
- 공유기 웹사이트의 미리보기 기능을 제공합니다.
### 개선
- 펌웨어 정보에 관리 소프트웨어의 버전을 표시합니다.
## [0.2.28-r9] - 2026-10-17
### 추가
- 추천 규칙의 허용 목록 반영을 확인하고 서버에 결과를 전달합니다.
- 직접 허용한 도메인과 추천 규칙이 추가한 도메인을 구분해 표시할 수 있습니다.
### 호환성
- 기존 직접 추가한 허용 규칙과 오프라인 상태를 보존합니다.
- 확인 정보는 임시 메모리에 보관해 공유기의 저장장치 쓰기를 늘리지 않습니다.
- 확인 정보가 없어도 기존 기기 동기화가 계속 동작합니다.
### 테스트
- 기존 허용 규칙 판정, 동기화 실패·재시도 및 잘못된 확인 응답을 검증합니다.
## [0.2.28-r8] - 2026-10-16
### 추가
- 사용자 규칙 화면에서 추천 사용자 규칙을 확인하고 웹사이트로 이동할 수 있습니다.
- 웹사이트에서 공유기에 적용하거나 해제한 추천 허용 규칙이 자동으로 동기화됩니다.
### 개선
- 추천 규칙으로 추가된 도메인에는 출처를 표시하고, 공유기에서 실수로 삭제하지 않도록 했습니다.
- 직접 추가한 허용 규칙은 추천 규칙을 해제해도 삭제하지 않습니다.
- 일시적인 동기화 실패 시 다음 연결에서 다시 적용합니다.
### 호환성
- 인터넷 연결이 끊기거나 계정을 연결하지 않아도 기존 사용자 규칙은 정상 동작합니다.
### 테스트
- 추천 규칙 추가·해제, 직접 등록한 허용 규칙 보존, 동기화 실패 및 잘못된 응답 처리를 확인합니다.
## [0.2.28-r7] - 2026-10-16
### 개선
- 펌웨어·관리 소프트웨어의 마지막 확인 시간과 SafeShield의 최근 갱신 시간을 `몇 분 전`처럼 알아보기 쉽게 표시합니다.
- 표시된 시간 옆의 정보 아이콘을 누르면 정확한 날짜와 시각을 확인할 수 있습니다.
- 페이지를 새로고침하지 않아도 경과 시간이 자동으로 바뀌며, 오래된 기록은 날짜로 표시합니다.
### 테스트
- 시간 경계값, 기록이 없는 경우, 오래된 기록 및 잘못된 보호 데이터 버전의 처리와 화면 연결을 검증합니다.
## [0.2.28-r6] - 2026-10-16
### 개선
- 설정을 '진단 및 지원', '시간 및 예약', '관리 및 복구' 탭으로 나눴습니다. 진단 화면이 가장 먼저 열립니다.
- 네트워크를 '인터넷 연결'과 'LAN 및 DHCP' 탭으로 나눠 필요한 설정을 더 쉽게 찾을 수 있습니다.
- 대시보드에서 확인할 수 있는 시스템 상태 요약 카드는 설정 화면에서 제거했습니다.
- 탭을 오가더라도 저장하지 않은 입력 내용은 유지됩니다.
### 호환성
- 기존 인터넷·내부 네트워크 바로가기 주소를 계속 사용할 수 있으며, 선택한 탭의 주소를 공유할 수 있습니다.
### 테스트
- 기본 탭, 주소 직접 접속, 이전 주소 호환성, 입력 상태 보존과 모바일 탭 구조를 확인합니다.
## [0.2.28-r5] - 2026-10-16
### 추가
- 새 펌웨어에 포함된 SmartSafeHub 관리 소프트웨어 버전을 업데이트 화면에 표시합니다.
### 개선
- 관리 소프트웨어가 이미 같은 버전일 때는 별도로 업데이트하지 않아도 된다고 안내합니다.
- 펌웨어에 포함된 관리 소프트웨어가 현재 설치 버전보다 오래된 경우 확인 안내를 표시합니다.
- 버전 정보가 제공되지 않는 펌웨어에서는 기존 업데이트 화면을 그대로 유지합니다.
### 테스트
- 펌웨어의 포함 버전 전달과 동일·신규·이전 버전 비교 및 정보 누락 처리를 확인합니다.
## [0.2.28-r4] - 2026-10-13
### 수정
- 사용자 규칙에서 도메인을 추가하면 저장 직후 입력창이 비워지도록 수정했습니다.
- 규칙 저장에 실패한 경우에는 입력한 도메인을 유지해 다시 시도할 수 있습니다.
- DNS 적용 진행 상황은 기존과 같이 별도로 안내합니다.
### 개선
- 도메인을 연속 등록할 때 이전 규칙의 적용 결과가 새로운 안내를 덮어쓰지 않도록 했습니다.
- DNS 적용 상태를 확인할 수 없는 경우에도 저장 성공 여부를 구분해서 안내합니다.
### 테스트
- 허용·차단 목록 모두 저장 성공 시에만 입력창을 초기화하는지 확인합니다.
- DNS 적용을 기다리는 동안에도 입력창을 초기화하고, 저장 실패 시 입력을 유지하는 동작을 확인합니다.
- 여러 규칙을 연속 등록해도 진행 안내가 뒤섞이지 않는지 확인합니다.
## [0.2.28-r3] - 2026-10-11
### 수정
- 공유기 체험 화면의 다크 모드에서 안내 문구와 링크가 잘 보이지 않던 문제를 수정했습니다.
### 추가
- 공유기 체험 메뉴에 최근 활동, 네트워크, Wi-Fi, 사용자 규칙, 업데이트 화면을 추가했습니다.
- 새 화면에서도 예시 데이터로 기능을 살펴볼 수 있도록 했습니다.
### 개선
- 데모의 Wi-Fi QR 코드는 예시 정보만 사용하며 실제 공유기에는 연결하지 않습니다.
- 체험 화면에서는 저장 및 업데이트 설치 같은 변경 작업이 실행되지 않습니다.
### 테스트
- 추가한 데모 메뉴와 데이터 및 실제 공유기 통신 차단을 확인합니다.
## [0.2.28-r2] - 2026-10-10
### 추가
- SmartSafeHub 웹사이트에서 공유기 관리 화면의 대시보드, SafeShield, 연결 기기를 미리 체험할 수 있도록 했습니다.
### 개선
- 데모에서는 실제 공유기에 접속하거나 설정을 변경하지 않도록 분리했습니다.
- 웹사이트 배포에서 자동으로 사용할 수 있도록 데모 실행 파일도 저장소에서 관리합니다.
### 테스트
- 데모 화면에 실제 로그인 및 공유기 통신 코드가 포함되지 않도록 검사합니다.
- 데모를 새로 빌드했을 때 저장소에 등록된 파일과 일치하는지 확인합니다.
## [0.2.28-r1] - 2026-10-10
### 추가
+3 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-smartsafehub
PKG_VERSION:=0.2.28
PKG_RELEASE:=8
PKG_VERSION:=0.2.29
PKG_RELEASE:=9
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
PKG_LICENSE:=GPL-3.0-or-later
@@ -23,6 +23,7 @@ LUCI_PKGARCH:=all
define Package/luci-app-smartsafehub/conffiles
/etc/config/smartsafehub
/etc/smartsafehub/device-credential.json
/etc/smartsafehub/recommended-rules.state
endef
define Package/luci-app-smartsafehub/preinst
+25
View File
@@ -0,0 +1,25 @@
# 공유기 웹사이트 데모 번들 관리
웹사이트(`/router-demo/`)에 표시되는 공유기 대시보드 데모의 **원본 소스와 빌드 결과물**은 이 저장소에서 관리합니다. 실제 공유기 인증/RPC 코드는 데모에 포함되지 않습니다.
```bash
cd frontend
npm ci
npm run build:demo
cd ..
git add frontend/demo-dist
# 기존 코드 변경과 함께 커밋하고 push
```
- `frontend/demo-dist/`는 `.gitignore` 대상이 **아닙니다**. 데모 빌드 결과물(`demo.js`, `demo.css`, 동적으로 생성된 파일)을 **모두 커밋**합니다.
- `.github/workflows/ci.yml`에서 동일한 빌드를 다시 실행하여 커밋된 결과물과의 일치 여부를 검사합니다. 커밋을 누락하거나 파일이 달라지면 CI가 실패합니다.
- **hub** 저장소에서는 `router-demo.ref`에 위 커밋의 전체 40자리 Git SHA를 지정합니다. Hub CI 및 배포 스크립트가 해당 커밋을 가져와 정적 자산으로 설치하므로 Hub 서버에서 Node/Vite 빌드는 필요하지 않습니다.
- 공유기 관리 페이지의 일반 빌드(`npm run build`)와 데모 전용 빌드(`npm run build:demo`)는 서로 독립적입니다.
## 공개 데모 화면
- 대시보드, 최근 활동, 네트워크(WAN/LAN), Wi-Fi/게스트 Wi-Fi, 연결된 기기, SafeShield, 사용자 규칙, 업데이트를 체험할 수 있습니다.
- 저장, 차단 규칙 수정, WAN 재연결, 업데이트 확인/설치 등은 **실제 요청을 보내지 않고** 안내 메시지만 보여줍니다.
- QR 코드도 실제 자격 정보 요청 대신 **데모 전용 예시 Wi-Fi 정보**로 생성합니다. 실제 공유기에서는 기존 인증된 RPC를 계속 사용합니다.
- `frontend/src/app/routes.ts`의 `DEMO_ROUTES`는 데모 메뉴 및 허용 경로의 단일 기준입니다.
- 화면이나 데모 데이터를 수정하면 `npm run build:demo`로 데모 빌드 파일을 로컬에서 갱신해 커밋하세요. Hub는 고정 Git SHA로 해당 파일을 가져옵니다.
@@ -0,0 +1,3 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{:host,*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:Line truncated
/*$vite$:1*/
@@ -0,0 +1,4 @@
var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])&&n.__e)return n.__e;return typeof e.type!=`function`||e.props.__P?null:b(e)}function x(e){if((e=e.__)&&e.__c&&!e.props.__P)return e.__e=null,e.__k.some(function(t){return t&&(e.__e=t.__e)}),x(e)}function S(t){(8&t.__g||!(t.__g|=8)||!n.push(t)||i++)&&r==e.debounceRendering||((r=e.debounceRendering)||queueMicrotask)(C)}function C(){var t,r,o,s,c,l,u,d,f;try{for(r=1;n.length;)n.length>r&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),M(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,te(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;p<b;p++)(h=n.__k[p])!=null&&(m=~h.__i&&y[h.__i]||u,h.__i=p,v=M(e,h,m,i,a,o,s,c,l,f),g=h.__e,(m.ref!=h.ref||8&m.__u)&&(m.ref&&re(m.ref,null,h,m),h.ref&&f.push(h.ref,h.__c||g,h)),_=_||g,4&h.__u?(c=E(h,c,e,!m.__v),m.__e&&(m.__e=null)):typeof h.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),h.__u&=-7);return n.__e=_,c}function T(e,t,n,r,i){var a,o,s,c,l,u,d,f,m,h,g=n.length,y=g,x=0,S=!1,C=e.__k=Array(i);for(a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o!=`object`||o.constructor==String?o=C[a]=_(null,o):p(o)?o=C[a]=_(v,{children:o}):o.constructor===void 0&&o.__b?o=C[a]=_(o.type,o.props,o.key,o.ref,o.__v):C[a]=o,c=a+x,o.__=e,o.__b=e.__b+1,s=null,~(l=o.__i=O(o,n,c,y))&&(y--,(s=n[l])&&(s.__u|=2)),s&&s.__v?(o.__u|=2,l==c-1?x--:l==c+1?x++:l!=c&&(l>c?x--:x++,S=!0)):(~l||(i>g?x--:i<g&&x++),typeof o.type!=`function`&&(o.__u|=4))):C[a]=null;if(S){for(u=[],d=[],a=0;a<i;a++)if((o=C[a])&&2&o.__u){for(f=0,m=u.length;f<m;)u[h=f+m>>1]<o.__i?f=h+1:m=h;u[f]=o.__i,d[a]=f+1}for(x=u.length;a--;)d[a]&&(d[a]==x?x--:C[a].__u|=4)}if(y)for(a=0;a<g;a++)!(s=n[a])||2&s.__u||(s.__e==r&&(r=b(s)),ie(s,s));return r}function E(e,t,n,r){var i,a,o;if(typeof e.type==`function`){if(e.props.__P)return t;if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&(i[a].__=e,t=E(i[a],t,n,!1));return t}for(t&&!t.parentNode&&(t=b(e))&&!t.parentNode&&(t=null),o=t;o&&o.nodeType==8;)o=o.nextSibling;for(e.__e!=o&&(!r&&n.moveBefore&&e.__e.parentNode?n.moveBefore(e.__e,t):n.insertBefore(e.__e,t||null)),t=e.__e;(t=t&&t.nextSibling)&&t.nodeType==8;);return t}function D(e,t){return t=t||[],e!=null&&typeof e!=`boolean`&&(p(e)?e.some(function(e){D(e,t)}):t.push(e)),t}function O(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l&&!(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++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(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||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,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 j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]<this.__a[t])return;return r(e.event?e.event(n):n)}}}function M(t,n,r,i,a,o,s,c,l,u){var f,m,_,x,S,C,T,E,D,O,k,A,j,M,te,re,oe,se,ce,le,N=n.type;if(n.constructor!==void 0)return null;if(128&r.__u&&(l=32&r.__u,f=r.__c.__z)){if(n.__u|=l,m=o=[],f.nodeType==8)for(_=1,x=f.nextSibling;x;x=x.nextSibling){if(x.nodeType==8){if(x.data.startsWith(`$s`))_++;else if(x.data.startsWith(`/$s`)&&!--_)break}o.push(x)}else o.push(f);c=o[0]}(f=e.__b)&&f(n);n:if(typeof N==`function`){S=s.length;try{if(O=n.props,k=(f=N.prototype)&&f.render,A=(f=N.contextType)&&i[f.__c],j=f?A?A.props.value:f.__:i,r.__c?2&(C=n.__c=r.__c).__g&&(C.__g|=1):(k?n.__c=C=new N(O,j):(n.__c=C=new y(O,j),C.constructor=N,C.render=ae),A&&A.sub(C),C.state||(C.state={}),C.__n=i,C.__g|=8,C.__h=[],C._Line truncated
`),l]),i&&!t?L(Rn,{}):n&&!t?L(zn,{message:n,onRetry:c}):t?L(v,{children:[L(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,children:L(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[L(`div`,{class:`flex min-w-0 items-start gap-3 sm:gap-4`,children:[L(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:L(pt,{class:`size-7`})}),L(`div`,{class:`min-w-0`,children:[L(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Local overrides`}),L(`h2`,{class:`mt-2 mb-0 break-words text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:`사용자 도메인 규칙`}),L(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-600`,children:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`})]})]}),L(`dl`,{class:`m-0 grid grid-cols-3 gap-2 sm:min-w-80`,children:[L(`div`,{class:`rounded-xl bg-emerald-50 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-emerald-700`,children:`허용`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-emerald-950`,children:t.counts.allow})]}),L(`div`,{class:`rounded-xl bg-red-50 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-red-700`,children:`차단`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-red-950`,children:t.counts.block})]}),L(`div`,{class:`rounded-xl bg-slate-100 px-3 py-3 text-center`,children:[L(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-slate-600`,children:`전체`}),L(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:t.counts.total})]})]})]})}),L(`section`,{class:`mt-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:L(`div`,{class:`flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center`,children:[L(`div`,{class:`min-w-0`,children:[L(`div`,{class:`flex flex-wrap items-center gap-2`,children:[L(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:L(B,{class:`size-4`})}),L(`h2`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`추천 사용자 규칙`}),f&&u.length>0?L(`span`,{class:`rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:[`추천으로 추가 `,u.length,`개`]}):null]}),L(`div`,{class:`mt-2 flex flex-wrap items-center gap-2`,"aria-label":`추천 규칙 분류`,children:[L(`span`,{class:`inline-flex items-center rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:`공식 추천`}),L(`span`,{class:`inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-bold text-slate-700`,children:`커뮤니티`})]}),L(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`SmartSafeHub의 공식 추천과 관리자가 검토한 커뮤니티 허용 규칙을 확인하고, 필요한 규칙을 적용해 보세요.`}),L(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`공유기 선택과 적용 관리는 SmartSafeHub 웹사이트에서 진행합니다. 적용 결과는 다음 기기 동기화 후 반영되며, 연결된 모든 기기에 영향을 줍니다.`})]}),L(`a`,{class:`inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-teal-700 bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 sm:w-auto`,href:`https://www.smartsafehub.com/dashboard/recommended-rules/`,target:`_blank`,rel:`noopener noreferrer`,children:`추천 규칙 보기 ↗`})]})}),t.applyLocalOverrides?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[L(V,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:L(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[L(Ue,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),L(`div`,{children:[L(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),L(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?L(`section`,{"aria-live":`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border border-teal-200 Line truncated
`)>-1?e.split(`
`):e}function Ef(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 Df(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=gs(t.bodyFont),l=gs(t.titleFont),u=gs(t.footerFont),d=a.length,f=i.length,p=r.length,m=hs(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,J(e.title,y),n.font=c.string,J(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,J(r,e=>{J(e.before,y),J(e.lines,y),J(e.after,y)}),v=0,n.font=u.string,J(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Of(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function kf(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 Af(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`),kf(l,e,t,n)&&(l=`center`),l}function jf(e,t,n){let r=n.yAlign||t.yAlign||Of(e,n);return{xAlign:n.xAlign||t.xAlign||Af(e,t,n,r),yAlign:r}}function Mf(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Nf(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Pf(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}=ms(o),m=Mf(t,s),h=Nf(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:io(m,0,r.width-t.width),y:io(h,0,r.height-t.height)}}function Ff(e,t,n){let r=hs(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function If(e){return wf([],Tf(e))}function Lf(e,t,n){return ys(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Rf(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var zf={beforeTitle:ma,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:ma,beforeBody:ma,beforeLabel:ma,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 U(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:ma,afterBody:ma,beforeFooter:ma,footer:ma,afterFooter:ma};function Bf(e,t,n,r){let i=e[t].call(n,r);return i===void 0?zf[t].call(n,r):i}var Vf=class extends Mu{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 Hc(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Lf(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=Bf(n,`beforeTitle`,this,e),i=Bf(n,`title`,this,e),a=Bf(n,`afterTitle`,this,e),o=[];return o=wf(o,Tf(r)),o=wf(o,Tf(i)),o=wf(o,Tf(a)),o}getBeforeBody(e,t){return If(Bf(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];return J(e Line truncated
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "luci-app-smartsafehub-frontend",
"version": "0.2.28",
"version": "0.2.29",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "luci-app-smartsafehub-frontend",
"version": "0.2.28",
"version": "0.2.29",
"dependencies": {
"chart.js": "4.5.1",
"preact": "11.0.1"
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "luci-app-smartsafehub-frontend",
"private": true,
"version": "0.2.28",
"version": "0.2.29",
"type": "module",
"engines": {
"node": ">=24.0"
@@ -10,6 +10,7 @@
"dev": "vite --host 0.0.0.0",
"typecheck": "tsc --noEmit",
"build": "npm run typecheck && vite build",
"build:demo": "npm run typecheck && vite build --config vite.demo.config.ts",
"preview": "vite preview --host 0.0.0.0"
},
"dependencies": {
@@ -28,6 +28,7 @@ import type {
SoftwareUpdateStatus,
} from '../types/updates';
import type {
WifiQrCredentials,
GuestWifiSummary,
GuestWifiUpdateInput,
GuestWifiUpdateResult,
@@ -87,6 +88,15 @@ export interface DeviceRegistrationStatus {
lastSuccessAt: number;
}
export interface RecommendedRulesStatus {
revision: string | null;
domains: string[];
}
export function fetchRecommendedRulesStatus(): Promise<RecommendedRulesStatus> {
return callApi(API_OBJECT, 'device_recommended_rules_status', {}, { timeoutMs: 5000 });
}
export function fetchDeviceRegistrationStatus(): Promise<DeviceRegistrationStatus> {
return callApi(API_OBJECT, 'device_registration_status', {}, { timeoutMs: 5000 });
}
@@ -309,11 +319,7 @@ export function updateIptvSettings(
);
}
export interface WifiQrCredentials {
ssid: string;
security: 'none' | 'psk2' | 'sae' | 'sae-mixed';
password: string;
}
export type { WifiQrCredentials } from '../types/wifi';
export function fetchWifiQr(section: string): Promise<WifiQrCredentials> {
return callApi(API_OBJECT, 'wifi_qr', { section }, { timeoutMs: 8000 });
+29 -6
View File
@@ -1,14 +1,16 @@
import type { ComponentChildren } from 'preact';
import { useEffect } from 'preact/hooks';
import { initializeSystemTimezone } from '../api/smartsafehub';
import { fetchGuestWifiQr, fetchRecommendedRulesStatus, fetchWifiQr, initializeSystemTimezone } from '../api/smartsafehub';
import { AppShell } from '../components/AppShell';
import { PageTabs } from '../components/PageTabs';
import { useActivityHistory } from '../hooks/useActivityHistory';
import { useConfigurationBackup } from '../hooks/useConfigurationBackup';
import { useConnectedDevices } from '../hooks/useConnectedDevices';
import { useFirmwareUpdates } from '../hooks/useFirmwareUpdates';
import { useHashRoute } from '../hooks/useHashRoute';
import { usePageTabs } from '../hooks/usePageTabs';
import { useHealth } from '../hooks/useHealth';
import { useIptv } from '../hooks/useIptv';
import { useLan } from '../hooks/useLan';
@@ -45,6 +47,7 @@ interface AppProps {
export function App({ onAdministratorPasswordChanged }: AppProps) {
const route = useHashRoute();
const tabs = usePageTabs();
const configurationBackup = useConfigurationBackup();
const activity = useActivityHistory(route === 'activity');
const status = useStatus(route === 'home' || route === 'settings');
@@ -123,7 +126,22 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
case 'network':
content = (
<div class="min-w-0">
<div aria-label="인터넷 연결 설정" class="min-w-0">
<PageTabs
active={tabs.network === 'internet' ? 'internet' : 'lan'}
label="네트워크 설정 메뉴"
tabs={[
{ id: 'internet', label: '인터넷 연결', href: '#network' },
{ id: 'lan', label: 'LAN 및 DHCP', href: '#network/lan' },
]}
/>
<div
aria-label="인터넷 연결 설정"
aria-labelledby="internet-tab"
class="min-w-0"
hidden={tabs.network !== 'internet'}
id="internet-panel"
role="tabpanel"
>
<WanPage
action={wan.action}
data={wan.data}
@@ -138,7 +156,11 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
</div>
<div
aria-label="내부 네트워크 설정"
class="mt-8 min-w-0 border-t border-slate-200 pt-8"
aria-labelledby="lan-tab"
class="min-w-0"
hidden={tabs.network !== 'lan'}
id="lan-panel"
role="tabpanel"
>
<LanPage
action={lan.action}
@@ -183,6 +205,8 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
onRetry={() => void wifi.refresh()}
onUpdate={wifi.update}
onUpdateGuest={wifi.updateGuest}
onLoadQr={fetchWifiQr}
onLoadGuestQr={fetchGuestWifiQr}
updatingSection={wifi.updatingSection}
/>
);
@@ -251,6 +275,7 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
case 'settings':
content = (
<SettingsPage
activeTab={tabs.settings}
action={systemActions.action}
backupAction={configurationBackup.action}
backupError={configurationBackup.error}
@@ -262,9 +287,6 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
error={status.error}
feedbackError={systemActions.error}
feedbackMessage={systemActions.message}
firmware={firmware.data}
firmwareError={firmware.error}
firmwareLoading={firmware.loading}
health={health.data}
healthActionError={health.actionError}
healthActionMessage={health.actionMessage}
@@ -352,6 +374,7 @@ export function App({ onAdministratorPasswordChanged }: AppProps) {
loading={rules.loading}
onAddRule={rules.addRule}
onDeleteRule={rules.deleteRule}
onLoadRecommendedRulesStatus={fetchRecommendedRulesStatus}
onDismissFeedback={rules.dismissFeedback}
onRetry={() => void rules.refresh()}
/>
@@ -94,6 +94,48 @@ export function formatRelativeTime(timestamp: number, nowTimestamp = Date.now()
return `${Math.floor(elapsedSeconds / 86_400)}일 전`;
}
/** Recent event timestamps use a readable elapsed time; older events retain their date. */
export function formatRecentTimestamp(timestamp: number, nowTimestamp = Date.now() / 1000): string {
if (!Number.isFinite(timestamp) || timestamp <= 0 || timestamp > 8.64e12) {
return '기록 없음';
}
if (!Number.isFinite(nowTimestamp)) {
return formatTimestamp(timestamp);
}
// Large future offsets can happen with a router clock mismatch; do not call them "just now".
if (timestamp > nowTimestamp + 300 || nowTimestamp - timestamp >= 7 * 86_400) {
return formatTimestamp(timestamp);
}
return formatRelativeTime(timestamp, nowTimestamp);
}
/** Artifact versions contain a UTC timestamp, e.g. 20261016T020000Z. */
export function parseArtifactVersionTimestamp(version: string | null): number | null {
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(version ?? '');
if (!match) {
return null;
}
const [, year, month, day, hour, minute, second] = match;
const timestamp = Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
const date = new Date(timestamp);
if (
date.getUTCFullYear() !== Number(year) ||
date.getUTCMonth() + 1 !== Number(month) ||
date.getUTCDate() !== Number(day) ||
date.getUTCHours() !== Number(hour) ||
date.getUTCMinutes() !== Number(minute) ||
date.getUTCSeconds() !== Number(second) ||
timestamp <= 0
) {
return null;
}
return timestamp / 1000;
}
export function formatTimestampInTimezone(
timestamp: number,
zonename: string,
@@ -102,3 +102,8 @@ export const ROUTES: readonly RouteDefinition[] = [
export const ROUTE_BY_NAME = Object.fromEntries(
ROUTES.map((definition) => [definition.route, definition]),
) as Record<AppRoute, RouteDefinition>;
// Public demo exposes only these read-only, fixture-backed screens.
export const DEMO_ROUTES: readonly AppRoute[] = [
'home', 'activity', 'network', 'wifi', 'devices', 'safeshield', 'rules', 'system',
];
@@ -18,6 +18,7 @@ interface AppShellProps {
refreshing: boolean;
route: AppRoute;
updateCount: number;
demo?: boolean;
}
function readSidebarCollapsed(): boolean {
@@ -40,6 +41,7 @@ export function AppShell({
refreshing,
route,
updateCount,
demo = false,
}: AppShellProps) {
const copy = ROUTE_BY_NAME[route];
const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed);
@@ -72,6 +74,7 @@ export function AppShell({
>
<ProductNavigation
accountRegistered={accountRegistered}
demo={demo}
collapsed={sidebarCollapsed}
loading={loading}
onRefresh={onRefresh}
@@ -1,7 +1,10 @@
import { useEffect, useRef, useState } from 'preact/hooks';
import { RelativeTimestamp } from './RelativeTimestamp';
import type { FirmwareAction } from '../hooks/useFirmwareUpdates';
import type { FirmwareStatus } from '../types/firmware';
import type { SoftwareUpdateStatus } from '../types/updates';
import { compareBundledVersion } from '../utils/firmwareBundling';
import {
AlertIcon,
CheckCircleIcon,
@@ -16,6 +19,7 @@ interface FirmwareUpdatesCardProps {
action: FirmwareAction;
actionError: string | null;
data: FirmwareStatus | null;
software: SoftwareUpdateStatus | null;
error: string | null;
loading: boolean;
message: string | null;
@@ -40,16 +44,6 @@ function formatBytes(value: number | null | undefined): string {
return `${(value / 1024).toFixed(1)} KB`;
}
function formatTimestamp(value: number | null): string {
if (!value) {
return '아직 없음';
}
return new Date(value * 1000).toLocaleString('ko-KR', {
dateStyle: 'medium',
timeStyle: 'short',
});
}
function phaseLabel(data: FirmwareStatus): string {
switch (data.phase) {
case 'checking':
@@ -146,6 +140,7 @@ export function FirmwareUpdatesCard({
action,
actionError,
data,
software,
error,
loading,
message,
@@ -180,6 +175,9 @@ export function FirmwareUpdatesCard({
const checking = action === 'check' || data?.phase === 'checking';
const preparing = action === 'prepare' || data?.phase === 'downloading' || data?.phase === 'verifying';
const uploading = action === 'upload' || action === 'validate-upload';
const bundledVersion = data?.release?.bundledSoftware?.smartsafehub ?? null;
const installedVersion = software?.packages.find((item) => item.name === 'luci-app-smartsafehub')?.installedVersion ?? null;
const bundledComparison = compareBundledVersion(bundledVersion, installedVersion);
const uploadSelected = async () => {
if (!selectedFile) {
@@ -380,7 +378,7 @@ export function FirmwareUpdatesCard({
</div>
<div class="bg-white p-4">
<dt class="text-[11px] font-black uppercase tracking-[0.16em] text-slate-400">Last check</dt>
<dd class="mt-2 mb-0 ml-0 text-sm font-black text-slate-950">{formatTimestamp(data.lastCheckAt)}</dd>
<dd class="mt-2 mb-0 ml-0 text-sm font-black text-slate-950"><RelativeTimestamp fallback="아직 없음" timestamp={data.lastCheckAt} /></dd>
</div>
</dl>
</>
@@ -400,6 +398,35 @@ export function FirmwareUpdatesCard({
{data.release.version || 'Latest'}
</span>
</div>
{bundledVersion ? (
<div
class="mt-4 rounded-xl border border-sky-100 bg-white p-4"
data-section="firmware-bundled-software"
>
<div class="flex flex-wrap items-center justify-between gap-2">
<span class="text-xs font-extrabold text-slate-800">포함된 관리 소프트웨어</span>
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-black text-slate-950">{bundledVersion}</span>
{bundledComparison === 'same' ? (
<span class="rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-extrabold text-emerald-700 ring-1 ring-inset ring-emerald-200">
현재 버전과 동일
</span>
) : null}
</div>
</div>
<p class="mt-2 mb-0 text-xs leading-5 text-slate-600">
{bundledComparison === 'same'
? '관리 소프트웨어는 이미 같은 버전입니다. 펌웨어 업데이트는 시스템 구성 요소를 갱신합니다.'
: bundledComparison === 'older'
? `현재 설치된 관리 소프트웨어(${installedVersion})보다 이전 버전이 포함되어 있습니다. 펌웨어 설치 전 버전을 확인해 주세요.`
: bundledComparison === 'newer'
? `현재 설치된 관리 소프트웨어(${installedVersion})보다 새 버전이 포함되어 있습니다.`
: installedVersion
? `현재 설치 버전 ${installedVersion} · 펌웨어 포함 버전 ${bundledVersion}`
: '이 펌웨어에 포함된 관리 소프트웨어 버전입니다.'}
</p>
</div>
) : null}
{data.release.releaseNotes.length ? (
<ul class="mt-4 mb-0 space-y-2 rounded-xl bg-white p-4 pl-9 text-sm leading-6 text-slate-700 ring-1 ring-inset ring-sky-100">
{data.release.releaseNotes.map((note, index) => (
@@ -3,18 +3,20 @@ import { useEffect, useState } from 'preact/hooks';
import { WifiQrDialog } from './WifiQrDialog';
import { AlertIcon, CheckCircleIcon, ChevronDownIcon, QrCodeIcon, RouterIcon } from './Icons';
import type { GuestWifiSummary, GuestWifiUpdateInput } from '../types/wifi';
import type { GuestWifiSummary, GuestWifiUpdateInput, WifiQrCredentials } from '../types/wifi';
export function GuestWifiCard({
guest,
busy,
saving,
onUpdate,
onLoadQr,
}: {
guest: GuestWifiSummary;
busy: boolean;
saving: boolean;
onUpdate: (input: GuestWifiUpdateInput) => Promise<boolean>;
onLoadQr: () => Promise<WifiQrCredentials>;
}) {
// Keep guest settings out of the way until the administrator needs them.
const [expanded, setExpanded] = useState(false);
@@ -169,7 +171,7 @@ export function GuestWifiCard({
{validation && <p class="mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800">{validation}</p>}
</div>
</form>
{showQr && <WifiQrDialog guest section="ssh_guest" onClose={() => setShowQr(false)} />}
{showQr && <WifiQrDialog load={onLoadQr} onClose={() => setShowQr(false)} />}
</>
);
}
@@ -0,0 +1,56 @@
interface PageTab {
id: string;
label: string;
href: `#${string}`;
}
interface PageTabsProps {
active: string;
label: string;
tabs: readonly PageTab[];
}
/** Hash links support bookmarks, back/forward and keyboard navigation. */
export function PageTabs({ active, label, tabs }: PageTabsProps) {
return (
<nav aria-label={label} class="mb-5 min-w-0" role="tablist">
<div class={`grid w-full min-w-0 gap-2 ${tabs.length === 3
? 'grid-cols-3 sm:max-w-[592px]'
: 'grid-cols-2 sm:max-w-[392px]'
}`}>
{tabs.map((tab) => {
const selected = tab.id === active;
return (
<a
aria-controls={`${tab.id}-panel`}
aria-selected={selected}
class={`flex min-h-11 min-w-0 items-center justify-center rounded-xl border px-2 py-2.5 text-center text-[11px] font-extrabold leading-5 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 sm:px-4 sm:text-sm ${
selected
? 'border-teal-200 bg-teal-50 text-teal-800'
: 'border-slate-200 bg-white text-slate-600 hover:border-teal-200 hover:text-teal-700'
}`}
href={tab.href}
id={`${tab.id}-tab`}
key={tab.id}
role="tab"
tabIndex={selected ? 0 : -1}
onKeyDown={(event) => {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
event.preventDefault();
const currentIndex = tabs.findIndex((item) => item.id === tab.id);
const nextIndex = event.key === 'Home' ? 0
: event.key === 'End' ? tabs.length - 1
: (currentIndex + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
const target = event.currentTarget.parentElement?.children[nextIndex] as HTMLAnchorElement | undefined;
target?.focus();
target?.click();
}}
>
{tab.label}
</a>
);
})}
</div>
</nav>
);
}
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'preact/hooks';
import { ROUTE_BY_NAME } from '../app/routes';
import { DEMO_ROUTES, ROUTE_BY_NAME } from '../app/routes';
import type { AppRoute } from '../app/routes';
import { luciAdminUrl } from '../utils/luci';
import {
@@ -37,6 +37,7 @@ interface ProductNavigationProps {
theme: 'dark' | 'light';
title: string;
updateCount: number;
demo?: boolean;
}
const NAVIGATION_GROUPS: readonly {
@@ -99,7 +100,9 @@ function NavigationItems({
collapsed = false,
route,
updateCount,
demo = false,
}: {
demo?: boolean;
accountRegistered: boolean | null;
collapsed?: boolean;
route: AppRoute;
@@ -107,7 +110,7 @@ function NavigationItems({
}) {
return (
<div class={collapsed ? 'space-y-3' : 'space-y-5'}>
{NAVIGATION_GROUPS.map((group, groupIndex) => (
{NAVIGATION_GROUPS.filter((group) => !demo || group.routes.some((name) => DEMO_ROUTES.includes(name))).map((group, groupIndex) => (
<section
aria-label={group.label}
class={
@@ -121,7 +124,7 @@ function NavigationItems({
</p>
)}
<div class="space-y-1">
{group.routes.map((routeName) => {
{group.routes.filter((name) => !demo || DEMO_ROUTES.includes(name)).map((routeName) => {
const item = ROUTE_BY_NAME[routeName];
const active = route === routeName;
return (
@@ -207,6 +210,7 @@ export function ProductNavigation({
theme,
title,
updateCount,
demo = false,
}: ProductNavigationProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const themeLabel = themeActionLabel(theme);
@@ -288,8 +292,8 @@ export function ProductNavigation({
{mobileMenuOpen ? (
<div class="ssh-mobile-menu border-t border-slate-100 py-4" id="smartsafehub-mobile-menu">
<NavigationItems accountRegistered={accountRegistered} route={route} updateCount={updateCount} />
<div class="mt-5 space-y-1 border-t border-slate-100 pt-4">
<NavigationItems accountRegistered={accountRegistered} demo={demo} route={route} updateCount={updateCount} />
{!demo ? <div class="mt-5 space-y-1 border-t border-slate-100 pt-4">
<a
class={`${navigationClass(false)} text-rose-700 hover:bg-rose-50 hover:text-rose-800`}
href={luciAdminUrl('/admin/logout')}
@@ -299,7 +303,7 @@ export function ProductNavigation({
</span>
로그아웃
</a>
</div>
</div> : null}
</div>
) : null}
</nav>
@@ -357,9 +361,9 @@ export function ProductNavigation({
class={`flex-1 overflow-y-auto py-5 ${collapsed ? 'px-2' : 'px-3'}`}
id="smartsafehub-desktop-navigation"
>
<NavigationItems accountRegistered={accountRegistered} collapsed={collapsed} route={route} updateCount={updateCount} />
<NavigationItems accountRegistered={accountRegistered} collapsed={collapsed} demo={demo} route={route} updateCount={updateCount} />
</nav>
<div class={`border-t border-slate-100 ${collapsed ? 'p-2' : 'p-3'}`}>
{!demo ? <div class={`border-t border-slate-100 ${collapsed ? 'p-2' : 'p-3'}`}>
<a
aria-label="SmartSafeHub에서 로그아웃"
class={`${navigationClass(false, collapsed)} mt-1 text-rose-700 hover:bg-rose-50 hover:text-rose-800`}
@@ -371,7 +375,7 @@ export function ProductNavigation({
</span>
{collapsed ? null : '로그아웃'}
</a>
</div>
</div> : null}
</div>
</aside>
</>
@@ -0,0 +1,56 @@
import { useEffect, useState } from 'preact/hooks';
import { formatRecentTimestamp, formatTimestamp } from '../app/format';
interface RelativeTimestampProps {
timestamp: number | null | undefined;
fallback?: string;
}
/** Consistent relative timestamps with an accessible, tappable absolute-time disclosure. */
export function RelativeTimestamp({ timestamp, fallback = '기록 없음' }: RelativeTimestampProps) {
const [nowTimestamp, setNowTimestamp] = useState(() => Date.now() / 1000);
const validTimestamp = typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0 && timestamp <= 8.64e12;
useEffect(() => {
if (!validTimestamp) {
return;
}
setNowTimestamp(Date.now() / 1000);
const timer = window.setInterval(() => setNowTimestamp(Date.now() / 1000), 60_000);
return () => window.clearInterval(timer);
}, [timestamp, validTimestamp]);
if (!validTimestamp || timestamp === null || timestamp === undefined) {
return <span>{fallback}</span>;
}
const exactTime = formatTimestamp(timestamp);
return (
<span class="inline-flex max-w-full flex-wrap items-center gap-1">
<time
class="min-w-0"
dateTime={new Date(timestamp * 1000).toISOString()}
title={exactTime}
>
{formatRecentTimestamp(timestamp, nowTimestamp)}
</time>
<details class="group inline-flex max-w-full flex-wrap items-center gap-1 align-middle">
<summary
aria-label="정확한 날짜 및 시간 확인"
class="inline-flex size-6 cursor-pointer list-none items-center justify-center rounded-full text-slate-400 transition hover:text-teal-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-500 [&::-webkit-details-marker]:hidden"
title="정확한 날짜 및 시간 확인"
>
<svg aria-hidden="true" class="size-3.5" fill="none" stroke="currentColor" strokeWidth="1.8" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" />
<path d="M12 11v5m0-8h.01" strokeLinecap="round" />
</svg>
</summary>
<span class="hidden max-w-full break-words rounded-md bg-slate-100 px-2 py-1 text-xs font-semibold text-slate-700 group-open:inline-block">
{exactTime}
</span>
</details>
</span>
);
}
@@ -1,8 +1,10 @@
import { useEffect, useState } from 'preact/hooks';
import { CustomSelect } from './CustomSelect';
import { RelativeTimestamp } from './RelativeTimestamp';
import { TimeSelect } from './TimeSelect';
import type { SoftwareUpdateAction } from '../hooks/useSoftwareUpdates';
import type { FirmwareStatus } from '../types/firmware';
import type {
SoftwareUpdateChannel,
SoftwareUpdateError,
@@ -10,6 +12,7 @@ import type {
SoftwareUpdateStatus,
} from '../types/updates';
import { isSoftwareUpdateCheckStale } from '../utils/softwareUpdates';
import { compareBundledVersion } from '../utils/firmwareBundling';
import {
AlertIcon,
CheckCircleIcon,
@@ -24,6 +27,7 @@ interface SoftwareUpdatesCardProps {
action: SoftwareUpdateAction;
actionError: string | null;
data: SoftwareUpdateStatus | null;
firmware: FirmwareStatus | null;
error: string | null;
loading: boolean;
message: string | null;
@@ -188,6 +192,7 @@ export function SoftwareUpdatesCard({
action,
actionError,
data,
firmware,
error,
loading,
message,
@@ -226,6 +231,12 @@ export function SoftwareUpdatesCard({
const currentPackage = data?.packages.find((item) => item.name === UPDATE_PACKAGE) ??
data?.packages[0] ??
null;
const firmwareBundledVersion = firmware?.updateAvailable
? firmware.release?.bundledSoftware?.smartsafehub
: null;
const softwareIncludedInFirmware =
Boolean(data?.lastCheckAt && data.updateCount === 0 && !updateCheckStale && currentPackage?.name === UPDATE_PACKAGE) &&
compareBundledVersion(firmwareBundledVersion, currentPackage?.installedVersion) === 'same';
const hasSettingsChanges = (next: Partial<SoftwareUpdateSettingsInput> = {}) => {
if (!data) {
@@ -495,7 +506,7 @@ export function SoftwareUpdatesCard({
Last check
</dt>
<dd class="mt-2 mb-0 ml-0 text-sm font-black text-slate-950">
{formatTimestamp(data.lastCheckAt)}
<RelativeTimestamp fallback="아직 없음" timestamp={data.lastCheckAt} />
</dd>
</div>
<div class="bg-white p-4">
@@ -522,7 +533,7 @@ export function SoftwareUpdatesCard({
<div>
<h3 class="m-0 text-base font-black text-amber-950">업데이트 확인이 지연되고 있습니다.</h3>
<p class="mt-1 mb-0 text-sm leading-6 text-amber-800">
마지막 확인은 {formatTimestamp(data.lastCheckAt)}에 완료되었습니다. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.
마지막 업데이트 확인: <RelativeTimestamp timestamp={data.lastCheckAt} />. 자동 확인 상태를 점검하거나 업데이트 확인을 실행해 주세요.
</p>
</div>
</div>
@@ -539,8 +550,13 @@ export function SoftwareUpdatesCard({
<div>
<h3 class="m-0 text-base font-black text-emerald-950">최신 버전을 사용 중입니다.</h3>
<p class="mt-1 mb-0 text-sm leading-6 text-emerald-800">
마지막 확인은 {formatTimestamp(data.lastCheckAt)}에 완료되었습니다.
마지막 업데이트 확인: <RelativeTimestamp timestamp={data.lastCheckAt} />.
</p>
{softwareIncludedInFirmware ? (
<p class="mt-2 mb-0 text-xs leading-5 text-emerald-800" data-section="software-firmware-match">
새 펌웨어에도 동일한 관리 소프트웨어({firmwareBundledVersion})가 포함되어 있어 별도로 업데이트할 필요가 없습니다.
</p>
) : null}
</div>
</div>
</section>
@@ -13,9 +13,6 @@ export function LoadingPanel() {
<h2 class="m-0 text-lg font-extrabold text-slate-950">
장치 상태를 확인하고 있습니다
</h2>
<p class="mt-1 mb-0 text-sm leading-6 text-slate-600">
OpenWrt의 rpcd API에서 정보를 불러오는 중입니다.
</p>
</div>
</div>
</section>
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'preact/hooks';
import { fetchGuestWifiQr, fetchWifiQr } from '../api/smartsafehub';
import type { WifiQrCredentials } from '../types/wifi';
import { DownloadIcon } from './Icons';
import { createQrMatrix } from '../utils/qrMatrix';
@@ -31,19 +31,19 @@ export function drawWifiQrPng(
}));
}
export function WifiQrDialog({ section, guest = false, onClose }: { section: string; guest?: boolean; onClose: () => void }) {
export function WifiQrDialog({ load, onClose }: { load: () => Promise<WifiQrCredentials>; onClose: () => void }) {
const [qr, setQr] = useState<{ ssid: string; matrix: boolean[][] } | null>(null);
const [error, setError] = useState<string | null>(null);
const [downloadError, setDownloadError] = useState<string | null>(null);
useEffect(() => {
let active = true;
void (guest ? fetchGuestWifiQr() : fetchWifiQr(section)).then((data) => {
void load().then((data) => {
if (!active) return;
try { setQr({ ssid: data.ssid, matrix: createQrMatrix(wifiQrPayload(data.ssid, data.security, data.password)) }); }
catch { setError('QR 코드를 생성할 수 없습니다.'); }
}).catch(() => { if (active) setError('Wi-Fi 정보를 불러오지 못했습니다.'); });
return () => { active = false; setQr(null); };
}, [section, guest]);
}, [load]);
const count = qr?.matrix.length ?? 0;
const downloadQr = () => {
if (!qr) return;
@@ -0,0 +1,182 @@
// Entirely fictional values. Never use a real router address, MAC, credential or token.
import type { ActivityHistory } from '../types/activity';
import type { ConnectedDevicesSummary } from '../types/devices';
import type { FirmwareStatus } from '../types/firmware';
import type { WanSettings } from '../types/wan';
import type { WifiSummary, WifiQrCredentials } from '../types/wifi';
import type { SafeShieldRules } from '../types/rules';
import type { HealthStatus } from '../types/health';
import type { LanSettings } from '../types/lan';
import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield';
import type { SmartSafeHubStatus } from '../types/status';
import type { SoftwareUpdateStatus } from '../types/updates';
const now = Math.floor(Date.now() / 1000);
const hour = 3600;
export const demoDevices: ConnectedDevicesSummary = {
generatedAt: now,
devices: [
{ id: 'demo-phone', hostname: '거실 스마트폰', mac: '02:00:00:00:00:01', ipv4Address: '192.0.2.101', connection: 'wifi', online: true, leaseActive: true, leaseExpiresAt: now + 6 * hour, interface: 'wlan0', ssid: 'SmartSafeHub', radio: 'radio0', band: '5g', bandLabel: '5 GHz', signalDbm: -43, inactiveMs: 250, connectedSeconds: 8300 },
{ id: 'demo-laptop', hostname: '작업용 노트북', mac: '02:00:00:00:00:02', ipv4Address: '192.0.2.102', connection: 'wifi', online: true, leaseActive: true, leaseExpiresAt: now + 8 * hour, interface: 'wlan0', ssid: 'SmartSafeHub', radio: 'radio0', band: '5g', bandLabel: '5 GHz', signalDbm: -57, inactiveMs: 720, connectedSeconds: 29200 },
{ id: 'demo-tv', hostname: '거실 TV', mac: '02:00:00:00:00:03', ipv4Address: '192.0.2.103', connection: 'ethernet', online: true, leaseActive: true, leaseExpiresAt: now + 12 * hour, interface: 'lan1', ssid: null, radio: null, band: null, bandLabel: null, signalDbm: null, inactiveMs: null, connectedSeconds: 140000 },
{ id: 'demo-tablet', hostname: '태블릿', mac: '02:00:00:00:00:04', ipv4Address: '192.0.2.104', connection: 'wifi', online: false, leaseActive: false, leaseExpiresAt: null, interface: null, ssid: 'SmartSafeHub', radio: 'radio1', band: '2g', bandLabel: '2.4 GHz', signalDbm: null, inactiveMs: null, connectedSeconds: null },
],
totals: { known: 4, online: 3, wireless: 2, ethernet: 1, offline: 1 },
};
export const demoActivity: ActivityHistory = {
schema: 1, scope: 'current_boot', volatile: true, maxEvents: 100,
cloud: { enabled: false, phase: 'disabled', eligible: false, plan: 'free', retentionDays: 0, pendingEvents: 0, lastAttemptAt: 0, lastSuccessAt: 0, lastUploadedCount: 0, lastErrorCode: null, nextSyncAt: 0 },
events: [
{ schema: 1, eventId: 'demo-event-1', eventType: 'safeshield.blocklist.updated', severity: 'success', occurredAt: now - 7200, deviceUuid: null, source: 'safeshield', metadata: {} },
{ schema: 1, eventId: 'demo-event-2', eventType: 'system.boot', severity: 'info', occurredAt: now - 86400, deviceUuid: null, source: 'system', metadata: {} },
],
};
export const demoStatus: SmartSafeHubStatus = {
device: { hostname: 'SmartRouter', model: 'ipTIME AX3000SM', boardName: 'iptime,ax3000sm' },
software: { distribution: 'OpenWrt', version: '25.12', revision: 'demo', kernel: '6.12' },
runtime: { uptime: 86400, localtime: now, load: [0.08, 0.12, 0.05], memory: { total: 268435456, free: 83886080, shared: 4194304, buffered: 41943040, available: 171966464, cached: 41943040 } },
network: { available: true, up: true, protocol: 'dhcp', ipv4Address: '198.51.100.20' },
activityHistory: demoActivity,
};
export const demoLan: LanSettings = {
lan: { address: '192.168.1.1', prefixLength: 24, netmask: '255.255.255.0', subnet: '192.168.1.0/24' },
dhcp: { enabled: true, start: '192.168.1.100', end: '192.168.1.249', leaseTime: '12h' },
wan: { connected: true, protocol: 'dhcp', address: '198.51.100.20', prefixLength: 24, subnet: '198.51.100.0/24' },
conflict: { detected: false, wanSubnet: null }, recommendation: null,
};
export const demoSafeShield: SafeShieldStatus = {
available: true, version: '0.3.25', enabled: true, active: true, status: 'idle', stage: null,
summary: { label: '보호 중', message: 'DNS 보호가 정상적으로 동작하고 있습니다.', severity: 'info' },
runtime: { refreshdRunning: true, dnsmasqRunning: true, dnsRuntimeOk: true, lastResult: 'success', lastErrorCode: null },
entitlement: { plan: 'free', status: 'active' },
artifact: { resolved: true, tier: 'light', version: '20261010T010000Z', uniqueDomains: 48000, rules: 48000 },
localOverrides: { enabled: true, allowlistPath: '/etc/safeshield/allowlist', blocklistPath: '/etc/safeshield/blocklist' },
blocklist: { installed: true, validLineCount: 48000, fileSizeKb: 4700, verificationOk: true },
health: { overall: 'ok' }, issueCounts: { warnings: 0, errors: 0 },
timestamps: { lastAttempt: now - 2 * hour, lastSuccess: now - 2 * hour, lastFailure: 0, lastLocalApply: now - 2 * hour, lastLocalApplyFailure: 0, nextRefreshAt: now + 21 * hour, refreshIntervalS: 82800 },
};
const latestHourBucket = Math.floor(now / hour) * hour;
const hourlyProfiles = [
{ queries: 351, blocked: 18 },
{ queries: 297, blocked: 15 },
{ queries: 243, blocked: 11 },
{ queries: 216, blocked: 9 },
{ queries: 203, blocked: 8 },
{ queries: 230, blocked: 10 },
{ queries: 324, blocked: 17 },
{ queries: 567, blocked: 35 },
{ queries: 783, blocked: 54 },
{ queries: 864, blocked: 61 },
{ queries: 756, blocked: 50 },
{ queries: 702, blocked: 43 },
{ queries: 635, blocked: 36 },
{ queries: 580, blocked: 30 },
{ queries: 608, blocked: 33 },
{ queries: 689, blocked: 40 },
{ queries: 837, blocked: 54 },
{ queries: 1026, blocked: 73 },
{ queries: 1256, blocked: 99 },
{ queries: 1458, blocked: 125 },
{ queries: 1512, blocked: 135 },
{ queries: 1323, blocked: 111 },
{ queries: 1026, blocked: 75 },
{ queries: 648, blocked: 39 },
] as const;
const hourly = Array.from({ length: 24 }, (_, i) => {
const bucketStart = latestHourBucket - (23 - i) * hour;
const hourOfDay = new Date(bucketStart * 1_000).getHours();
const profile = hourlyProfiles[hourOfDay]!;
return {
bucketStart,
queries: profile.queries,
blocked: profile.blocked,
};
});
export const demoStatistics: SafeShieldStatistics = {
enabled: true, collectorRunning: true, available: true, schemaVersion: 1, volatile: true,
startedAt: now - 7 * 86400, updatedAt: now - 30, retentionHours: 168,
totals: { queries: hourly.reduce((sum, item) => sum + item.queries, 0), blocked: hourly.reduce((sum, item) => sum + item.blocked, 0) },
hourly, deviceLimit: 128, devicesTruncated: false,
devices: [
{ id: 'demo-phone', mac: '02:00:00:00:00:01', ip: '192.0.2.101', hostname: '거실 스마트폰', identified: true, queries: 3400, blocked: 470 },
{ id: 'demo-laptop', mac: '02:00:00:00:00:02', ip: '192.0.2.102', hostname: '작업용 노트북', identified: true, queries: 2900, blocked: 345 },
{ id: 'demo-tv', mac: '02:00:00:00:00:03', ip: '192.0.2.103', hostname: '거실 TV', identified: true, queries: 1200, blocked: 185 },
],
};
export const demoHealth: HealthStatus = {
schema: 1, generatedAt: now, overall: 'ok',
summary: { message: '모든 주요 항목이 정상입니다.', total: 3, ok: 3, warning: 0, critical: 0, unknown: 0 },
metrics: { memoryAvailablePercent: 64, load1m: 0.08, overlayAvailablePercent: 80 },
checks: [],
reporter: { enabled: false, eligible: false, plan: 'free', entitlementStatus: 'active', lastReportAt: 0, nextReportAt: 0, lastResult: '', lastErrorCode: null },
};
export const demoUpdates: SoftwareUpdateStatus = {
phase: 'idle', updateCount: 0, packages: [], lastCheckAt: now - hour, lastInstallAt: null, lastError: null,
releaseNotes: [], releaseNotesComplete: true,
settings: { channel: 'stable', checkEnabled: true, checkIntervalSeconds: 21600, autoInstall: true, autoInstallTime: '03:00', repositoryHost: 'example.invalid', updatePackage: 'luci-app-smartsafehub' },
};
export const demoFirmware: FirmwareStatus = {
phase: 'idle', source: null, lastCheckAt: now - hour, lastPrepareAt: null, lastError: null, prepared: null,
current: { deviceCode: 'iptime-ax3000sm', boardName: 'iptime,ax3000sm', buildId: 'demo', releaseVersion: '1.0.2', openwrtVersion: '25.12', metadataAvailable: true },
updateAvailable: false, release: null,
settings: { channel: 'stable', apiBaseUrl: 'https://www.smartsafehub.com', checkIntervalSeconds: 21600, autoInstall: false },
};
// The interactive demo deliberately uses only invented, RFC 5737 addresses.
export const demoWan: WanSettings = {
configuration: {
protocol: 'dhcp', supported: true,
pppoe: { username: '', passwordConfigured: false },
static: { address: null, prefixLength: null, netmask: null, gateway: null, dns: [] },
},
status: {
connected: true, pending: false, protocol: 'dhcp', device: 'wan',
address: '198.51.100.20', prefixLength: 24, gateway: '198.51.100.1',
dns: ['1.1.1.1', '8.8.8.8'], uptimeSeconds: 86400,
},
};
export const demoWifi: WifiSummary = {
networks: [
{ section: 'demo_5g', device: 'radio0', band: '5g', bandLabel: '5 GHz',
ssid: 'SmartSafeHub', enabled: true, runtimeUp: true, security: 'sae-mixed',
securityRaw: 'sae-mixed', passwordConfigured: true, channel: '36',
networks: ['lan'], clientCount: 2 },
{ section: 'demo_2g', device: 'radio1', band: '2g', bandLabel: '2.4 GHz',
ssid: 'SmartSafeHub-2G', enabled: true, runtimeUp: true, security: 'sae-mixed',
securityRaw: 'sae-mixed', passwordConfigured: true, channel: '6',
networks: ['lan'], clientCount: 0 },
],
totalClients: 2,
guest: { supported: true, configured: true, enabled: true,
ssid: 'SmartSafeHub-Guest', passwordConfigured: true,
subnet: '192.0.2.0/24', radio: 'radio1' },
};
export const demoRules: SafeShieldRules = {
allow: ['trusted.example', 'updates.example'],
block: ['ads.example', 'tracker.example'],
counts: { allow: 2, block: 2, total: 4 },
safeshieldEnabled: true, applyLocalOverrides: true,
};
// The QR dialog uses an injected demo-only data provider, never the router RPC.
export const demoWifiQr = async (section: string): Promise<WifiQrCredentials> => ({
ssid: demoWifi.networks.find((network) => network.section === section)?.ssid ?? 'SmartSafeHub',
security: 'sae-mixed', password: 'demo-only-2026',
});
export const demoGuestWifiQr = async (): Promise<WifiQrCredentials> => ({
ssid: 'SmartSafeHub-Guest', security: 'sae-mixed', password: 'guest-demo-only-2026',
});
@@ -0,0 +1,154 @@
import { render } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { AppShell } from '../components/AppShell';
import { DEMO_ROUTES } from '../app/routes';
import { ActivityPage } from '../pages/ActivityPage';
import { WanPage } from '../pages/WanPage';
import { LanPage } from '../pages/LanPage';
import { WifiPage } from '../pages/WifiPage';
import { SafeShieldRulesPage } from '../pages/SafeShieldRulesPage';
import { UpdatePage } from '../pages/UpdatePage';
import { useHashRoute } from '../hooks/useHashRoute';
import { usePageTabs } from '../hooks/usePageTabs';
import { PageTabs } from '../components/PageTabs';
import { ConnectedDevicesPage } from '../pages/ConnectedDevicesPage';
import { HomePage } from '../pages/HomePage';
import { SafeShieldPage } from '../pages/SafeShieldPage';
import {
demoActivity, demoDevices, demoFirmware, demoHealth, demoLan,
demoSafeShield, demoStatistics, demoStatus, demoUpdates,
demoWan, demoWifi, demoRules, demoWifiQr, demoGuestWifiQr,
} from './data';
import '../styles/app.css';
// This entry MUST NOT import App, auth/session, or api/*. No backend is involved.
const ALLOWED_ROUTES = new Set(DEMO_ROUTES);
function DemoApp() {
const currentRoute = useHashRoute();
const tabs = usePageTabs();
const route = ALLOWED_ROUTES.has(currentRoute) ? currentRoute : 'home';
const [notice, setNotice] = useState('');
useEffect(() => {
if (!ALLOWED_ROUTES.has(currentRoute)) {
window.history.replaceState(null, '', `${window.location.pathname}${window.location.search}#home`);
setNotice('해당 기능은 현재 데모에서 제공하지 않습니다.');
}
}, [currentRoute]);
const readonlyNotice = () => setNotice('데모에서는 실제 공유기 설정을 변경할 수 없습니다.');
const dismiss = () => setNotice('');
const reset = () => setNotice('예시 데이터가 표시되고 있습니다. 실제 공유기에는 연결되지 않습니다.');
// All callbacks are local-only. Never load router hooks or the authenticated App.
const disabledAction = async (): Promise<boolean> => {
readonlyNotice();
return false;
};
const firmware = {
action: null, actionError: null, data: demoFirmware, error: null,
loading: false, message: null, reconnecting: false, uploadProgress: null,
check: readonlyNotice, discard: disabledAction, dismissFeedback: dismiss,
install: disabledAction, prepare: readonlyNotice, refresh: reset, upload: disabledAction,
};
let content;
switch (route) {
case 'activity':
content = <ActivityPage actionError={null} actionMessage={null} data={demoActivity}
error={null} loading={false} onDismissActionFeedback={dismiss} onRetry={reset}
onSetCloudSync={readonlyNotice} savingCloudSync={false} />;
break;
case 'network':
content = <div class="min-w-0">
<PageTabs active={tabs.network} label="네트워크 설정 메뉴" tabs={[
{ id: 'internet', label: '인터넷 연결', href: '#network' },
{ id: 'lan', label: 'LAN 및 DHCP', href: '#network/lan' },
]} />
<div aria-label="인터넷 연결 설정" aria-labelledby="internet-tab" hidden={tabs.network !== 'internet'} id="internet-panel" role="tabpanel"><WanPage action={null} data={demoWan} error={null}
feedback={null} loading={false} onDismissFeedback={dismiss} onReconnect={readonlyNotice}
onRetry={reset} onSave={disabledAction} /></div>
<div aria-label="내부 네트워크 설정" aria-labelledby="lan-tab" hidden={tabs.network !== 'lan'} id="lan-panel" role="tabpanel">
<LanPage action={null} data={demoLan} error={null} feedback={null} loading={false}
onApplyRecommendation={disabledAction} onDismissFeedback={dismiss} onRetry={reset}
onSave={disabledAction} />
</div>
</div>;
break;
case 'wifi':
content = <WifiPage data={demoWifi} error={null} feedback={null} loading={false}
updatingSection={null} onDismissFeedback={dismiss} onRetry={reset}
onUpdate={disabledAction} onUpdateGuest={disabledAction}
onLoadQr={demoWifiQr} onLoadGuestQr={demoGuestWifiQr} />;
break;
case 'rules':
content = <SafeShieldRulesPage action={null} data={demoRules} error={null}
feedback={null} loading={false} onAddRule={disabledAction} onDeleteRule={disabledAction}
onDismissFeedback={dismiss} onRetry={reset} />;
break;
case 'system':
content = <UpdatePage action={null} actionError={null} data={demoUpdates}
error={null} firmware={firmware} loading={false} message={null}
onCheck={readonlyNotice} onDismissFeedback={dismiss} onInstall={readonlyNotice}
onRetry={reset} onSaveSettings={disabledAction} />;
break;
case 'devices':
content = <ConnectedDevicesPage data={demoDevices} error={null} loading={false} onRetry={reset} />;
break;
case 'safeshield':
content = (
<SafeShieldPage
action={null} actionError={null} actionMessage={null} data={demoSafeShield}
error={null} loading={false} statistics={demoStatistics} statisticsError={null}
statisticsLoading={false} statisticsRefreshing={false} onDismissFeedback={dismiss}
onRefreshBlocklist={readonlyNotice} onRetry={reset} onRetryStatistics={reset}
onSetEnabled={readonlyNotice} onSetStatisticsEnabled={readonlyNotice}
/>
);
break;
default:
content = (
<HomePage
accountRegistered={false} activity={demoActivity} activityError={null} activityLoading={false}
data={demoStatus} devices={demoDevices} devicesError={null} devicesLoading={false}
error={null} firmware={demoFirmware} firmwareError={null} firmwareLoading={false}
health={demoHealth} healthError={null} healthLoading={false}
lan={demoLan} lanError={null} lanLoading={false} loading={false} onRetry={reset}
safeshield={demoSafeShield} safeshieldError={null} safeshieldLoading={false}
statistics={demoStatistics} statisticsError={null} statisticsLoading={false}
statisticsRefreshing={false} updates={demoUpdates} updatesError={null} updatesLoading={false}
/>
);
}
return (
<AppShell accountRegistered={false} demo loading={false} onRefresh={reset} refreshing={false} route={route} updateCount={0}>
<div role="status" class="mb-5 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-teal-200 bg-teal-50 px-4 py-3 text-sm font-semibold text-teal-800">
<span>체험 모드 · 표시되는 기기와 보호 통계는 모두 예시입니다.</span>
<a class="font-extrabold underline" href="/devices/">지원 기기 살펴보기 →</a>
</div>
{notice ? (
<div role="alert" class="mb-5 flex items-center justify-between gap-3 rounded-xl border border-amber-300 bg-amber-50 p-4 text-sm font-semibold text-amber-900">
{notice}
<button aria-label="안내 닫기" class="font-black" onClick={dismiss} type="button">✕</button>
</div>
) : null}
{content}
</AppShell>
);
}
// The real router app mounts into a shadow root too. Isolate Tailwind Preflight
// from Django's global CSS and keep the same actual router components/styling.
const host = document.getElementById('smartsafehub-demo-root');
if (host) {
const shadow = host.attachShadow({ mode: 'open' });
const stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = host.dataset.demoCss || '/static/router-demo/demo.css';
shadow.appendChild(stylesheet);
const mount = document.createElement('div');
mount.className = 'smartsafehub-shadow-root';
shadow.appendChild(mount);
render(<DemoApp />, mount);
}
@@ -6,6 +6,8 @@ const HASH_ROUTES: Readonly<Record<string, AppRoute>> = {
'#home': 'home',
'#activity': 'activity',
'#network': 'network',
'#network/internet': 'network',
'#network/lan': 'network',
'#wan': 'network',
'#lan': 'network',
'#wifi': 'wifi',
@@ -16,6 +18,9 @@ const HASH_ROUTES: Readonly<Record<string, AppRoute>> = {
'#account': 'account',
'#system': 'system',
'#settings': 'settings',
'#settings/diagnostics': 'settings',
'#settings/time': 'settings',
'#settings/management': 'settings',
};
function routeFromHash(): AppRoute {
@@ -0,0 +1,36 @@
import { useEffect, useState } from 'preact/hooks';
export type NetworkTab = 'internet' | 'lan';
export type SettingsTab = 'diagnostics' | 'time' | 'management';
// The parent sidebar still owns #network / #settings. The hashes below select
// a panel without introducing new sidebar routes, and preserve old WAN/LAN URLs.
export function networkTabFromHash(hash: string): NetworkTab {
return hash === '#lan' || hash === '#network/lan' ? 'lan' : 'internet';
}
export function settingsTabFromHash(hash: string): SettingsTab {
if (hash === '#settings/time') return 'time';
if (hash === '#settings/management') return 'management';
return 'diagnostics';
}
function readTabs() {
const hash = window.location.hash;
return {
network: networkTabFromHash(hash),
settings: settingsTabFromHash(hash),
};
}
export function usePageTabs() {
const [tabs, setTabs] = useState(readTabs);
useEffect(() => {
const onHashChange = () => setTabs(readTabs());
window.addEventListener('hashchange', onHashChange);
return () => window.removeEventListener('hashchange', onHashChange);
}, []);
return tabs;
}
@@ -1,4 +1,4 @@
import { useCallback, useState } from 'preact/hooks';
import { useCallback, useRef, useState } from 'preact/hooks';
import {
addSafeShieldRule,
@@ -129,14 +129,25 @@ export function useSafeShieldRules(active: boolean) {
error: null,
feedback: null,
});
const latestMutationId = useRef(0);
const mutate = useCallback(
async (
operation: 'add' | 'delete',
action: SafeShieldRuleAction,
domain: string,
onPersisted?: () => void,
): Promise<boolean> => {
setMutation({
const mutationId = ++latestMutationId.current;
// DNS verification can outlive persistence, while a newer rule is saved.
// Never overwrite the newer request's feedback with an older result.
const report = (next: SafeShieldRulesMutationState) => {
if (latestMutationId.current === mutationId) {
setMutation(next);
}
};
report({
action: { operation, action, domain },
error: null,
feedback: null,
@@ -153,6 +164,9 @@ export function useSafeShieldRules(active: boolean) {
: await deleteSafeShieldRule(action, domain, true);
resource.replaceData(result.rules);
// A successful rules API response means the rule is saved. Notify the
// input immediately, without waiting for DNS apply verification.
onPersisted?.();
const shouldApply =
result.changed &&
@@ -160,7 +174,7 @@ export function useSafeShieldRules(active: boolean) {
result.rules.applyLocalOverrides;
if (!shouldApply) {
setMutation({
report({
action: null,
error: null,
feedback: savedOnlyMessage(operation, result),
@@ -169,7 +183,7 @@ export function useSafeShieldRules(active: boolean) {
}
if (!result.refresh.accepted) {
setMutation({
report({
action: null,
error: `규칙은 저장되었지만 로컬 DNS 적용 요청을 시작하지 못했습니다${result.refresh.reason ? ` (${result.refresh.reason})` : ''}.`,
feedback: null,
@@ -177,16 +191,28 @@ export function useSafeShieldRules(active: boolean) {
return true;
}
setMutation({
report({
action: null,
error: null,
feedback: applyingMessage(operation, result),
});
const outcome = await waitForLocalApply(baseline);
let outcome: LocalApplyOutcome;
try {
outcome = await waitForLocalApply(baseline);
} catch {
// The rule is already saved; a status-query failure must not
// incorrectly report the whole mutation as an unsuccessful save.
report({
action: null,
error: '규칙은 저장되었지만 DNS 적용 상태를 확인하지 못했습니다. SafeShield 상태를 확인해 주세요.',
feedback: null,
});
return true;
}
if (outcome.kind === 'success') {
setMutation({
report({
action: null,
error: null,
feedback: appliedMessage(operation, result),
@@ -195,7 +221,7 @@ export function useSafeShieldRules(active: boolean) {
}
if (outcome.kind === 'disabled') {
setMutation({
report({
action: null,
error: null,
feedback: '규칙은 저장되었지만 SafeShield가 꺼져 있어 DNS 적용을 대기하고 있습니다.',
@@ -204,7 +230,7 @@ export function useSafeShieldRules(active: boolean) {
}
if (outcome.kind === 'failure') {
setMutation({
report({
action: null,
error: '규칙은 저장되었지만 로컬 DNS 적용이 실패했습니다. SafeShield 상태를 확인해 주세요.',
feedback: null,
@@ -212,14 +238,14 @@ export function useSafeShieldRules(active: boolean) {
return true;
}
setMutation({
report({
action: null,
error: '규칙은 저장되었지만 제한 시간 안에 로컬 DNS 적용 완료를 확인하지 못했습니다.',
feedback: null,
});
return true;
} catch (error) {
setMutation({
report({
action: null,
error: errorMessage(error, '사용자 규칙을 변경하지 못했습니다.'),
feedback: null,
@@ -231,8 +257,8 @@ export function useSafeShieldRules(active: boolean) {
);
const addRule = useCallback(
(action: SafeShieldRuleAction, domain: string) =>
mutate('add', action, domain),
(action: SafeShieldRuleAction, domain: string, onPersisted?: () => void) =>
mutate('add', action, domain, onPersisted),
[mutate],
);
+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.28-r1',
assetVersion: host.dataset.assetVersion ?? '0.2.29-r1',
locale: document.documentElement.lang || 'ko',
});
}
@@ -12,6 +12,7 @@ import {
ShieldIcon,
} from '../components/Icons';
import { SafeShieldStatisticsPanel } from '../components/SafeShieldStatisticsPanel';
import { RelativeTimestamp } from '../components/RelativeTimestamp';
import { ErrorPanel, LoadingPanel } from '../components/StatePanels';
import type { SafeShieldAction } from '../hooks/useSafeShieldActions';
import type { SafeShieldStatistics, SafeShieldStatus } from '../types/safeshield';
@@ -25,6 +26,7 @@ import {
formatInterval,
formatNumber,
formatTimestamp,
parseArtifactVersionTimestamp,
} from '../app/format';
const SMARTSAFEHUB_PRICING_URL = 'https://www.smartsafehub.com/pricing/';
@@ -152,28 +154,11 @@ function getArtifactOptimizationLabel(tier: string | null): string {
return '자동 최적화';
}
function formatArtifactUpdatedAt(version: string | null): string {
if (!version) {
return '확인되지 않음';
}
const match = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(version);
if (!match) {
return '확인되지 않음';
}
const [, year, month, day, hour, minute, second] = match;
const timestamp = Date.UTC(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
Number(second),
) / 1000;
return formatTimestamp(timestamp);
function formatArtifactUpdatedAt(version: string | null): ComponentChildren {
const timestamp = parseArtifactVersionTimestamp(version);
return timestamp === null
? '확인되지 않음'
: <RelativeTimestamp timestamp={timestamp} />;
}
function getSummaryMessage(data: SafeShieldStatus): string {
@@ -369,13 +354,13 @@ function RefreshProgress({ data }: { data: SafeShieldStatus }) {
);
}
function SummaryFact({ label, value }: { label: string; value: string }) {
function SummaryFact({ label, value }: { label: string; value: ComponentChildren }) {
return (
<div class="bg-white px-5 py-4 sm:px-6">
<dt class="text-[0.68rem] font-black uppercase tracking-[0.14em] text-slate-400">
{label}
</dt>
<dd class="mt-2 mb-0 ml-0 truncate text-sm font-extrabold text-slate-950" title={value}>
<dd class="mt-2 mb-0 ml-0 text-sm font-extrabold text-slate-950" title={typeof value === 'string' ? value : undefined}>
{value}
</dd>
</div>
@@ -739,7 +724,7 @@ export function SafeShieldPage({
label="Blocklist"
value={`${formatNumber(displayedBlocklistCount)}개 도메인`}
/>
<SummaryFact label="Last refresh" value={formatTimestamp(data.timestamps.lastSuccess)} />
<SummaryFact label="Last refresh" value={<RelativeTimestamp timestamp={data.timestamps.lastSuccess} />} />
<PlanSummary plan={planName} />
</dl>
{planName === 'FREE' ? <FreePlanUpgrade /> : null}
@@ -815,7 +800,7 @@ export function SafeShieldPage({
icon={<CalendarIcon class="size-5" />}
title="차단 목록 갱신"
>
<DetailRow label="마지막 성공" value={formatTimestamp(data.timestamps.lastSuccess)} />
<DetailRow label="마지막 성공" value={<RelativeTimestamp timestamp={data.timestamps.lastSuccess} />} />
<DetailRow label="다음 갱신" value={formatTimestamp(data.timestamps.nextRefreshAt)} />
<DetailRow label="갱신 주기" value={formatInterval(data.timestamps.refreshIntervalS)} />
<DetailRow
@@ -1,5 +1,5 @@
import type { TargetedEvent, TargetedSubmitEvent } from 'preact';
import { useMemo, useState } from 'preact/hooks';
import { useEffect, useMemo, useState } from 'preact/hooks';
import {
AlertIcon,
@@ -23,6 +23,7 @@ interface SafeShieldRulesPageProps {
onAddRule: (
action: SafeShieldRuleAction,
domain: string,
onPersisted?: () => void,
) => Promise<boolean>;
onDeleteRule: (
action: SafeShieldRuleAction,
@@ -30,6 +31,8 @@ interface SafeShieldRulesPageProps {
) => Promise<boolean>;
onDismissFeedback: () => void;
onRetry: () => void;
// Injected only by the authenticated router app; the public demo has no RPC.
onLoadRecommendedRulesStatus?: () => Promise<{ domains: string[] }>;
}
interface RuleListCardProps {
@@ -38,12 +41,15 @@ interface RuleListCardProps {
onAddRule: (
action: SafeShieldRuleAction,
domain: string,
onPersisted?: () => void,
) => Promise<boolean>;
onDeleteRule: (
action: SafeShieldRuleAction,
domain: string,
) => Promise<boolean>;
rules: string[];
recommendedDomains: string[];
recommendedKnown: boolean;
}
function normalizeDomain(value: string): string {
@@ -73,6 +79,8 @@ function RuleListCard({
onAddRule,
onDeleteRule,
rules,
recommendedDomains,
recommendedKnown,
}: RuleListCardProps) {
const [input, setInput] = useState('');
const [search, setSearch] = useState('');
@@ -100,11 +108,11 @@ function RuleListCard({
}
setValidationError(null);
const saved = await onAddRule(kind, domain);
if (saved) {
setInput('');
}
await onAddRule(kind, domain, () => {
// Clear as soon as the server has saved the rule, not after DNS apply.
// Do not erase a newer draft if the input was edited in the meantime.
setInput((current) => normalizeDomain(current) === domain ? '' : current);
});
}
const theme = isAllow
@@ -217,6 +225,13 @@ function RuleListCard({
{filteredRules.length > 0 ? (
<ul class="m-0 list-none divide-y divide-slate-200 p-0">
{filteredRules.map((domain) => {
// Only domains created by the local recommender are managed.
// A previously manual allow entry stays manual even when the
// same domain appears in an assigned recommendation.
const cloudManaged = isAllow && recommendedDomains.includes(domain);
const ruleOrigin = !isAllow || recommendedKnown
? (cloudManaged ? '추천 규칙' : '직접 추가')
: null;
const deleting =
action?.operation === 'delete' &&
action.action === kind &&
@@ -224,15 +239,27 @@ function RuleListCard({
return (
<li class="flex min-w-0 items-center justify-between gap-3 px-3.5 py-3" key={domain}>
<code class="min-w-0 break-all font-mono text-sm font-semibold text-slate-800">
{domain}
</code>
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1.5">
<code class="min-w-0 break-all font-mono text-sm font-semibold text-slate-800">
{domain}
</code>
{ruleOrigin ? (
<span
class={`shrink-0 rounded-full px-2.5 py-1 text-[11px] font-bold ${cloudManaged ? 'bg-teal-50 text-teal-800' : 'bg-slate-100 text-slate-600'}`}
title={cloudManaged
? '추천 규칙 동기화로 추가된 도메인입니다. 웹사이트에서 적용을 해제해 주세요.'
: '공유기에 직접 등록되어 유지되는 규칙입니다.'}
>
{ruleOrigin}
</span>
) : null}
</div>
<button
aria-label={`${domain} ${theme.label}에서 삭제`}
class="inline-flex size-9 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-500 transition hover:border-red-200 hover:bg-red-50 hover:text-red-700 focus:outline-none focus-visible:ring-4 focus-visible:ring-red-100 disabled:cursor-not-allowed disabled:opacity-50"
disabled={busy}
disabled={busy || cloudManaged}
onClick={() => void onDeleteRule(kind, domain)}
title="삭제"
title={cloudManaged ? '웹사이트에서 적용 해제할 수 있습니다.' : '삭제'}
type="button"
>
<TrashIcon class={`size-4 ${deleting ? 'animate-pulse' : ''}`} />
@@ -267,7 +294,27 @@ export function SafeShieldRulesPage({
onDeleteRule,
onDismissFeedback,
onRetry,
onLoadRecommendedRulesStatus,
}: SafeShieldRulesPageProps) {
const [recommendedDomains, setRecommendedDomains] = useState<string[]>([]);
const [recommendedKnown, setRecommendedKnown] = useState(false);
useEffect(() => {
if (!onLoadRecommendedRulesStatus) return;
let mounted = true;
void onLoadRecommendedRulesStatus()
.then((status) => {
if (mounted) {
setRecommendedDomains(status.domains);
setRecommendedKnown(true);
}
})
.catch(() => {
// Older firmware/API errors must not hide or break manual rules.
if (mounted) setRecommendedKnown(false);
});
return () => { mounted = false; };
}, [data?.allow.join('\n'), onLoadRecommendedRulesStatus]);
if (loading && !data) {
return <LoadingPanel />;
}
@@ -317,6 +364,27 @@ export function SafeShieldRulesPage({
</div>
</section>
<section class="mt-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5" aria-label="추천 사용자 규칙">
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700"><CheckCircleIcon class="size-4" /></span>
<h2 class="m-0 text-base font-extrabold text-slate-950">추천 사용자 규칙</h2>
{recommendedKnown && recommendedDomains.length > 0 ? (
<span class="rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800">추천으로 추가 {recommendedDomains.length}개</span>
) : null}
</div>
<div class="mt-2 flex flex-wrap items-center gap-2" aria-label="추천 규칙 분류">
<span class="inline-flex items-center rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800">공식 추천</span>
<span class="inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-bold text-slate-700">커뮤니티</span>
</div>
<p class="mt-2 mb-0 text-sm leading-6 text-slate-600">SmartSafeHub의 공식 추천과 관리자가 검토한 커뮤니티 허용 규칙을 확인하고, 필요한 규칙을 적용해 보세요.</p>
<p class="mt-1 mb-0 text-xs leading-5 text-slate-500">공유기 선택과 적용 관리는 SmartSafeHub 웹사이트에서 진행합니다. 적용 결과는 다음 기기 동기화 후 반영되며, 연결된 모든 기기에 영향을 줍니다.</p>
</div>
<a class="inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-teal-700 bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 sm:w-auto" href="https://www.smartsafehub.com/dashboard/recommended-rules/" target="_blank" rel="noopener noreferrer">추천 규칙 보기 ↗</a>
</div>
</section>
{!data.applyLocalOverrides ? (
<section class="mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4" role="alert">
<AlertIcon class="mt-0.5 size-5 shrink-0 text-amber-700" />
@@ -371,6 +439,8 @@ export function SafeShieldRulesPage({
onAddRule={onAddRule}
onDeleteRule={onDeleteRule}
rules={data.allow}
recommendedDomains={recommendedDomains}
recommendedKnown={recommendedKnown}
/>
<RuleListCard
action={action}
@@ -378,6 +448,8 @@ export function SafeShieldRulesPage({
onAddRule={onAddRule}
onDeleteRule={onDeleteRule}
rules={data.block}
recommendedDomains={recommendedDomains}
recommendedKnown={recommendedKnown}
/>
</section>
@@ -3,15 +3,13 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import {
formatBytes,
formatLoadAverage,
formatRelativeTime,
formatTimestampInTimezone,
formatUptime,
getMemoryUsage,
} from '../app/format';
import { changeRootPassword } from '../api/security';
import { logoutLuciSession } from '../auth/session';
import { CustomSelect } from '../components/CustomSelect';
import { PageTabs } from '../components/PageTabs';
import { TimeSelect } from '../components/TimeSelect';
import {
AlertIcon,
@@ -28,9 +26,9 @@ import {
ShieldIcon,
} from '../components/Icons';
import type { ConfigurationBackupAction } from '../hooks/useConfigurationBackup';
import type { SettingsTab } from '../hooks/usePageTabs';
import type { SystemAction } from '../hooks/useSystemActions';
import type { ConfigurationBackupValidation } from '../types/backup';
import type { FirmwareStatus } from '../types/firmware';
import type { HealthReporterStatus, HealthSeverity, HealthStatus } from '../types/health';
import type { SmartSafeHubStatus } from '../types/status';
import type {
@@ -46,6 +44,7 @@ import { passwordPolicy, passwordPolicySatisfied } from '../utils/password';
import { browserTimezone } from '../utils/timezone';
interface SettingsPageProps {
activeTab: SettingsTab;
action: SystemAction;
backupAction: ConfigurationBackupAction;
backupError: string | null;
@@ -57,9 +56,6 @@ interface SettingsPageProps {
error: string | null;
feedbackError: string | null;
feedbackMessage: string | null;
firmware: FirmwareStatus | null;
firmwareError: string | null;
firmwareLoading: boolean;
health: HealthStatus | null;
healthActionError: string | null;
healthActionMessage: string | null;
@@ -102,20 +98,6 @@ interface SettingsPageProps {
onAdministratorPasswordChanged: () => void;
}
function InfoCard(props: { label: string; value: string; description: string }) {
return (
<article class="min-w-0 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5">
<p class="m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-slate-500">
{props.label}
</p>
<p class="mt-3 mb-0 break-words text-2xl font-black tracking-tight text-slate-950">
{props.value}
</p>
<p class="mt-2 mb-0 text-sm leading-6 text-slate-500">{props.description}</p>
</article>
);
}
function ActionCard(props: {
title: string;
description: string;
@@ -1562,6 +1544,7 @@ function ScheduledRebootSection(props: {
}
export function SettingsPage({
activeTab,
action,
backupAction,
backupError,
@@ -1573,9 +1556,6 @@ export function SettingsPage({
error,
feedbackError,
feedbackMessage,
firmware,
firmwareError,
firmwareLoading,
health,
healthActionError,
healthActionMessage,
@@ -1619,62 +1599,8 @@ export function SettingsPage({
}: SettingsPageProps) {
const [confirmingReboot, setConfirmingReboot] = useState(false);
if (loading && !data) {
return (
<div class="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm shadow-slate-900/5">
<p class="m-0 text-sm font-bold text-slate-500">시스템 정보를 확인하고 있습니다.</p>
</div>
);
}
if (error && !data) {
return (
<div class="rounded-2xl border border-rose-200 bg-rose-50 p-6">
<h2 class="m-0 text-lg font-black text-rose-900">
시스템 정보를 불러오지 못했습니다.
</h2>
<p class="mt-2 mb-0 text-sm leading-6 text-rose-700">{error}</p>
<button
class="mt-4 inline-flex min-h-10 items-center rounded-xl bg-rose-700 px-4 py-2 text-sm font-extrabold text-white transition hover:bg-rose-800"
onClick={onRetry}
type="button"
>
다시 시도
</button>
</div>
);
}
const memory = data?.runtime.memory;
const totalMemory = memory?.total ?? 0;
const memoryUsage = memory ? getMemoryUsage(memory) : null;
const usedMemory = memoryUsage?.used ?? 0;
const memoryPercent = Math.round(memoryUsage?.percent ?? 0);
const advancedSystemUrl = luciAdminUrl('/admin/system');
const logsUrl = luciAdminUrl('/admin/status/logs');
const customFirmwareAvailable = Boolean(firmware?.current.metadataAvailable);
const firmwareValue = !data
? '미확인'
: firmwareLoading && !firmware
? '확인 중'
: firmwareError && !firmware
? '확인 필요'
: customFirmwareAvailable
? firmware?.current.releaseVersion
? `SmartSafeHub ${firmware.current.releaseVersion}`
: 'SmartSafeHub 펌웨어'
: `${data.software.distribution} ${data.software.version}`;
const firmwareDescription = !data
? '버전 정보를 확인할 수 없습니다.'
: firmwareLoading && !firmware
? '펌웨어 정보를 확인하고 있습니다.'
: firmwareError && !firmware
? firmwareError
: customFirmwareAvailable
? firmware?.current.buildId
? `빌드 ID ${firmware.current.buildId}`
: '빌드 ID를 확인할 수 없습니다.'
: `리비전 ${data.software.revision}`;
return (
<section class="min-w-0 space-y-7">
@@ -1699,57 +1625,63 @@ export function SettingsPage({
</div>
)}
<section class="min-w-0">
<div class="mb-4">
<p class="m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700">
System
</p>
<h2 class="mt-2 mb-0 text-xl font-black text-slate-950">시스템 상태</h2>
<p class="mt-2 mb-0 text-sm leading-6 text-slate-500">
장치의 펌웨어, 실행 시간, 메모리와 시스템 부하를 확인합니다.
</p>
</div>
<div class="grid min-w-0 grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<InfoCard
label="Firmware"
value={firmwareValue}
description={firmwareDescription}
/>
<InfoCard
label="Uptime"
value={data ? formatUptime(data.runtime.uptime) : '미확인'}
description={
data ? `커널 ${data.software.kernel}` : '실행 시간을 확인할 수 없습니다.'
}
/>
<InfoCard
label="Memory"
value={`${memoryPercent}%`}
description={`${formatBytes(usedMemory)} / ${formatBytes(totalMemory)} 사용`}
/>
<InfoCard
label="Load"
value={data ? formatLoadAverage(data.runtime.load[0]) : '0.00'}
description={
data
? `5분 ${formatLoadAverage(data.runtime.load[1])} · 15분 ${formatLoadAverage(data.runtime.load[2])}`
: '시스템 부하를 확인할 수 없습니다.'
}
<PageTabs
active={activeTab}
label="설정 메뉴"
tabs={[
{ id: 'diagnostics', label: '진단 및 지원', href: '#settings' },
{ id: 'time', label: '시간 및 예약', href: '#settings/time' },
{ id: 'management', label: '관리 및 복구', href: '#settings/management' },
]}
/>
<section
aria-labelledby="diagnostics-tab"
class="min-w-0"
hidden={activeTab !== 'diagnostics'}
id="diagnostics-panel"
role="tabpanel"
>
{error && !data ? (
<div class="mb-4 rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm font-semibold text-amber-900" role="status">
<p class="m-0">시스템 정보를 불러오지 못해 진단 파일 다운로드가 제한될 수 있습니다. {error}</p>
<button class="mt-2 rounded-lg border border-current px-3 py-1.5 text-xs font-extrabold" onClick={onRetry} type="button">다시 시도</button>
</div>
) : loading && !data ? (
<p class="mb-4 text-sm font-semibold text-slate-500" role="status">진단 파일에 필요한 시스템 정보를 확인하고 있습니다.</p>
) : null}
<div class="min-w-0">
<HealthDiagnosticCard
actionError={healthActionError}
actionMessage={healthActionMessage}
data={health}
downloadBusy={action === 'diagnostics'}
error={healthError}
loading={healthLoading}
onDismissFeedback={onDismissHealthFeedback}
onDownload={onDownloadDiagnostics}
onRetry={onRetry}
onRun={onRunHealth}
onSetReporter={onSetHealthReporter}
running={healthRunning}
savingReporter={healthSavingReporter}
/>
</div>
</section>
<section class="min-w-0">
<section
aria-labelledby="time-tab"
class="min-w-0"
hidden={activeTab !== 'time'}
id="time-panel"
role="tabpanel"
>
<div class="mb-4">
<p class="m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700">
Device settings
</p>
<h2 class="mt-2 mb-0 text-xl font-black text-slate-950">장치 설정</h2>
<p class="mt-2 mb-0 text-sm leading-6 text-slate-500">
시간 기준과 예약 재부팅, 진단 정보를 SmartSafeHub에서 직접 관리합니다.
</p>
<p class="m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700">Device settings</p>
<h2 class="mt-2 mb-0 text-xl font-black text-slate-950">시간 및 예약</h2>
<p class="mt-2 mb-0 text-sm leading-6 text-slate-500">시간 기준과 예약 재부팅을 SmartSafeHub에서 직접 관리합니다.</p>
</div>
<div class="grid min-w-0 grid-cols-1 gap-4 lg:grid-cols-2">
<div class="min-w-0 max-w-4xl">
<TimeSettingsCard
data={timeData}
error={timeError}
@@ -1771,26 +1703,16 @@ export function SettingsPage({
scheduledRebootSaving={scheduledRebootSaving}
syncing={timeSyncing}
/>
<HealthDiagnosticCard
actionError={healthActionError}
actionMessage={healthActionMessage}
data={health}
downloadBusy={action === 'diagnostics'}
error={healthError}
loading={healthLoading}
onDismissFeedback={onDismissHealthFeedback}
onDownload={onDownloadDiagnostics}
onRetry={onRetry}
onRun={onRunHealth}
onSetReporter={onSetHealthReporter}
running={healthRunning}
savingReporter={healthSavingReporter}
/>
</div>
</section>
<section class="min-w-0">
<section
aria-labelledby="management-tab"
class="min-w-0"
hidden={activeTab !== 'management'}
id="management-panel"
role="tabpanel"
>
<div class="mb-4">
<p class="m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700">
System management
@@ -1817,7 +1739,6 @@ export function SettingsPage({
validated={backupValidated}
/>
<SystemToolsCard
action={action}
className="lg:col-span-2"
@@ -1,18 +1,39 @@
import { FirmwareUpdatesCard } from '../components/FirmwareUpdatesCard';
import { SoftwareUpdatesCard } from '../components/SoftwareUpdatesCard';
import type { useFirmwareUpdates } from '../hooks/useFirmwareUpdates';
import type { FirmwareAction } from '../hooks/useFirmwareUpdates';
import type { FirmwareStatus } from '../types/firmware';
import type { SoftwareUpdateAction } from '../hooks/useSoftwareUpdates';
import type {
SoftwareUpdateSettingsInput,
SoftwareUpdateStatus,
} from '../types/updates';
// Structural contract shared by the real hook and the fixture-only public demo.
// No hook is executed inside this page.
interface FirmwareController {
action: FirmwareAction;
actionError: string | null;
data: FirmwareStatus | null;
error: string | null;
loading: boolean;
message: string | null;
reconnecting: boolean;
uploadProgress: number | null;
check: () => void | Promise<void>;
discard: () => Promise<boolean>;
dismissFeedback: () => void;
install: (keepSettings: boolean) => Promise<boolean>;
prepare: () => void | Promise<void>;
refresh: () => void | Promise<void>;
upload: (file: File) => Promise<boolean>;
}
interface UpdatePageProps {
action: SoftwareUpdateAction;
actionError: string | null;
data: SoftwareUpdateStatus | null;
error: string | null;
firmware: ReturnType<typeof useFirmwareUpdates>;
firmware: FirmwareController;
loading: boolean;
message: string | null;
onCheck: () => void;
@@ -42,6 +63,7 @@ export function UpdatePage({
action={firmware.action}
actionError={firmware.actionError}
data={firmware.data}
software={data}
error={firmware.error}
loading={firmware.loading}
message={firmware.message}
@@ -56,6 +78,7 @@ export function UpdatePage({
onUpload={firmware.upload}
/>
<SoftwareUpdatesCard
firmware={firmware.data}
action={action}
actionError={actionError}
data={data}
@@ -1,5 +1,5 @@
import type { TargetedSubmitEvent } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import { useCallback, useEffect, useState } from 'preact/hooks';
import { CustomSelect } from '../components/CustomSelect';
import { GuestWifiCard } from '../components/GuestWifiCard';
@@ -10,6 +10,7 @@ import type { WifiFeedback } from '../hooks/useWifi';
import type {
GuestWifiUpdateInput,
WifiNetworkSummary,
WifiQrCredentials,
WifiSecurityChoice,
WifiSummary,
WifiUpdateInput,
@@ -25,6 +26,8 @@ interface WifiPageProps {
onRetry: () => void;
onUpdate: (input: WifiUpdateInput) => Promise<boolean>;
onUpdateGuest: (input: GuestWifiUpdateInput) => Promise<boolean>;
onLoadQr: (section: string) => Promise<WifiQrCredentials>;
onLoadGuestQr: () => Promise<WifiQrCredentials>;
}
const SECURITY_OPTIONS: ReadonlyArray<{
@@ -64,11 +67,13 @@ function WifiNetworkCard({
busy,
saving,
onUpdate,
onLoadQr,
}: {
network: WifiNetworkSummary;
busy: boolean;
saving: boolean;
onUpdate: (input: WifiUpdateInput) => Promise<boolean>;
onLoadQr: (section: string) => Promise<WifiQrCredentials>;
}) {
const [ssid, setSsid] = useState(network.ssid);
const [enabled, setEnabled] = useState(network.enabled);
@@ -77,6 +82,7 @@ function WifiNetworkCard({
);
const [password, setPassword] = useState('');
const [showQr, setShowQr] = useState(false);
const loadQr = useCallback(() => onLoadQr(network.section), [onLoadQr, network.section]);
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
@@ -302,7 +308,7 @@ function WifiNetworkCard({
</button>
</div>
{showQr ? <WifiQrDialog section={network.section} onClose={() => setShowQr(false)} /> : null}
{showQr ? <WifiQrDialog load={loadQr} onClose={() => setShowQr(false)} /> : null}
{validationError ? (
<p class="mt-4 mb-0 rounded-xl bg-red-50 px-4 py-3 text-sm font-bold text-red-800">
{validationError}
@@ -353,6 +359,8 @@ export function WifiPage({
onRetry,
onUpdate,
onUpdateGuest,
onLoadQr,
onLoadGuestQr,
}: WifiPageProps) {
if (loading) {
return <LoadingPanel />;
@@ -393,6 +401,7 @@ export function WifiPage({
busy={updatingSection !== null}
network={network}
onUpdate={onUpdate}
onLoadQr={onLoadQr}
saving={updatingSection === network.section}
/>
))}
@@ -416,6 +425,7 @@ export function WifiPage({
busy={updatingSection !== null}
saving={updatingSection === 'ssh_guest'}
onUpdate={onUpdateGuest}
onLoadQr={onLoadGuestQr}
/>
</>
)}
@@ -42,6 +42,8 @@ export interface FirmwareRelease {
openwrtVersion: string | null;
publishedAt: string | null;
releaseNotes: string[];
/** Actual SmartSafeHub package embedded in this firmware image, when published by Hub. */
bundledSoftware?: { smartsafehub: string } | null;
sysupgrade: FirmwareImage;
}
@@ -59,3 +59,9 @@ export interface GuestWifiUpdateResult {
reloaded: boolean;
guest: GuestWifiSummary;
}
export interface WifiQrCredentials {
ssid: string;
security: 'none' | 'psk2' | 'sae' | 'sae-mixed';
password: string;
}
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: GPL-3.0-or-later
export type BundledVersionComparison = 'same' | 'newer' | 'older' | 'different' | 'unknown';
/** Compare SmartSafeHub numeric package versions, including the -rN revision. */
export function compareBundledVersion(
bundled: string | null | undefined,
installed: string | null | undefined,
): BundledVersionComparison {
if (!bundled || !installed) {
return 'unknown';
}
if (bundled === installed) {
return 'same';
}
const pattern = /^(\d+)\.(\d+)\.(\d+)(?:-r(\d+))?$/;
const target = pattern.exec(bundled);
const current = pattern.exec(installed);
if (!target || !current) {
return 'different';
}
for (let index = 1; index <= 4; index += 1) {
const targetPart = BigInt(target[index] || '0');
const currentPart = BigInt(current[index] || '0');
if (targetPart > currentPart) return 'newer';
if (targetPart < currentPart) return 'older';
}
return 'same';
}
@@ -0,0 +1,27 @@
import { fileURLToPath, URL } from 'node:url';
import preact from '@preact/preset-vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
// Separate public website bundle: never include LuCI authentication or RPC entry code.
export default defineConfig({
base: '/static/router-demo/',
publicDir: false,
plugins: [preact(), tailwindcss()],
build: {
target: 'es2020',
outDir: fileURLToPath(new URL('./demo-dist', import.meta.url)),
emptyOutDir: true,
cssCodeSplit: false,
sourcemap: false,
rollupOptions: {
input: fileURLToPath(new URL('./src/demo/main.tsx', import.meta.url)),
output: {
entryFileNames: 'demo.js',
chunkFileNames: 'chunks/[name]-[hash].js',
assetFileNames: (asset) => asset.name?.endsWith('.css') ? 'demo.css' : 'assets/[name]-[hash][extname]',
},
},
},
});
@@ -1 +1,2 @@
/etc/smartsafehub/device-credential.json
/etc/smartsafehub/recommended-rules.state
@@ -14,6 +14,8 @@ RUNTIME_DIR="${SMARTSAFEHUB_DEVICE_RUNTIME_DIR:-/tmp/smartsafehub}"
STATE_FILE="${SMARTSAFEHUB_DEVICE_STATE_FILE:-$RUNTIME_DIR/device.json}"
CREDENTIAL_FILE="${SMARTSAFEHUB_DEVICE_CREDENTIAL_FILE:-/etc/smartsafehub/device-credential.json}"
ACTIVITY_CREDENTIAL_FILE="${SMARTSAFEHUB_ACTIVITY_CREDENTIAL_FILE:-$RUNTIME_DIR/activity-sync-credential.json}"
RECOMMENDED_HELPER="${SMARTSAFEHUB_RECOMMENDED_HELPER:-/usr/libexec/smartsafehub-recommended-rules}"
RECOMMENDED_RECEIPT="${SMARTSAFEHUB_RECOMMENDED_RECEIPT_FILE:-$RUNTIME_DIR/recommended-rules.receipt.json}"
API_BASE_URL="${SMARTSAFEHUB_DEVICE_API_BASE_URL:-https://www.smartsafehub.com/api/v1}"
UCLIENT_FETCH_BIN="${SMARTSAFEHUB_DEVICE_UCLIENT_FETCH_BIN:-uclient-fetch}"
JSONFILTER_BIN="${SMARTSAFEHUB_DEVICE_JSONFILTER_BIN:-jsonfilter}"
@@ -208,7 +210,17 @@ status_sync() {
now="$(now_epoch)"; next=$((now + CHECK_INTERVAL_S))
ensure_device_bootstrapped || return 1
token="$(credential_token)" || return 1
body="$RUNTIME_DIR/device-sync-request.$$.json"; out="$RUNTIME_DIR/device-sync-response.$$.json"; printf '{}\n' > "$body"
body="$RUNTIME_DIR/device-sync-request.$$.json"; out="$RUNTIME_DIR/device-sync-response.$$.json"
# A receipt is written by the reconciler only after verifying the allow list.
# It is authenticated using the existing device credential. Send the prior
# receipt here; the latest reconciliation is delivered on the next sync.
if [ -f "$RECOMMENDED_RECEIPT" ] && [ ! -L "$RECOMMENDED_RECEIPT" ] && [ "$(wc -c < "$RECOMMENDED_RECEIPT")" -le 40000 ]; then
printf '{"recommended_rules_ack":' > "$body"
cat "$RECOMMENDED_RECEIPT" >> "$body"
printf '}\n' >> "$body"
else
printf '{}\n' > "$body"
fi
if ! post_json "$API_BASE_URL/devices/sync" "$body" "$out" "$token"; then last_success="$(json_value "$STATE_FILE" '@.lastSuccessAt')"; case "$last_success" in ''|*[!0-9]*) last_success=0 ;; esac; protection_refresh_pending="$(json_value "$STATE_FILE" '@.protectionRefreshPending')"; write_state registered failed DEVICE_SYNC_UNAVAILABLE unknown '' "$(json_value "$STATE_FILE" '@.pairingCode')" "$(json_value "$STATE_FILE" '@.pairingExpiresAt')" "$next" "$last_success" "$protection_refresh_pending"; rm -f "$body" "$out"; return 1; fi
previous_registered="$(json_value "$STATE_FILE" '@.accountRegistered')"
previous_plan="$(ascii_lower "$(json_value "$STATE_FILE" '@.plan')")"
@@ -250,6 +262,13 @@ status_sync() {
# the router UI even though Hub already accepted the ownership change.
write_state registered active '' "$registered" "$plan" "$code" "$expires" "$next" "$now" "$protection_refresh_pending"
# Hub-approved recommendations are reconciled by a separate local helper.
# Keep the account state available even when SafeShield cannot be reached;
# a failed reconciliation is retried on the next device sync.
if [ -x "$RECOMMENDED_HELPER" ]; then
"$RECOMMENDED_HELPER" "$out" || log_message 'recommended rules will retry on next device sync'
fi
if [ "$protection_refresh_pending" = true ]; then
if refresh_safeshield_after_entitlement_change; then
protection_refresh_pending=false
@@ -0,0 +1,154 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
# Reconcile only allow domains inserted on behalf of SmartSafeHub Hub.
# A locally existing allow entry is never claimed or deleted.
set -eu
umask 077
RESPONSE="${1:?missing device sync response}"
STATE="${SMARTSAFEHUB_RECOMMENDED_STATE_FILE:-/etc/smartsafehub/recommended-rules.state}"
JSONFILTER="${SMARTSAFEHUB_DEVICE_JSONFILTER_BIN:-jsonfilter}"
UBUS="${SMARTSAFEHUB_DEVICE_UBUS_BIN:-ubus}"
RUNTIME="${SMARTSAFEHUB_DEVICE_RUNTIME_DIR:-/tmp/smartsafehub}"
RECEIPT="${SMARTSAFEHUB_RECOMMENDED_RECEIPT_FILE:-${RUNTIME}/recommended-rules.receipt.json}"
mkdir -p "$RUNTIME" "${STATE%/*}"
LOCK="$RUNTIME/recommended-rules.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
# A crashed reconciler may have left its lock; a live one must win.
owner="$(cat "$LOCK/pid" 2>/dev/null || true)"
case "$owner" in ''|*[!0-9]*) exit 1 ;; esac
kill -0 "$owner" 2>/dev/null && exit 1
rm -f "$LOCK/pid"
rmdir "$LOCK" 2>/dev/null || exit 1
mkdir "$LOCK" 2>/dev/null || exit 1
fi
printf '%s\n' "$$" > "$LOCK/pid"
DESIRED="$RUNTIME/recommended-desired.$$"
OWNED="$RUNTIME/recommended-owned.$$"
SNAPSHOT="$RUNTIME/recommended-snapshot.$$"
REPLY="$RUNTIME/recommended-reply.$$"
ALLOW="$RUNTIME/recommended-allow.$$"
VERIFY="$RUNTIME/recommended-verify.$$"
cleanup() { rm -f "$DESIRED" "$OWNED" "$SNAPSHOT" "$REPLY" "$ALLOW" "$VERIFY" "$LOCK/pid"; rmdir "$LOCK" 2>/dev/null || true; }
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
field() { "$JSONFILTER" -i "$1" -e "$2" 2>/dev/null || true; }
valid_hash() { [ "${#1}" -eq 64 ] && ! printf '%s' "$1" | LC_ALL=C grep -q '[^0-9a-f]'; }
valid_domain() {
[ -n "$1" ] && [ "${#1}" -le 253 ] || return 1
printf '%s\n' "$1" | LC_ALL=C grep -Eq '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || return 1
! printf '%s\n' "$1" | LC_ALL=C grep -Eq '\.[0-9]+$' || return 1
rest="$1"
while [ -n "$rest" ]; do
label="${rest%%.*}"
[ "${#label}" -le 63 ] || return 1
case "$rest" in *.*) rest="${rest#*.}" ;; *) break ;; esac
done
}
revision="$(field "$RESPONSE" '@.recommended_rules.revision')"
# Do not erase cloud-managed rules if the server is not yet sending this feature.
[ -n "$revision" ] || exit 0
# Never resend a stale confirmation if reconciliation fails midway.
rm -f "$RECEIPT"
valid_hash "$revision" || exit 1
"$JSONFILTER" -i "$RESPONSE" -e '@.recommended_rules.domains[*]' > "$DESIRED" || exit 1
[ "$(wc -l < "$DESIRED")" -le 100 ] || exit 1
while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$DESIRED"
sort -u "$DESIRED" -o "$DESIRED"
previous=''
: > "$OWNED"
if [ -e "$STATE" ]; then
[ -f "$STATE" ] && [ ! -L "$STATE" ] || exit 1
previous="$(sed -n '1p' "$STATE")"
[ "$previous" = '-' ] || valid_hash "$previous" || exit 1
sed '1d' "$STATE" > "$OWNED"
while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$OWNED"
sort -u "$OWNED" -o "$OWNED"
fi
persist() {
temp="${STATE}.tmp.$$"
{ printf '%s\n' "$1"; sort -u "$OWNED"; } > "$temp"
chmod 600 "$temp"
mv -f "$temp" "$STATE"
}
"$UBUS" call safeshield rules_list '{}' > "$REPLY" || exit 1
[ "$(field "$REPLY" '@.ok')" != false ] || exit 1
# A broken or truncated rules_list must never be mistaken for an empty list.
LC_ALL=C grep -Eq '"allow"[[:space:]]*:[[:space:]]*\[' "$REPLY" || exit 1
"$JSONFILTER" -i "$REPLY" -e '@.allow[*]' > "$ALLOW" || exit 1
while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$ALLOW"
sort -u "$ALLOW" -o "$ALLOW"
mutate() {
method="$1"; domain="$2"
"$UBUS" call safeshield "$method" "{\"action\":\"allow\",\"domain\":\"$domain\",\"refresh\":false}" > "$REPLY" || return 1
[ "$(field "$REPLY" '@.ok')" != false ] || return 1
}
# Refresh is retried after a partial or unsuccessful reconciliation.
need_refresh=0
[ "$previous" = '-' ] && need_refresh=1
cp "$OWNED" "$SNAPSHOT"
while IFS= read -r domain; do
[ -n "$domain" ] || continue
if ! grep -Fxq "$domain" "$DESIRED"; then
if grep -Fxq "$domain" "$ALLOW"; then
mutate rule_delete "$domain" || exit 1
need_refresh=1
fi
grep -Fxv "$domain" "$OWNED" > "$REPLY" || true
mv "$REPLY" "$OWNED"
persist '-'
fi
done < "$SNAPSHOT"
while IFS= read -r domain; do
[ -n "$domain" ] || continue
if ! grep -Fxq "$domain" "$ALLOW"; then
mutate rule_add "$domain" || exit 1
need_refresh=1
if ! grep -Fxq "$domain" "$OWNED"; then
printf '%s\n' "$domain" >> "$OWNED"
fi
persist '-'
fi
done < "$DESIRED"
if [ "$need_refresh" -eq 1 ]; then
"$UBUS" call safeshield refresh '{}' > "$REPLY" || exit 1
[ "$(field "$REPLY" '@.ok')" != false ] || exit 1
[ "$(field "$REPLY" '@.accepted')" != false ] || exit 1
fi
# Read the actual SafeShield rule list again rather than trusting rule_add results.
"$UBUS" call safeshield rules_list '{}' > "$REPLY" || exit 1
[ "$(field "$REPLY" '@.ok')" != false ] || exit 1
LC_ALL=C grep -Eq '"allow"[[:space:]]*:[[:space:]]*\[' "$REPLY" || exit 1
"$JSONFILTER" -i "$REPLY" -e '@.allow[*]' > "$VERIFY" || exit 1
while IFS= read -r domain || [ -n "$domain" ]; do valid_domain "$domain" || exit 1; done < "$VERIFY"
sort -u "$VERIFY" -o "$VERIFY"
while IFS= read -r domain; do
[ -n "$domain" ] || continue
grep -Fxq "$domain" "$VERIFY" || exit 1
done < "$DESIRED"
# The receipt describes *allow-list presence*, not proven DNS query results.
# Domains not in OWNED existed before this recommendation (manual configuration).
receipt_tmp="${RECEIPT}.tmp.$$"
{
printf '{"revision":"%s","status":"confirmed","domains":[' "$revision"
first=1
while IFS= read -r domain; do
[ -n "$domain" ] || continue
if grep -Fxq "$domain" "$OWNED"; then source=managed; else source=manual; fi
[ "$first" -eq 1 ] || printf ','
printf '{"domain":"%s","source":"%s"}' "$domain" "$source"
first=0
done < "$DESIRED"
printf ']}\n'
} > "$receipt_tmp"
chmod 600 "$receipt_tmp"
persist "$revision"
mv -f "$receipt_tmp" "$RECEIPT"
@@ -14,6 +14,7 @@
"system_scheduled_reboot_settings",
"health_status",
"device_registration_status",
"device_recommended_rules_status",
"activity_history"
],
"safeshield": [
@@ -50,6 +50,7 @@ import {
} from './smartsafehub/health.uc';
import {
read_device_registration_status,
read_recommended_rules_status,
refresh_device_registration_status,
refresh_device_pairing
} from './smartsafehub/device-registration.uc';
@@ -318,6 +319,11 @@ const methods = {
return mutate_safeshield_rule(request, 'rule_delete');
}),
},
device_recommended_rules_status: {
call: require_root_password(function(request) {
return read_recommended_rules_status(request);
}),
},
device_registration_status: {
call: require_root_password(function(request) {
return read_device_registration_status(request);
@@ -9,6 +9,24 @@ function read_state() {
if (raw == null || length(raw) == 0) return { schema:1, component:'device', phase:'pending', lastResult:'never', lastErrorCode:null, accountRegistered:null, plan:null, pairingCode:null, pairingExpiresAt:null, protectionRefreshPending:false, nextSyncAt:0, lastSuccessAt:0 };
try { return json(raw); } catch (e) { return null; }
}
// Local state only: the hub owns the list of recommended rules, while
// the router exposes the domains actually owned by the reconciler.
const RECOMMENDED_FILE = '/etc/smartsafehub/recommended-rules.state';
export function read_recommended_rules_status(request) {
const raw = fs.readfile(RECOMMENDED_FILE);
if (raw == null || length(raw) == 0) return success({ revision: null, domains: [] });
const lines = split(raw, '\n');
const revision = lines[0];
if (revision != '-' && match(revision, /^[0-9a-f]{64}$/) == null) return failure('RECOMMENDED_STATE_INVALID', '추천 규칙 상태가 올바르지 않습니다.');
let domains = [];
for (let i = 1; i < length(lines); i++) {
let domain = lines[i];
if (domain == '') continue;
if (match(domain, /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/) == null) return failure('RECOMMENDED_STATE_INVALID', '추천 규칙 상태가 올바르지 않습니다.');
push(domains, domain);
}
return success({ revision: revision == '-' ? null : revision, domains: domains });
};
export function read_device_registration_status(request) { const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); };
export function refresh_device_registration_status(request) { if (!run_command([ HELPER, 'status-sync' ], 25000)) return failure('DEVICE_SYNC_FAILED', 'SmartSafeHub 계정 연결 상태를 확인하지 못했습니다.'); const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); };
export function refresh_device_pairing(request) { if (!run_command([ HELPER, 'pairing-session' ], 15000)) return failure('DEVICE_PAIRING_FAILED', 'SmartSafeHub 계정 연결 코드를 발급하지 못했습니다.'); const state = read_state(); return state != null ? success(state) : failure('DEVICE_STATE_INVALID', '기기 등록 상태를 읽지 못했습니다.'); };
@@ -301,6 +301,14 @@ function sanitize_release(document, current) {
}
}
// Hub may expose the manifest field directly or within the build metadata.
// An absent or malformed version is unknown, not evidence that it matches.
const bundled = release?.bundled_software ?? release?.metadata?.bundled_software;
let bundled_version = limited_string(bundled?.smartsafehub, 40);
if (bundled_version != null && match(bundled_version, /^[0-9]+\.[0-9]+\.[0-9]+(-r[0-9]+)?$/) == null) {
bundled_version = null;
}
const size = integer_value(image?.size_bytes, 0);
return {
currentVersion: current_version,
@@ -316,6 +324,7 @@ function sanitize_release(document, current) {
openwrtVersion: limited_string(release?.openwrt_version, 80),
publishedAt: limited_string(release?.published_at, 80),
releaseNotes: notes,
bundledSoftware: bundled_version != null ? { smartsafehub: bundled_version } : null,
sysupgrade: {
id: integer_value(image?.id, 0),
filename: limited_string(image?.filename, 180),
@@ -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.28-r1">
<link rel="icon" type="image/svg+xml" href="/luci-static/smartsafehub/favicon.svg?v=0.2.29-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.28-r1"
data-asset-version="0.2.29-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.28-r1"></script>
<script type="module" src="/luci-static/smartsafehub/app.js?v=0.2.29-r1"></script>
</body>
</html>
@@ -1,3 +1,3 @@
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties{:host,*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:Line truncated
@layer properties{:host,*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-ease:initial}::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:Line truncated
/*$vite$:1*/
@@ -1,3 +1,4 @@
var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])&&n.__e)return n.__e;return typeof e.type!=`function`||e.props.__P?null:b(e)}function x(e){if((e=e.__)&&e.__c&&!e.props.__P)return e.__e=null,e.__k.some(function(t){return t&&(e.__e=t.__e)}),x(e)}function S(t){(8&t.__g||!(t.__g|=8)||!n.push(t)||i++)&&r==e.debounceRendering||((r=e.debounceRendering)||queueMicrotask)(C)}function C(){var t,r,o,s,c,l,u,d,f;try{for(r=1;n.length;)n.length>r&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),ee(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,ne(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;p<b;p++)(h=n.__k[p])!=null&&(m=~h.__i&&y[h.__i]||u,h.__i=p,v=ee(e,h,m,i,a,o,s,c,l,f),g=h.__e,(m.ref!=h.ref||8&m.__u)&&(m.ref&&ie(m.ref,null,h,m),h.ref&&f.push(h.ref,h.__c||g,h)),_=_||g,4&h.__u?(c=E(h,c,e,!m.__v),m.__e&&(m.__e=null)):typeof h.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),h.__u&=-7);return n.__e=_,c}function T(e,t,n,r,i){var a,o,s,c,l,u,d,f,m,h,g=n.length,y=g,x=0,S=!1,C=e.__k=Array(i);for(a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o!=`object`||o.constructor==String?o=C[a]=_(null,o):p(o)?o=C[a]=_(v,{children:o}):o.constructor===void 0&&o.__b?o=C[a]=_(o.type,o.props,o.key,o.ref,o.__v):C[a]=o,c=a+x,o.__=e,o.__b=e.__b+1,s=null,~(l=o.__i=O(o,n,c,y))&&(y--,(s=n[l])&&(s.__u|=2)),s&&s.__v?(o.__u|=2,l==c-1?x--:l==c+1?x++:l!=c&&(l>c?x--:x++,S=!0)):(~l||(i>g?x--:i<g&&x++),typeof o.type!=`function`&&(o.__u|=4))):C[a]=null;if(S){for(u=[],d=[],a=0;a<i;a++)if((o=C[a])&&2&o.__u){for(f=0,m=u.length;f<m;)u[h=f+m>>1]<o.__i?f=h+1:m=h;u[f]=o.__i,d[a]=f+1}for(x=u.length;a--;)d[a]&&(d[a]==x?x--:C[a].__u|=4)}if(y)for(a=0;a<g;a++)!(s=n[a])||2&s.__u||(s.__e==r&&(r=b(s)),ae(s,s));return r}function E(e,t,n,r){var i,a,o;if(typeof e.type==`function`){if(e.props.__P)return t;if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&(i[a].__=e,t=E(i[a],t,n,!1));return t}for(t&&!t.parentNode&&(t=b(e))&&!t.parentNode&&(t=null),o=t;o&&o.nodeType==8;)o=o.nextSibling;for(e.__e!=o&&(!r&&n.moveBefore&&e.__e.parentNode?n.moveBefore(e.__e,t):n.insertBefore(e.__e,t||null)),t=e.__e;(t=t&&t.nextSibling)&&t.nodeType==8;);return t}function D(e,t){return t=t||[],e!=null&&typeof e!=`boolean`&&(p(e)?e.some(function(e){D(e,t)}):t.push(e)),t}function O(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l&&!(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++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(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||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,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 j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]<this.__a[t])return;return r(e.event?e.event(n):n)}}}function ee(t,n,r,i,a,o,s,c,l,u){var f,m,_,x,S,C,T,E,D,O,k,A,j,ee,ne,ie,se,ce,le,ue,de=n.type;if(n.constructor!==void 0)return null;if(128&r.__u&&(l=32&r.__u,f=r.__c.__z)){if(n.__u|=l,m=o=[],f.nodeType==8)for(_=1,x=f.nextSibling;x;x=x.nextSibling){if(x.nodeType==8){if(x.data.startsWith(`$s`))_++;else if(x.data.startsWith(`/$s`)&&!--_)break}o.push(x)}else o.push(f);c=o[0]}(f=e.__b)&&f(n);n:if(typeof de==`function`){S=s.length;try{if(O=n.props,k=(f=de.prototype)&&f.render,A=(f=de.contextType)&&i[f.__c],j=f?A?A.props.value:f.__:i,r.__c?2&(C=n.__c=r.__c).__g&&(C.__g|=1):(k?n.__c=C=new de(O,j):(n.__c=C=new y(O,j),C.constructor=de,C.render=oe),A&&A.sub(C),C.state||(C.state={}),C.__n=i,C.__g|=8,C.Line truncated
var e,t,n,r,i,a,o,s,c,l,u={},d=[],f=/^m(i|n|o|s|text|space)$/,p=Array.isArray,m=d.slice,h=Object.assign;function g(e){e&&e.parentNode&&e.remove()}function _(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function v(e){return e.children}function y(e,t){this.props=e,this.context=t,this.__g=0}function b(e,t){if(t==null)return e.__?b(e.__,e.__i+1):null;for(var n;t<e.__k.length;t++)if((n=e.__k[t])&&n.__e)return n.__e;return typeof e.type!=`function`||e.props.__P?null:b(e)}function x(e){if((e=e.__)&&e.__c&&!e.props.__P)return e.__e=null,e.__k.some(function(t){return t&&(e.__e=t.__e)}),x(e)}function S(t){(8&t.__g||!(t.__g|=8)||!n.push(t)||i++)&&r==e.debounceRendering||((r=e.debounceRendering)||queueMicrotask)(C)}function C(){var t,r,o,s,c,l,u,d,f;try{for(r=1;n.length;)n.length>r&&n.sort(a),t=n.shift(),r=n.length,8&t.__g&&(s=void 0,c=void 0,l=(c=(o=t).__v).__e,u=[],d=[],(f=o.__P)&&((s=h({constructor:void 0},c)).__v=c.__v+1,e.vnode&&e.vnode(s),ee(f,s,c,o.__n,f.namespaceURI,32&c.__u?[l]:null,u,l||b(c),32&c.__u,d),s.__v=c.__v,s.__.__k[s.__i]=s,ne(u,s,d),c.__=c.__e=null,s.__e!=l&&x(s)))}finally{n.length=i=0}}function w(e,t,n,r,i,a,o,s,c,l,f){var p,m,h,g,_,v,y=r.__k||d,b=t.length;for(c=T(n,t,y,c,b),p=0;p<b;p++)(h=n.__k[p])!=null&&(m=~h.__i&&y[h.__i]||u,h.__i=p,v=ee(e,h,m,i,a,o,s,c,l,f),g=h.__e,(m.ref!=h.ref||8&m.__u)&&(m.ref&&ie(m.ref,null,h,m),h.ref&&f.push(h.ref,h.__c||g,h)),_=_||g,4&h.__u?(c=E(h,c,e,!m.__v),m.__e&&(m.__e=null)):typeof h.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),h.__u&=-7);return n.__e=_,c}function T(e,t,n,r,i){var a,o,s,c,l,u,d,f,m,h,g=n.length,y=g,x=0,S=!1,C=e.__k=Array(i);for(a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o!=`object`||o.constructor==String?o=C[a]=_(null,o):p(o)?o=C[a]=_(v,{children:o}):o.constructor===void 0&&o.__b?o=C[a]=_(o.type,o.props,o.key,o.ref,o.__v):C[a]=o,c=a+x,o.__=e,o.__b=e.__b+1,s=null,~(l=o.__i=O(o,n,c,y))&&(y--,(s=n[l])&&(s.__u|=2)),s&&s.__v?(o.__u|=2,l==c-1?x--:l==c+1?x++:l!=c&&(l>c?x--:x++,S=!0)):(~l||(i>g?x--:i<g&&x++),typeof o.type!=`function`&&(o.__u|=4))):C[a]=null;if(S){for(u=[],d=[],a=0;a<i;a++)if((o=C[a])&&2&o.__u){for(f=0,m=u.length;f<m;)u[h=f+m>>1]<o.__i?f=h+1:m=h;u[f]=o.__i,d[a]=f+1}for(x=u.length;a--;)d[a]&&(d[a]==x?x--:C[a].__u|=4)}if(y)for(a=0;a<g;a++)!(s=n[a])||2&s.__u||(s.__e==r&&(r=b(s)),ae(s,s));return r}function E(e,t,n,r){var i,a,o;if(typeof e.type==`function`){if(e.props.__P)return t;if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&(i[a].__=e,t=E(i[a],t,n,!1));return t}for(t&&!t.parentNode&&(t=b(e))&&!t.parentNode&&(t=null),o=t;o&&o.nodeType==8;)o=o.nextSibling;for(e.__e!=o&&(!r&&n.moveBefore&&e.__e.parentNode?n.moveBefore(e.__e,t):n.insertBefore(e.__e,t||null)),t=e.__e;(t=t&&t.nextSibling)&&t.nodeType==8;);return t}function D(e,t){return t=t||[],e!=null&&typeof e!=`boolean`&&(p(e)?e.some(function(e){D(e,t)}):t.push(e)),t}function O(e,t,n,r){var i,a,o,s=e.key,c=e.type,l=t[n],u=l&&!(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++])&&!(2&l.__u)&&s==l.key&&c==l.type)return o}return-1}function k(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function A(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||k(e.style,t,``);if(n)for(t in n)r&&n[t]==r[t]||k(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=j(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,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 j(t){return function(n){if(this.__e){var r=this.__e[t];if(n[o]==null)n[o]=c++;else if(n[o]<this.__a[t])return;return r(e.event?e.event(n):n)}}}function ee(t,n,r,i,a,o,s,c,l,u){var f,m,_,x,S,C,T,E,D,O,k,A,j,ee,ne,ie,se,ce,le,ue,de=n.type;if(n.constructor!==void 0)return null;if(128&r.__u&&(l=32&r.__u,f=r.__c.__z)){if(n.__u|=l,m=o=[],f.nodeType==8)for(_=1,x=f.nextSibling;x;x=x.nextSibling){if(x.nodeType==8){if(x.data.startsWith(`$s`))_++;else if(x.data.startsWith(`/$s`)&&!--_)break}o.push(x)}else o.push(f);c=o[0]}(f=e.__b)&&f(n);n:if(typeof de==`function`){S=s.length;try{if(O=n.props,k=(f=de.prototype)&&f.render,A=(f=de.contextType)&&i[f.__c],j=f?A?A.props.value:f.__:i,r.__c?2&(C=n.__c=r.__c).__g&&(C.__g|=1):(k?n.__c=C=new de(O,j):(n.__c=C=new y(O,j),C.constructor=de,C.render=oe),A&&A.sub(C),C.state||(C.state={}),C.__n=i,C.__g|=8,C.Line truncated
`)>-1?e.split(`
`):e}function Dm(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 Om(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=bl(t.bodyFont),l=bl(t.titleFont),u=bl(t.footerFont),d=a.length,f=i.length,p=r.length,m=yl(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 km(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function Am(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 jm(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`),Am(l,e,t,n)&&(l=`center`),l}function Mm(e,t,n){let r=n.yAlign||t.yAlign||km(e,n);return{xAlign:n.xAlign||t.xAlign||jm(e,t,n,r),yAlign:r}}function Nm(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Pm(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Fm(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}=vl(o),m=Nm(t,s),h=Pm(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:cc(m,0,r.width-t.width),y:cc(h,0,r.height-t.height)}}function Im(e,t,n){let r=yl(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Lm(e){return Tm([],Em(e))}function Rm(e,t,n){return Cl(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function zm(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Bm={beforeTitle:hs,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:hs,beforeBody:hs,beforeLabel:hs,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:hs,afterBody:hs,beforeFooter:hs,footer:hs,afterFooter:hs};function Vm(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Bm[t].call(n,r):i}var Hm=class extends Pf{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 Uu(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Rm(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=Vm(n,`beforeTitle`,this,e),i=Vm(n,`title`,this,e),a=Vm(n,`afterTitle`,this,e),o=[];return o=Tm(o,Em(r)),o=Tm(o,Em(i)),o=Tm(o,Em(a)),o}getBeforeBody(e,t){return Lm(Vm(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];return Z(e Line truncated
`):e}function Im(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 Lm(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=kl(t.bodyFont),l=kl(t.titleFont),u=kl(t.footerFont),d=a.length,f=i.length,p=r.length,m=Ol(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 Rm(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function zm(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 Bm(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`),zm(l,e,t,n)&&(l=`center`),l}function Vm(e,t,n){let r=n.yAlign||t.yAlign||Rm(e,n);return{xAlign:n.xAlign||t.xAlign||Bm(e,t,n,r),yAlign:r}}function Hm(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Um(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function Wm(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}=Dl(o),m=Hm(t,s),h=Um(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:_c(m,0,r.width-t.width),y:_c(h,0,r.height-t.height)}}function Gm(e,t,n){let r=Ol(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Km(e){return Pm([],Fm(e))}function qm(e,t,n){return Ml(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Jm(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Ym={beforeTitle:ws,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:ws,beforeBody:ws,beforeLabel:ws,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:ws,afterBody:ws,beforeFooter:ws,footer:ws,afterFooter:ws};function Xm(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Ym[t].call(n,r):i}var Zm=class extends Uf{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 Qu(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=qm(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=Xm(n,`beforeTitle`,this,e),i=Xm(n,`title`,this,e),a=Xm(n,`afterTitle`,this,e),o=[];return o=Pm(o,Fm(r)),o=Pm(o,Fm(i)),o=Pm(o,Fm(a)),o}getBeforeBody(e,t){return Km(Xm(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];return Z(e Line truncated
`),l]),i&&!t?z(vo,{}):n&&!t?z(yo,{message:n,onRetry:c}):t?z(v,{children:[z(`section`,{class:`rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-6`,children:z(`div`,{class:`flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between`,children:[z(`div`,{class:`flex min-w-0 items-start gap-3 sm:gap-4`,children:[z(`span`,{class:`grid size-12 shrink-0 place-items-center rounded-2xl bg-teal-50 text-teal-700`,children:z(tn,{class:`size-7`})}),z(`div`,{class:`min-w-0`,children:[z(`p`,{class:`m-0 text-xs font-extrabold uppercase tracking-[0.16em] text-teal-700`,children:`Local overrides`}),z(`h2`,{class:`mt-2 mb-0 break-words text-xl font-black tracking-tight text-slate-950 sm:text-2xl`,children:`사용자 도메인 규칙`}),z(`p`,{class:`mt-2 mb-0 max-w-3xl text-sm leading-6 text-slate-600`,children:`허용 목록은 오탐을 해제하고, 차단 목록은 원하는 도메인을 SafeShield 보호 목록에 추가합니다.`})]})]}),z(`dl`,{class:`m-0 grid grid-cols-3 gap-2 sm:min-w-80`,children:[z(`div`,{class:`rounded-xl bg-emerald-50 px-3 py-3 text-center`,children:[z(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-emerald-700`,children:`허용`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-emerald-950`,children:t.counts.allow})]}),z(`div`,{class:`rounded-xl bg-red-50 px-3 py-3 text-center`,children:[z(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-red-700`,children:`차단`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-red-950`,children:t.counts.block})]}),z(`div`,{class:`rounded-xl bg-slate-100 px-3 py-3 text-center`,children:[z(`dt`,{class:`text-[0.68rem] font-extrabold uppercase tracking-wide text-slate-600`,children:`전체`}),z(`dd`,{class:`mt-1 mb-0 ml-0 text-lg font-black text-slate-950`,children:t.counts.total})]})]})]})}),z(`section`,{class:`mt-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm shadow-slate-900/5 sm:p-5`,"aria-label":`추천 사용자 규칙`,children:z(`div`,{class:`flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center`,children:[z(`div`,{class:`min-w-0`,children:[z(`div`,{class:`flex flex-wrap items-center gap-2`,children:[z(`span`,{class:`grid size-8 place-items-center rounded-lg bg-teal-50 text-teal-700`,children:z(H,{class:`size-4`})}),z(`h2`,{class:`m-0 text-base font-extrabold text-slate-950`,children:`추천 사용자 규칙`}),f&&u.length>0?z(`span`,{class:`rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:[`추천으로 추가 `,u.length,`개`]}):null]}),z(`div`,{class:`mt-2 flex flex-wrap items-center gap-2`,"aria-label":`추천 규칙 분류`,children:[z(`span`,{class:`inline-flex items-center rounded-full bg-teal-50 px-2.5 py-1 text-xs font-bold text-teal-800`,children:`공식 추천`}),z(`span`,{class:`inline-flex items-center rounded-full bg-slate-100 px-2.5 py-1 text-xs font-bold text-slate-700`,children:`커뮤니티`})]}),z(`p`,{class:`mt-2 mb-0 text-sm leading-6 text-slate-600`,children:`SmartSafeHub의 공식 추천과 관리자가 검토한 커뮤니티 허용 규칙을 확인하고, 필요한 규칙을 적용해 보세요.`}),z(`p`,{class:`mt-1 mb-0 text-xs leading-5 text-slate-500`,children:`공유기 선택과 적용 관리는 SmartSafeHub 웹사이트에서 진행합니다. 적용 결과는 다음 기기 동기화 후 반영되며, 연결된 모든 기기에 영향을 줍니다.`})]}),z(`a`,{class:`inline-flex min-h-10 w-full shrink-0 items-center justify-center rounded-xl border border-teal-700 bg-teal-700 px-4 py-2.5 text-sm font-extrabold text-white transition hover:bg-teal-800 sm:w-auto`,href:`https://www.smartsafehub.com/dashboard/recommended-rules/`,target:`_blank`,rel:`noopener noreferrer`,children:`추천 규칙 보기 ↗`})]})}),t.applyLocalOverrides?null:z(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-amber-200 bg-amber-50 p-4`,role:`alert`,children:[z(U,{class:`mt-0.5 size-5 shrink-0 text-amber-700`}),z(`div`,{children:[z(`p`,{class:`m-0 text-sm font-extrabold text-amber-950`,children:`로컬 규칙 적용이 꺼져 있습니다`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-amber-800`,children:`규칙은 저장되지만 SafeShield의 apply_local_overrides 설정을 켜기 전까지 DNS에 반영되지 않습니다.`})]})]}),t.safeshieldEnabled?null:z(`section`,{class:`mt-4 flex items-start gap-3 rounded-2xl border border-slate-200 bg-slate-100 p-4`,role:`status`,children:[z(At,{class:`mt-0.5 size-5 shrink-0 text-slate-600`}),z(`div`,{children:[z(`p`,{class:`m-0 text-sm font-extrabold text-slate-950`,children:`SafeShield가 비활성화되어 있습니다`}),z(`p`,{class:`mt-1 mb-0 text-sm leading-6 text-slate-600`,children:`규칙을 미리 저장할 수 있으며, SafeShield를 켜고 차단 목록을 갱신하면 적용됩니다.`})]})]}),r?z(`section`,{"aria-live":`polite`,class:`mt-4 flex flex-col items-start gap-3 rounded-2xl border border-teal-200 Line truncated
@@ -28,6 +28,13 @@ Describe 'SmartSafeHub 셸 계약 테스트'
The output should start with 'PASS:'
The error should be blank
End
It '공유기 데모의 실제 기기 접근 차단과 Git 배포 계약을 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-router-demo-contract.sh"
The status should be success
The output should start with 'PASS: router demo contract'
The error should be blank
End
It '게스트 Wi-Fi 분리망과 관리 UI 보안 계약을 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-guest-wifi-contract.sh"
The status should be success
@@ -56,6 +63,13 @@ Describe 'SmartSafeHub 셸 계약 테스트'
The error should be blank
End
It '추천 사용자 규칙의 적용과 해제 및 수동 규칙 보존을 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-recommended-rules.sh"
The status should be success
The output should start with '추천 사용자 규칙 동기화 및 UI 계약: PASS'
The error should be blank
End
It '계정 기반 Cloud 활동 동기화와 직접/관찰 이벤트 분리를 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-activity-cloud-sync.sh"
The status should be success
@@ -189,6 +203,13 @@ Describe 'SmartSafeHub 셸 계약 테스트'
The error should be blank
End
It '업데이트 및 SafeShield 상대 시간 표시 계약을 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-relative-time-contract.sh"
The status should be success
The output should start with 'PASS:'
The error should be blank
End
It '업데이트 UI 계약을 검증한다'
When run command sh "$SHELLSPEC_PROJECT_ROOT/tests/test-update-ui-contract.sh"
The status should be success
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
formatRecentTimestamp,
formatTimestamp,
parseArtifactVersionTimestamp,
} from '../../frontend/src/app/format.ts';
const NOW = 1_800_000_000;
test('시간 표시: 분·시간·일 경계와 방금 전을 구분한다', () => {
assert.equal(formatRecentTimestamp(NOW, NOW), '방금 전');
assert.equal(formatRecentTimestamp(NOW - 59, NOW), '방금 전');
assert.equal(formatRecentTimestamp(NOW - 60, NOW), '1분 전');
assert.equal(formatRecentTimestamp(NOW - 3599, NOW), '59분 전');
assert.equal(formatRecentTimestamp(NOW - 3600, NOW), '1시간 전');
assert.equal(formatRecentTimestamp(NOW - 86399, NOW), '23시간 전');
assert.equal(formatRecentTimestamp(NOW - 86400, NOW), '1일 전');
assert.equal(formatRecentTimestamp(NOW - 6 * 86400, NOW), '6일 전');
});
test('시간 표시: 7일 이상은 날짜로 바꾸고 잘못된 값은 무시한다', () => {
assert.equal(formatRecentTimestamp(NOW - 7 * 86400, NOW), formatTimestamp(NOW - 7 * 86400));
assert.equal(formatRecentTimestamp(NOW - 30 * 86400, NOW), formatTimestamp(NOW - 30 * 86400));
assert.equal(formatRecentTimestamp(NOW + 300, NOW), '방금 전');
assert.equal(formatRecentTimestamp(NOW + 301, NOW), formatTimestamp(NOW + 301));
assert.equal(formatRecentTimestamp(NOW, NaN), formatTimestamp(NOW));
for (const timestamp of [0, -1, NaN, Infinity, 8.64e12 + 1]) {
assert.equal(formatRecentTimestamp(timestamp, NOW), '기록 없음');
}
});
test('보호 데이터 버전: 정확한 UTC 시각을 변환한다', () => {
assert.equal(parseArtifactVersionTimestamp('20261016T020000Z'), Date.UTC(2026, 9, 16, 2) / 1000);
assert.equal(parseArtifactVersionTimestamp('20240229T235959Z'), Date.UTC(2024, 1, 29, 23, 59, 59) / 1000);
});
test('보호 데이터 버전: 누락·형식 오류·존재하지 않는 날짜를 거부한다', () => {
for (const value of [null, '', '20261016T020000', '20261016T020000+0900', '20230229T120000Z', '20261316T120000Z', '20261016T250000Z', '20261016T026000Z', '20261016T020060Z']) {
assert.equal(parseArtifactVersionTimestamp(value), null);
}
});
@@ -0,0 +1,81 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve, dirname, extname } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../frontend/src');
const visited = new Set();
function inspectImports(path) {
if (visited.has(path)) return;
visited.add(path);
const source = readFileSync(path, 'utf8');
// Follow executable imports only; TypeScript `import type` never reaches JS.
const regex = /^\s*import\s+(?!type\b)(?:[^;]*?\bfrom\s+)?["'](\.[^"']+)["']/gm;
for (const [, local] of source.matchAll(regex)) {
const base = resolve(dirname(path), local);
const target = extname(base) ? base : `${base}.tsx`;
let resolved = target;
try { readFileSync(resolved); } catch { resolved = `${base}.ts`; }
assert.ok(!resolved.includes('/api/'), `API dependency in demo: ${resolved}`);
assert.ok(!resolved.includes('/auth/'), `Authentication dependency in demo: ${resolved}`);
inspectImports(resolved);
}
}
test('standalone demo has no executable RPC/auth dependency', () => {
inspectImports(resolve(root, 'demo/main.tsx'));
assert.ok(visited.size > 10, 'Presentational components must be traversed');
});
test('fictional MAC addresses and documentation IP ranges are used', () => {
const fixtures = readFileSync(resolve(root, 'demo/data.ts'), 'utf8');
assert.match(fixtures, /02:00:00:00:00:01/);
assert.match(fixtures, /198\.51\.100\.20/);
assert.doesNotMatch(fixtures, /172\.16\.3\./);
});
test('public demo routes and fixture-backed pages match navigation', () => {
const routes = readFileSync(resolve(root, 'app/routes.ts'), 'utf8');
const app = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8');
const sidebar = readFileSync(resolve(root, 'components/ProductNavigation.tsx'), 'utf8');
const required = ['home', 'activity', 'network', 'wifi', 'devices', 'safeshield', 'rules', 'system'];
for (const route of required) {
assert.match(routes, new RegExp(`'${route}'`));
if (route !== 'home') assert.ok(app.includes(`case '${route}':`), `missing demo route ${route}`);
}
for (const component of ['ActivityPage', 'WanPage', 'LanPage', 'WifiPage', 'SafeShieldRulesPage', 'UpdatePage']) {
assert.ok(app.includes(`<${component}`), `missing real page ${component}`);
}
assert.ok(sidebar.includes('DEMO_ROUTES.includes(name)'), 'navigation must share allowed demo routes');
assert.match(app, /text-teal-800/, 'dark theme must map the notice text color');
assert.doesNotMatch(app, /text-teal-900/, 'notice would become illegible in dark mode');
});
test('demo actions never call RPC and Wi-Fi credentials are injected', () => {
const app = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8');
const qr = readFileSync(resolve(root, 'components/WifiQrDialog.tsx'), 'utf8');
const data = readFileSync(resolve(root, 'demo/data.ts'), 'utf8');
assert.match(app, /const disabledAction = async \(\): Promise<boolean> => \{/);
assert.match(app, /return false;/);
for (const prop of ['onSave', 'onUpdate', 'onUpdateGuest', 'onAddRule', 'onDeleteRule', 'onSaveSettings', 'onUpload']) {
assert.ok(app.includes(`${prop}={disabledAction}`) || (prop === 'onUpload' && app.includes('upload: disabledAction')), `unsafe demo action: ${prop}`);
}
assert.doesNotMatch(qr, /from ['"]\.\.\/api\//);
assert.match(qr, /void load\(\)\.then/);
assert.match(data, /demoWifiQr = async/);
assert.match(data, /demoGuestWifiQr = async/);
});
// The shared rules page must remain presentational: only the authenticated
// app supplies the status reader, never the fixture-backed public demo.
test('추천 규칙 API 조회는 실제 공유기 앱에서만 주입된다', () => {
const page = readFileSync(resolve(root, 'pages/SafeShieldRulesPage.tsx'), 'utf8');
const app = readFileSync(resolve(root, 'app/App.tsx'), 'utf8');
const demo = readFileSync(resolve(root, 'demo/main.tsx'), 'utf8');
assert.doesNotMatch(page, /from ['"]\.\.\/api\//);
assert.match(page, /if \(!onLoadRecommendedRulesStatus\) return;/);
assert.match(app, /onLoadRecommendedRulesStatus=\{fetchRecommendedRulesStatus\}/);
assert.doesNotMatch(demo, /onLoadRecommendedRulesStatus=/);
});
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const page = readFileSync(resolve(root, 'frontend/src/pages/SafeShieldRulesPage.tsx'), 'utf8');
const rpc = readFileSync(resolve(root, 'root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc'), 'utf8');
const helper = readFileSync(resolve(root, 'root/usr/libexec/smartsafehub-recommended-rules'), 'utf8');
test('추천 규칙 영역은 공식 추천과 커뮤니티를 구분하여 안내한다', () => {
assert.match(page, /aria-label="추천 규칙 분류"/);
assert.match(page, /공식 추천<\/span>/);
assert.match(page, /커뮤니티<\/span>/);
assert.match(page, /관리자가 검토한 커뮤니티 허용 규칙/);
assert.doesNotMatch(page, /다른 사용자가 공유한 검증된 허용 규칙/);
});
test('허용 목록의 수동 규칙과 추천 규칙을 로컬 소유 정보로 구분한다', () => {
assert.match(rpc, /recommended-rules\.state/);
assert.match(rpc, /domains: domains/);
assert.match(helper, /grep -Fxq "\$domain" "\$OWNED"/);
assert.match(page, /const cloudManaged = isAllow && recommendedDomains\.includes\(domain\);/);
assert.match(page, /cloudManaged \? '추천 규칙' : '직접 추가'/);
assert.match(page, /disabled=\{busy \|\| cloudManaged\}/);
});
test('허용 규칙 출처를 조회하지 못하면 직접 추가로 단정하지 않는다', () => {
assert.match(page, /const ruleOrigin = !isAllow \|\| recommendedKnown/);
assert.match(page, /: null;/);
assert.match(page, /recommendedKnown=\{recommendedKnown\}/g);
assert.match(page, /!onLoadRecommendedRulesStatus\) return;/);
assert.doesNotMatch(page, /from ['"]\.\.\/api\//);
});
+43
View File
@@ -0,0 +1,43 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
RPC="$ROOT_DIR/root/usr/share/rpcd/ucode/smartsafehub/firmware.uc"
FIRMWARE_TYPES="$ROOT_DIR/frontend/src/types/firmware.ts"
FIRMWARE_CARD="$ROOT_DIR/frontend/src/components/FirmwareUpdatesCard.tsx"
SOFTWARE_CARD="$ROOT_DIR/frontend/src/components/SoftwareUpdatesCard.tsx"
UPDATE_PAGE="$ROOT_DIR/frontend/src/pages/UpdatePage.tsx"
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
# Firmware resolve contract must prefer the published release field while still
# supporting metadata nested under Hub's firmware build record.
grep -Fq 'release?.bundled_software ?? release?.metadata?.bundled_software' "$RPC" || fail 'Hub bundled version is not read'
grep -Fq 'match(bundled_version, /^[0-9]+\.[0-9]+\.[0-9]+(-r[0-9]+)?$/)' "$RPC" || fail 'Hub bundled version must be validated'
grep -Fq 'bundledSoftware: bundled_version != null ? { smartsafehub: bundled_version } : null' "$RPC" || fail 'bundled software version must be exposed on firmware status'
grep -Fq 'bundledSoftware?: { smartsafehub: string } | null' "$FIRMWARE_TYPES" || fail 'typed bundled version missing'
grep -Fq 'software={data}' "$UPDATE_PAGE" || fail 'installed software status not passed to firmware card'
grep -Fq 'firmware={firmware.data}' "$UPDATE_PAGE" || fail 'firmware status not passed to management card'
grep -Fq 'data-section="firmware-bundled-software"' "$FIRMWARE_CARD" || fail 'bundled software display missing'
grep -Fq 'data-section="software-firmware-match"' "$SOFTWARE_CARD" || fail 'management software explanation missing'
grep -Fq "firmware?.updateAvailable" "$SOFTWARE_CARD" || fail 'management hint must depend on available firmware'
grep -Fq 'data.updateCount === 0 && !updateCheckStale' "$SOFTWARE_CARD" || fail 'stale/pending software updates must not claim no update needed'
# Real comparison logic: unchanged, newer/older, missing or unexpected versions.
ROOT_DIR="$ROOT_DIR" node --disable-warning=ExperimentalWarning --experimental-strip-types --input-type=module <<'JS'
import assert from 'node:assert/strict';
import { pathToFileURL } from 'node:url';
const root = process.env.ROOT_DIR;
const { compareBundledVersion } = await import(pathToFileURL(`${root}/frontend/src/utils/firmwareBundling.ts`).href);
assert.equal(compareBundledVersion('0.2.28-r4', '0.2.28-r4'), 'same');
assert.equal(compareBundledVersion('0.2.28-r5', '0.2.28-r4'), 'newer');
assert.equal(compareBundledVersion('0.2.28-r1', '0.2.28-r4'), 'older');
assert.equal(compareBundledVersion('0.2.29-r1', '0.2.28-r9'), 'newer');
assert.equal(compareBundledVersion('0.2.27-r9', '0.2.28-r1'), 'older');
assert.equal(compareBundledVersion('0.2.28', '0.2.28-r0'), 'same');
assert.equal(compareBundledVersion('0.2.28-rc1', '0.2.28-r1'), 'different');
assert.equal(compareBundledVersion(null, '0.2.28-r4'), 'unknown');
assert.equal(compareBundledVersion('0.2.28-r4', null), 'unknown');
JS
printf '%s\n' 'PASS: bundled firmware software version pipeline and comparison rules'
@@ -108,8 +108,9 @@ grep -q 'aria-controls="ssh-guest-settings"' "$PAGE"
grep -q "expanded ? '' : 'hidden'" "$PAGE"
grep -q 'busy || dirty || !guest.enabled || !guest.passwordConfigured' "$PAGE"
grep -q '게스트 Wi-Fi QR 코드 보기' "$PAGE"
grep -q 'WifiQrDialog guest section="ssh_guest"' "$PAGE"
grep -q "fetchGuestWifiQr() : fetchWifiQr(section)" frontend/src/components/WifiQrDialog.tsx
grep -Fq 'WifiQrDialog load={onLoadQr}' "$PAGE"
grep -Fq 'onLoadGuestQr={fetchGuestWifiQr}' frontend/src/app/App.tsx
grep -Fq 'void load().then((data)' frontend/src/components/WifiQrDialog.tsx
grep -q "callApi(GUEST_API_OBJECT, 'wifi_guest_qr'" frontend/src/api/smartsafehub.ts
# The guest card is separated from the primary Wi-Fi cards, with a full-width
# line only when guest settings are available. Reuse the theme-aware border token.
@@ -45,7 +45,9 @@ grep -Fq "const lan = useLan(route === 'home' || route === 'network');" "$APP" |
grep -Fq "case 'network':" "$APP" || fail 'App must render the combined network route'
grep -Fq 'aria-label="인터넷 연결 설정"' "$APP" || fail 'combined page must keep a distinct WAN region'
grep -Fq 'aria-label="내부 네트워크 설정"' "$APP" || fail 'combined page must keep a distinct LAN region'
grep -Fq 'class="mt-8 min-w-0 border-t border-slate-200 pt-8"' "$APP" || fail 'WAN and LAN regions must have a clear visual boundary'
grep -Fq "hidden={tabs.network !== 'internet'}" "$APP" || fail 'WAN tab must hide while LAN is active'
grep -Fq "hidden={tabs.network !== 'lan'}" "$APP" || fail 'LAN tab must hide while WAN is active'
grep -Fq "href: '#network/lan'" "$APP" || fail 'LAN tab should have a linkable URL'
grep -Fq '<WanPage' "$APP" || fail 'combined page must reuse the WAN settings implementation'
grep -Fq '<LanPage' "$APP" || fail 'combined page must reuse the LAN settings implementation'
grep -Fq "if (route === 'network')" "$APP" || fail 'combined route must have an explicit global refresh path'
@@ -69,6 +69,8 @@ require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-firmware"
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-device"
require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-recommended-rules"
require_executable "$ROOT_DIR/tests/test-recommended-rules.sh"
require_executable "$ROOT_DIR/root/usr/libexec/smartsafehub-backup"
[ ! -e "$ROOT_DIR/root/lib/upgrade/keep.d/smartsafehub" ] || fail "firmware.json must not be preserved through a SmartSafeHub keep.d rule"
require_file "$ROOT_DIR/root/lib/upgrade/keep.d/smartsafehub-device"
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
APP="$ROOT_DIR/frontend/src/app/App.tsx"
SETTINGS="$ROOT_DIR/frontend/src/pages/SettingsPage.tsx"
DEMO="$ROOT_DIR/frontend/src/demo/main.tsx"
HOOK="$ROOT_DIR/frontend/src/hooks/usePageTabs.ts"
HASH="$ROOT_DIR/frontend/src/hooks/useHashRoute.ts"
TABS="$ROOT_DIR/frontend/src/components/PageTabs.tsx"
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
for file in "$APP" "$SETTINGS" "$DEMO" "$HOOK" "$HASH" "$TABS"; do
[ -f "$file" ] || fail "missing ${file#$ROOT_DIR/}"
done
grep -Fq "'#network/lan': 'network'" "$HASH" || fail 'LAN deep link must open network route'
grep -Fq "'#settings/time': 'settings'" "$HASH" || fail 'time deep link must open settings route'
grep -Fq "'#settings/management': 'settings'" "$HASH" || fail 'management deep link must open settings route'
grep -Fq "'#wan': 'network'" "$HASH" || fail 'legacy WAN link must work'
grep -Fq "'#lan': 'network'" "$HASH" || fail 'legacy LAN link must work'
grep -Fq "hash === '#lan' || hash === '#network/lan'" "$HOOK" || fail 'legacy LAN link must activate LAN panel'
grep -Fq "return 'diagnostics';" "$HOOK" || fail 'diagnostics must be settings default'
grep -Fq "window.addEventListener('hashchange', onHashChange)" "$HOOK" || fail 'tabs must react to history/back/forward'
grep -Fq 'activeTab={tabs.settings}' "$APP" || fail 'settings tab must be selected from URL'
grep -Fq 'label="설정 메뉴"' "$SETTINGS" || fail 'missing settings tabs'
grep -Fq "{ id: 'diagnostics', label: '진단 및 지원', href: '#settings' }" "$SETTINGS" || fail 'diagnostics must appear as first tab'
grep -Fq "{ id: 'time', label: '시간 및 예약', href: '#settings/time' }" "$SETTINGS" || fail 'time tab must be directly addressable'
grep -Fq "{ id: 'management', label: '관리 및 복구', href: '#settings/management' }" "$SETTINGS" || fail 'management tab must be directly addressable'
grep -Fq "hidden={activeTab !== 'diagnostics'}" "$SETTINGS" || fail 'diagnostics content must be hidden on other tabs'
grep -Fq "hidden={activeTab !== 'time'}" "$SETTINGS" || fail 'time content must be hidden on other tabs'
grep -Fq "hidden={activeTab !== 'management'}" "$SETTINGS" || fail 'management content must be hidden on other tabs'
grep -Fq 'loading && !data' "$SETTINGS" || fail 'diagnostics should handle status loading gracefully'
if grep -Fq '<InfoCard' "$SETTINGS"; then fail 'settings still repeats dashboard system status'; fi
# Keep all existing form components mounted across tab changes: no unmounting
# means unsaved PPPoE, LAN, scheduled reboot and password inputs survive.
grep -Fq "hidden={tabs.network !== 'internet'}" "$APP" || fail 'WAN form must be retained while LAN is selected'
grep -Fq "hidden={tabs.network !== 'lan'}" "$APP" || fail 'LAN form must be retained while WAN is selected'
grep -Fq '<WanPage' "$APP" || fail 'WAN form missing'
grep -Fq '<LanPage' "$APP" || fail 'LAN form missing'
grep -Fq '<TimeSettingsCard' "$SETTINGS" || fail 'time form missing'
grep -Fq '<AdministratorPasswordCard' "$SETTINGS" || fail 'administrator form missing'
grep -Fq 'hidden={tabs.network !==' "$DEMO" || fail 'demo network tabs should mirror router UI'
grep -Fq 'role="tablist"' "$TABS" || fail 'accessible tab list missing'
grep -Fq 'aria-selected={selected}' "$TABS" || fail 'selected tab announcement missing'
grep -Fq 'aria-controls=' "$TABS" || fail 'tab/panel association missing'
grep -Fq "'ArrowLeft', 'ArrowRight', 'Home', 'End'" "$TABS" || fail 'keyboard traversal missing'
grep -Fq 'grid-cols-3' "$TABS" || fail 'mobile-friendly settings tab grid missing'
grep -Fq 'grid w-full min-w-0 gap-2' "$TABS" || fail 'tabs should fill available width on mobile'
grep -Fq 'sm:max-w-[592px]' "$TABS" || fail 'three settings tabs should be capped to 192px each on wider screens'
grep -Fq 'sm:max-w-[392px]' "$TABS" || fail 'two network tabs should be capped to 192px each on wider screens'
grep -Fq 'min-w-0' "$TABS" || fail 'tab layout must avoid mobile overflow'
echo 'PASS: settings/network tabs, direct links, keyboard access, mobile widths and preserved forms'
+149
View File
@@ -0,0 +1,149 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
HELPER="$ROOT/root/usr/libexec/smartsafehub-recommended-rules"
DEVICE="$ROOT/root/usr/libexec/smartsafehub-device"
PAGE="$ROOT/frontend/src/pages/SafeShieldRulesPage.tsx"
RPC="$ROOT/root/usr/share/rpcd/ucode/smartsafehub.uc"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT HUP INT TERM
mkdir -p "$TMP/bin" "$TMP/run" "$TMP/etc"
cat > "$TMP/bin/jsonfilter" <<'JS'
#!/usr/bin/env node
const fs=require('fs'), args=process.argv.slice(2);
let obj=JSON.parse(fs.readFileSync(args[args.indexOf('-i')+1]));
const expr=args[args.indexOf('-e')+1].replace(/^@\./,'').replace(/\[\*\]$/,'');
for (const key of expr.split('.')) obj=obj && !Array.isArray(obj) ? obj[key] : undefined;
if (Array.isArray(obj)) for (const item of obj) console.log(item);
else if (obj !== undefined && obj !== null) console.log(obj);
JS
cat > "$TMP/bin/ubus" <<'JS'
#!/usr/bin/env node
const fs=require('fs'),path=require('path');
const dir=process.env.MOCK_RECOMMENDED_DIR, method=process.argv[4], file=path.join(dir,'rules.json');
let rules=JSON.parse(fs.readFileSync(file,'utf8'));
if (method==='rules_list') {
console.log(process.env.BROKEN_LIST==='1' ? '{"ok":true}' : JSON.stringify({ok:true,allow:rules,block:[]}));
process.exit(0);
}
if (process.env.FAIL_METHOD === method) { console.log('{"ok":false}'); process.exit(1); }
let domain='';
if (method==='rule_add'||method==='rule_delete') {
const data=JSON.parse(process.argv[5]);
if (data.action!=='allow'||data.refresh!==false) process.exit(1);
domain=data.domain;
if (method==='rule_add' && !rules.includes(domain)) rules.push(domain);
if (method==='rule_delete') rules=rules.filter(v=>v!==domain);
fs.writeFileSync(file,JSON.stringify(rules));
}
fs.appendFileSync(path.join(dir,'calls.log'),method+' '+domain+'\n');
console.log('{"ok":true}');
JS
chmod +x "$TMP/bin/ubus" "$TMP/bin/jsonfilter"
printf '["manual.example.org"]' > "$TMP/rules.json"
export MOCK_RECOMMENDED_DIR="$TMP"
export SMARTSAFEHUB_RECOMMENDED_STATE_FILE="$TMP/etc/state"
export SMARTSAFEHUB_RECOMMENDED_RECEIPT_FILE="$TMP/etc/receipt.json"
export SMARTSAFEHUB_DEVICE_JSONFILTER_BIN="$TMP/bin/jsonfilter"
export SMARTSAFEHUB_DEVICE_UBUS_BIN="$TMP/bin/ubus"
export SMARTSAFEHUB_DEVICE_RUNTIME_DIR="$TMP/run"
response() {
python3 - "$TMP/response.json" "$1" <<'PY'
import sys,json,hashlib
items=json.loads(sys.argv[2]); h=hashlib.sha256('\n'.join(sorted(set(items))).encode()).hexdigest()
json.dump({'recommended_rules':{'revision':h,'domains':items}},open(sys.argv[1],'w'))
PY
}
assert_rules() {
python3 - "$TMP/rules.json" "$1" <<'PY'
import json,sys
assert sorted(json.load(open(sys.argv[1])))==sorted(json.loads(sys.argv[2]))
PY
}
# Existing manual entries are never claimed even if the cloud recommends them.
response '["tv.example.org","manual.example.org"]'
"$HELPER" "$TMP/response.json"
assert_rules '["manual.example.org","tv.example.org"]'
grep -Fqx 'tv.example.org' "$TMP/etc/state"
! grep -Fqx 'manual.example.org' "$TMP/etc/state"
# A pre-existing allow entry is confirmed but never attributed to Hub.
python3 - "$TMP/etc/receipt.json" <<'PY_CHECK'
import json,sys
receipt=json.load(open(sys.argv[1]))
assert receipt['status']=='confirmed'
assert {d['domain']:d['source'] for d in receipt['domains']} == {'manual.example.org':'manual','tv.example.org':'managed'}
PY_CHECK
# Stable revisions must not call refresh again.
count="$(wc -l < "$TMP/calls.log")"
"$HELPER" "$TMP/response.json"
[ "$count" -eq "$(wc -l < "$TMP/calls.log")" ]
# Removing one recommendation must only remove the locally owned entry.
response '[]'; "$HELPER" "$TMP/response.json"
assert_rules '["manual.example.org"]'
# Broken responses must not be interpreted as an empty rules list.
response '["tv.example.org"]'
if BROKEN_LIST=1 "$HELPER" "$TMP/response.json"; then exit 1; fi
[ ! -f "$TMP/etc/receipt.json" ]
assert_rules '["manual.example.org"]'
# Rule mutation failures must not acknowledge a newer revision.
if FAIL_METHOD=rule_add "$HELPER" "$TMP/response.json"; then exit 1; fi
[ ! -f "$TMP/etc/receipt.json" ]
expected="$(python3 - "$TMP/response.json" <<'PY'
import sys,json
print(json.load(open(sys.argv[1]))['recommended_rules']['revision'])
PY
)"
[ "$(sed -n '1p' "$TMP/etc/state")" != "$expected" ]
"$HELPER" "$TMP/response.json"
assert_rules '["manual.example.org","tv.example.org"]'
# Failed refresh retries without deleting or duplicating recommendations.
response '[]'
if FAIL_METHOD=refresh "$HELPER" "$TMP/response.json"; then exit 1; fi
[ ! -f "$TMP/etc/receipt.json" ]
[ "$(sed -n '1p' "$TMP/etc/state")" = '-' ]
"$HELPER" "$TMP/response.json"
assert_rules '["manual.example.org"]'
[ "$(sed -n '1p' "$TMP/etc/state")" = "$expected" ] && exit 1
# Bad domain or malformed hash must never mutate SafeShield.
response '["evil.example.org;reboot"]'
if "$HELPER" "$TMP/response.json"; then exit 1; fi
! grep -Fq evil.example.org "$TMP/calls.log"
printf '{"recommended_rules":{"revision":"invalid","domains":[]}}\n' > "$TMP/response.json"
if "$HELPER" "$TMP/response.json"; then exit 1; fi
# Older Hub versions do not opt into the cloud-managed list.
printf '{}\n' > "$TMP/response.json"
"$HELPER" "$TMP/response.json"
assert_rules '["manual.example.org"]'
# The UI must not allow manual deletion of entries owned by the cloud.
grep -Fq 'device_recommended_rules_status' "$RPC"
# A missing semicolon after an exported ucode function prevents the whole
# smartsafehub rpcd object from loading, including the security status RPC.
# Check this even on hosts without the ucode compiler (real compilation is
# separately mandatory in CI via tests/test-ucode-syntax.sh).
DEVICE_RPC="$ROOT/root/usr/share/rpcd/ucode/smartsafehub/device-registration.uc"
awk '
/^export function read_device_registration_status\(/ {
found = 1
if (previous !~ /^[[:space:]]*};[[:space:]]*$/) exit 1
}
{ if ($0 !~ /^[[:space:]]*$/) previous = $0 }
END { if (!found) exit 1 }
' "$DEVICE_RPC" || {
echo '추천 규칙 RPC 함수는 }; 로 끝나야 합니다 (ucode 모듈 로딩 실패 방지).' >&2
exit 1
}
# Keep the shared rules page independent from executable API imports so that
# the public router demo does not inherit authenticated RPC dependencies.
grep -Fq 'onLoadRecommendedRulesStatus' "$PAGE"
! grep -Fq "from '../api/" "$PAGE"
! grep -Fq 'from "../api/' "$PAGE"
grep -Fq 'onLoadRecommendedRulesStatus={fetchRecommendedRulesStatus}' "$ROOT/frontend/src/app/App.tsx"
grep -Fq 'disabled={busy || cloudManaged}' "$PAGE"
grep -Fq "cloudManaged ? '추천 규칙' : '직접 추가'" "$PAGE"
grep -Fq 'aria-label="추천 규칙 분류"' "$PAGE"
grep -Fq 'https://www.smartsafehub.com/dashboard/recommended-rules/' "$PAGE"
grep -Fq 'RECOMMENDED_HELPER' "$DEVICE"
grep -Fq 'recommended_rules_ack' "$DEVICE"
grep -Fq '/etc/smartsafehub/recommended-rules.state' "$ROOT/root/lib/upgrade/keep.d/smartsafehub-device"
echo '추천 사용자 규칙 동기화 및 UI 계약: PASS'
@@ -0,0 +1,32 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
COMPONENT="$ROOT/frontend/src/components/RelativeTimestamp.tsx"
FORMAT="$ROOT/frontend/src/app/format.ts"
FIRMWARE="$ROOT/frontend/src/components/FirmwareUpdatesCard.tsx"
SOFTWARE="$ROOT/frontend/src/components/SoftwareUpdatesCard.tsx"
SAFE="$ROOT/frontend/src/pages/SafeShieldPage.tsx"
fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
for file in "$COMPONENT" "$FORMAT" "$FIRMWARE" "$SOFTWARE" "$SAFE" "$ROOT/tests/runtime/relative-time.test.mjs"; do
[ -f "$file" ] || fail "상대 시간 소스가 없습니다: $file"
done
grep -Fq 'formatRecentTimestamp(timestamp, nowTimestamp)' "$COMPONENT" || fail '공통 경과 시간 계산이 필요합니다'
grep -Fq 'window.setInterval(' "$COMPONENT" || fail '시간이 지나면 화면을 갱신해야 합니다'
grep -Fq '60_000' "$COMPONENT" || fail '1분 주기로 갱신해야 합니다'
grep -Fq 'window.clearInterval(timer)' "$COMPONENT" || fail '페이지 이탈 시 타이머를 정리해야 합니다'
grep -Fq 'dateTime={new Date(timestamp * 1000).toISOString()}' "$COMPONENT" || fail '표준 time 요소에 정확한 시각이 필요합니다'
grep -Fq 'aria-label="정확한 날짜 및 시간 확인"' "$COMPONENT" || fail '모바일/키보드에서도 정확한 시각을 확인해야 합니다'
grep -Fq 'group-open:inline-block' "$COMPONENT" || fail '클릭 시 날짜와 시각을 보여줘야 합니다'
grep -Fq 'nowTimestamp - timestamp >= 7 * 86_400' "$FORMAT" || fail '7일 이상 지난 시간은 날짜로 표시해야 합니다'
grep -Fq '<RelativeTimestamp fallback="아직 없음" timestamp={data.lastCheckAt} />' "$FIRMWARE" || fail '펌웨어 마지막 확인 경과 시간을 표시해야 합니다'
grep -Fq '<RelativeTimestamp fallback="아직 없음" timestamp={data.lastCheckAt} />' "$SOFTWARE" || fail '소프트웨어 마지막 확인 경과 시간을 표시해야 합니다'
grep -Fq '<SummaryFact label="Last refresh" value={<RelativeTimestamp timestamp={data.timestamps.lastSuccess} />} />' "$SAFE" || fail 'SafeShield 마지막 갱신 경과 시간을 표시해야 합니다'
grep -Fq 'parseArtifactVersionTimestamp(version)' "$SAFE" || fail '보호 데이터 버전의 UTC 시각을 파싱해야 합니다'
grep -Fq '<RelativeTimestamp timestamp={timestamp} />' "$SAFE" || fail '보호 데이터 마지막 업데이트를 경과 시간으로 표시해야 합니다'
grep -Fq 'label="다음 갱신" value={formatTimestamp(data.timestamps.nextRefreshAt)}' "$SAFE" || fail '다음 갱신 예정 시각은 절대 시간으로 유지해야 합니다'
printf 'PASS: relative time presentation contract\n'
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
DEMO="$ROOT/frontend/src/demo/main.tsx"
FIXTURES="$ROOT/frontend/src/demo/data.ts"
CONFIG="$ROOT/frontend/vite.demo.config.ts"
for path in "$DEMO" "$FIXTURES" "$CONFIG"; do
[ -f "$path" ] || { echo "missing demo source: $path" >&2; exit 1; }
done
grep -Fq "<HomePage" "$DEMO"
grep -Fq "<SafeShieldPage" "$DEMO"
grep -Fq "<ConnectedDevicesPage" "$DEMO"
for page in ActivityPage WanPage LanPage WifiPage SafeShieldRulesPage UpdatePage; do
grep -Fq "<$page" "$DEMO"
done
grep -Fq "demo={demo}" "$ROOT/frontend/src/components/AppShell.tsx"
grep -Fq "!demo ?" "$ROOT/frontend/src/components/ProductNavigation.tsx"
grep -Fq "base: '/static/router-demo/'" "$CONFIG"
# No fake bootstrap or real RPC in the public demo entry.
if grep -Eq 'callApi|callRpc|fetch\(|probeLuciSession|__SMARTHUB_BOOTSTRAP__|/cgi-bin/' "$DEMO" "$FIXTURES"; then
echo 'Public demo must not call any router APIs' >&2
exit 1
fi
# Keep the Node.js test runner output out of ShellSpec stdout on success.
# Preserve its full diagnostics on stderr if any runtime contract fails.
node_test_log="$(mktemp)"
trap 'rm -f "$node_test_log"' 0
if ! node --test "$ROOT/tests/runtime/router-demo.test.mjs" >"$node_test_log" 2>&1; then
cat "$node_test_log" >&2
exit 1
fi
# The Hub consumes a committed, separately built bundle. New developers build
# it locally, then commit the output; this test does not generate build files.
if grep -q '^frontend/demo-dist/$' "$ROOT/.gitignore"; then
echo 'Demo build output must be committed, not ignored' >&2
exit 1
fi
grep -Fq 'Build standalone router demo' "$ROOT/.github/workflows/ci.yml"
grep -Fq 'Validate committed router demo assets' "$ROOT/.github/workflows/ci.yml"
printf "%s\n" "PASS: router demo contract"
@@ -26,4 +26,39 @@ if grep -Fq 'absolute top-1/2 left-3.5 size-4 -translate-y-1/2' "$PAGE"; then
fail 'search icon must not use transform-based vertical positioning'
fi
echo 'PASS: SafeShield rules input and search icon contracts are present'
# The add form must clear after the rules API confirms persistence, not
# after the slower SafeShield local DNS apply has completed (up to 45 s).
HOOK="$ROOT_DIR/frontend/src/hooks/useSafeShieldRules.ts"
grep -Fq 'onPersisted?.();' "$HOOK" || \
fail 'persisted rule must notify the form before DNS apply finishes'
grep -Fq "mutate('add', action, domain, onPersisted)" "$HOOK" || \
fail 'addRule must forward the persistence callback'
grep -Fq 'await onAddRule(kind, domain, () => {' "$PAGE" || \
fail 'rule form must clear from persistence callback, not final DNS result'
grep -Fq "setInput((current) => normalizeDomain(current) === domain ? '' : current)" "$PAGE" || \
fail 'successful saves must clear only the submitted value'
if grep -Fq 'if (saved)' "$PAGE"; then
fail 'input must not wait for final DNS apply before clearing'
fi
# Verify acknowledgement is after the API call and before DNS verification.
if ! awk '
/await addSafeShieldRule\(action, domain, true\)/ { api = NR }
/onPersisted\?\.\(\)/ { acknowledged = NR }
/outcome = await waitForLocalApply\(baseline\)/ { applied = NR }
END { exit !(api > 0 && acknowledged > api && applied > acknowledged) }
' "$HOOK"; then
fail 'save acknowledgement must happen after API success but before DNS apply'
fi
grep -Fq 'const latestMutationId = useRef(0);' "$HOOK" || \
fail 'DNS polling must track the newest mutation when users add another rule'
grep -Fq 'if (latestMutationId.current === mutationId)' "$HOOK" || \
fail 'an earlier DNS result must not overwrite newer save feedback'
grep -Fq 'DNS 적용 상태를 확인하지 못했습니다.' "$HOOK" || \
fail 'DNS status lookup failure must be reported separately from save failure'
# Both add and delete keep their DNS result reporting; only add clears a draft.
grep -Fq 'onDeleteRule(kind, domain)' "$PAGE" || \
fail 'delete action must continue to work independently of form clearing'
echo 'PASS: SafeShield rules input, persistence and DNS apply contracts are present'
@@ -32,12 +32,8 @@ grep -Fq "const scheduledReboot = useScheduledRebootSettings(route === 'settings
fail 'settings route must load scheduled reboot settings'
grep -Fq 'const firmware = useFirmwareUpdates(true);' "$APP" || \
fail 'settings route must share the globally active SmartSafeHub firmware state used by update notifications'
grep -Fq 'firmware={firmware.data}' "$APP" || \
fail 'settings page must receive SmartSafeHub firmware status'
grep -Fq 'firmwareError={firmware.error}' "$APP" || \
fail 'settings page must receive firmware status errors'
grep -Fq 'firmwareLoading={firmware.loading}' "$APP" || \
fail 'settings page must receive firmware loading state'
grep -Fq 'activeTab={tabs.settings}' "$APP" || \
fail 'settings must receive its selected tab'
grep -Fq 'const configurationBackup = useConfigurationBackup();' "$APP" || \
fail 'settings route must wire configuration backup and restore actions'
grep -Fq "case 'system':" "$APP" || fail 'update route must remain registered in App'
@@ -177,21 +173,15 @@ if grep -Fq 'SoftwareUpdatesCard' "$SETTINGS_PAGE"; then
fail 'settings page must not embed the software update experience'
fi
grep -Fq "const customFirmwareAvailable = Boolean(firmware?.current.metadataAvailable);" "$SETTINGS_PAGE" || \
fail 'settings system status must detect SmartSafeHub custom firmware metadata'
grep -Fq '`SmartSafeHub ${firmware.current.releaseVersion}`' "$SETTINGS_PAGE" || \
fail 'settings system status must prefer the SmartSafeHub product firmware version'
grep -Fq '`빌드 ID ${firmware.current.buildId}`' "$SETTINGS_PAGE" || \
fail 'settings system status must show the immutable SmartSafeHub build ID'
grep -Fq '`${data.software.distribution} ${data.software.version}`' "$SETTINGS_PAGE" || \
fail 'settings system status must retain OpenWrt firmware fallback for legacy images'
grep -Fq '`리비전 ${data.software.revision}`' "$SETTINGS_PAGE" || \
fail 'settings system status must retain OpenWrt revision fallback for legacy images'
grep -Fq '`커널 ${data.software.kernel}`' "$SETTINGS_PAGE" || \
fail 'settings system status must keep the actual running kernel version'
if grep -Fq 'label="Firmware"' "$SETTINGS_PAGE"; then
fail 'settings must not duplicate dashboard firmware/memory/load summary cards'
fi
grep -Fq 'id="diagnostics-panel"' "$SETTINGS_PAGE" || fail 'diagnostics must have its own panel'
grep -Fq 'id="time-panel"' "$SETTINGS_PAGE" || fail 'time and scheduled reboot must have their own panel'
grep -Fq 'id="management-panel"' "$SETTINGS_PAGE" || fail 'management must have its own panel'
grep -Fq 'Device settings' "$SETTINGS_PAGE" || \
fail 'settings page must visually separate device settings from system management'
grep -Fq '시간 기준과 예약 재부팅, 진단 정보를 SmartSafeHub에서 직접 관리합니다.' "$SETTINGS_PAGE" || \
grep -Fq '시간 기준과 예약 재부팅을 SmartSafeHub에서 직접 관리합니다.' "$SETTINGS_PAGE" || \
fail 'device settings description must include scheduled reboot after regrouping'
grep -Fq 'System management' "$SETTINGS_PAGE" || \
fail 'settings page must retain a dedicated system management section'
@@ -67,6 +67,7 @@ for script in \
tests/test-wan-uci-runtime.sh \
tests/test-iptv.sh \
tests/test-update-ui-contract.sh \
tests/test-relative-time-contract.sh \
tests/test-reload-safety.sh \
tests/test-settings-ui-contract.sh \
tests/test-system-time-contract.sh \
@@ -525,8 +525,9 @@ grep -Fq 'window.location.reload();' "$FIRMWARE_HOOK" || \
if grep -Fq '시스템 상태' "$UPDATE_PAGE" || grep -Fq '시스템 관리' "$UPDATE_PAGE"; then
fail 'update page must not mix system status or management controls'
fi
grep -Fq '시스템 상태' "$SETTINGS_PAGE" || \
fail 'settings page must own system status'
if grep -Fq 'label="Firmware"' "$SETTINGS_PAGE"; then
fail 'system summary cards should live only on dashboard'
fi
grep -Fq '시스템 관리' "$SETTINGS_PAGE" || \
fail 'settings page must own system management actions'
@@ -10,6 +10,8 @@ grep -q 'passwordConfigured: length(key) > 0' root/usr/share/rpcd/ucode/smartsaf
if grep -q 'password: key' root/usr/share/rpcd/ucode/smartsafehub/wifi.uc; then echo 'FAIL: wifi summary leaks password'; exit 1; fi
grep -q 'wifi_qr' root/usr/share/rpcd/acl.d/luci-app-smartsafehub.json
grep -q 'QR 코드 보기' frontend/src/pages/WifiPage.tsx
grep -Fq 'onLoadQr={fetchWifiQr}' frontend/src/app/App.tsx
grep -Fq 'load={loadQr}' frontend/src/pages/WifiPage.tsx
grep -q 'TextEncoder' frontend/src/utils/qrMatrix.ts
# The PNG is generated in-browser from the same QR matrix; no new RPC endpoint.
grep -q 'drawWifiQrPng(qr.matrix, context, moduleSize)' frontend/src/components/WifiQrDialog.tsx
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-trafficctl
PKG_VERSION:=1.21.6
PKG_RELEASE:=11
PKG_VERSION:=1.22.0
PKG_RELEASE:=12
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=minigate
PKG_VERSION:=2026.10.14-3
PKG_RELEASE:=2
PKG_VERSION:=2026.10.17
PKG_RELEASE:=3
PKG_LICENSE:=MIT
PKG_MAINTAINER:=MiniGate
+35 -15
View File
@@ -2,9 +2,9 @@
一个类似 Lucky 的轻量级 OpenWrt 应用,提供四大核心功能:DDNS、SSL 证书、反向代理、登录防护。
当前版本:**v2026.10.14-3**。项目名称、仓库名及 OpenWrt 内部包名均为 **`minigate`**。
当前版本:**v2026.10.17**。项目名称、仓库名及 OpenWrt 内部包名均为 **`minigate`**。
> OpenWrt 用户请从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载 `minigate_2026.10.14-3_all.ipk`。不要把 GitHub 自动生成的 `Source code (zip)` / `Source code (tar.gz)` 当安装包上传到 LuCI。
> OpenWrt 用户请从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载 `minigate_2026.10.17_all.ipk`。不要把 GitHub 自动生成的 `Source code (zip)` / `Source code (tar.gz)` 当安装包上传到 LuCI。
## 项目定位
@@ -58,6 +58,7 @@ minigate 面向需要自托管轻量网关能力的 OpenWrt / ImmortalWrt 用户
- 多站点管理
- **直接 IP 访问拒绝**:只允许域名访问,扫描器直接断开
- **未知 Host 拒绝**:同一监听端口下,未配置的域名/前缀不会落到第一个反代站点
- **局域网直连解析**(可选):内网设备把代理域名直接解析到本机 LAN 地址,公网 IP 变化、上游 DNS 缓存过期或主路由不支持回流时,内网访问不受影响,流量也不进入代理
### 🛡 登录防护 (Login Guard)
- 监控 **SSH(dropbear)** 和 **LuCI 网页** 登录失败
@@ -91,20 +92,20 @@ minigate 面向需要自托管轻量网关能力的 OpenWrt / ImmortalWrt 用户
从 [Releases](https://github.com/tpxcer/minigate/releases/latest) 下载:
`minigate_2026.10.14-3_all.ipk`
`minigate_2026.10.17_all.ipk`
**通过 LuCI 界面安装:**
1. 打开 LuCI → **系统** → **软件包**
2. 点击 **上传软件包**
3. 选择 `minigate_2026.10.14-3_all.ipk`,点击安装
3. 选择 `minigate_2026.10.17_all.ipk`,点击安装
**通过命令行安装:**
```bash
cd /tmp
wget -O minigate_2026.10.14-3_all.ipk https://github.com/tpxcer/minigate/releases/download/v2026.10.14-3/minigate_2026.10.14-3_all.ipk
wget -O minigate_2026.10.17_all.ipk https://github.com/tpxcer/minigate/releases/download/v2026.10.17/minigate_2026.10.17_all.ipk
opkg update
opkg install /tmp/minigate_2026.10.14-3_all.ipk
opkg install /tmp/minigate_2026.10.17_all.ipk
rm -f /tmp/luci-indexcache /tmp/luci-modulecache 2>/dev/null
/etc/init.d/uhttpd restart
/etc/init.d/minigate restart
@@ -123,7 +124,7 @@ Release 用的 `.ipk` 请用仓库脚本生成,避免生成 Debian 风格 ar
脚本会输出:
```text
dist/minigate_2026.10.14-3_all.ipk
dist/minigate_2026.10.17_all.ipk
```
该文件外层是 OpenWrt/ImmortalWrt 24.10 兼容的 `tar.gz`,内部成员顺序为 `debian-binary`、`control.tar.gz`、`data.tar.gz`。
@@ -151,7 +152,7 @@ wget -qO- https://raw.githubusercontent.com/tpxcer/minigate/main/scripts/install
需要指定并核对具体版本时,执行:
```bash
d="$(mktemp -d /tmp/minigate.XXXXXX)" && cd "$d" && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.14-3/minigate-v2026.10.14-3-src.tar.gz && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.14-3/SHA256SUMS && grep 'minigate-v2026.10.14-3-src.tar.gz$' SHA256SUMS | sha256sum -c - && tar xzf minigate-v2026.10.14-3-src.tar.gz && sh install.sh && /etc/init.d/uhttpd restart && /etc/init.d/minigate restart
d="$(mktemp -d /tmp/minigate.XXXXXX)" && cd "$d" && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.17/minigate-v2026.10.17-src.tar.gz && wget -q https://github.com/tpxcer/minigate/releases/download/v2026.10.17/SHA256SUMS && grep 'minigate-v2026.10.17-src.tar.gz$' SHA256SUMS | sha256sum -c - && tar xzf minigate-v2026.10.17-src.tar.gz && sh install.sh && /etc/init.d/uhttpd restart && /etc/init.d/minigate restart
```
其中 `minigate.XXXXXX` 是临时目录模板,系统会自动把六个 `X` 替换为随机字符,避免与已有目录重名。该命令会校验源码包的 SHA-256、保留现有 minigate 配置并重启 LuCI 与 minigate 服务。
@@ -166,14 +167,14 @@ ssh root@192.168.1.1
# 2. 在路由器上直接下载 Release 源码包
cd /tmp
wget -O minigate-src.tar.gz https://github.com/tpxcer/minigate/releases/download/v2026.10.14-3/minigate-v2026.10.14-3-src.tar.gz
wget -O minigate-SHA256SUMS https://github.com/tpxcer/minigate/releases/download/v2026.10.14-3/SHA256SUMS
awk '$2 == "minigate-v2026.10.14-3-src.tar.gz" {print $1 " minigate-src.tar.gz"}' minigate-SHA256SUMS | sha256sum -c - || exit 1
wget -O minigate-src.tar.gz https://github.com/tpxcer/minigate/releases/download/v2026.10.17/minigate-v2026.10.17-src.tar.gz
wget -O minigate-SHA256SUMS https://github.com/tpxcer/minigate/releases/download/v2026.10.17/SHA256SUMS
awk '$2 == "minigate-v2026.10.17-src.tar.gz" {print $1 " minigate-src.tar.gz"}' minigate-SHA256SUMS | sha256sum -c - || exit 1
# 3. 解压到独立目录并安装
mkdir -p /tmp/minigate-v2026.10.14-3
tar xzf minigate-src.tar.gz -C /tmp/minigate-v2026.10.14-3
cd /tmp/minigate-v2026.10.14-3
mkdir -p /tmp/minigate-v2026.10.17
tar xzf minigate-src.tar.gz -C /tmp/minigate-v2026.10.17
cd /tmp/minigate-v2026.10.17
sh install.sh
# 4. 启动服务
@@ -223,7 +224,7 @@ SSH 登录后执行上方「方法 2」的一条命令,保留原有配置并
### IPK 升级
```bash
opkg install --force-reinstall /tmp/minigate_2026.10.14-3_all.ipk
opkg install --force-reinstall /tmp/minigate_2026.10.17_all.ipk
rm -f /tmp/luci-indexcache /tmp/luci-modulecache 2>/dev/null
/etc/init.d/minigate restart
```
@@ -307,6 +308,21 @@ opkg remove minigate 2>/dev/null || apk del minigate 2>/dev/null
3. 启用 SSL(自动关联 ACME 证书)
4. 如需输入 `http://域名:监听端口` 后自动进入 HTTPS,开启「HTTP 自动跳转」
5. 保存并应用;minigate 会自动跟随每条 HTTPS 规则的监听端口,无需额外指定固定跳转端口
6. 如需内网访问不绕公网,开启「局域网直连解析」(见下方说明)
### 局域网直连解析
宽带重新拨号后,DDNS 会很快更新公网记录,但主路由或运营商 DNS 可能在一段时间内继续返回旧 IP(部分运营商 DNS 不遵守 TTL),内网访问因此时好时坏;主路由不支持 NAT 回流时也无法从内网访问。
开启后,minigate 在每次启动和重载时把已生成站点的具体域名写入 `/tmp/hosts/minigate`,并通知 dnsmasq 重新加载(不重启任何服务,内容未变时不重载):
```
192.168.1.1 app.example.com
```
- 解析地址默认取 LAN 口 IPv4,也可以手动指定
- 只覆盖使用本机 dnsmasq 的设备;浏览器自行开启的安全 DNS(DoH)不受影响
- 内网访问使用与公网相同的端口,因此要求公网端口映射没有改端口号(例如外网 443 映射到内网 2000 时不适用)
- 关闭开关或停止 minigate 时会移除该文件;也可以手动执行 `/usr/lib/minigate/proxy.sh lan-dns` 重新同步
### Cloudflare API Token 创建方法
1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
@@ -373,6 +389,10 @@ minigate/
## 更新日志
### v2026.10.17
- 新增「局域网直连解析」(默认关闭):把已配置的代理域名直接解析到本机 LAN 地址,宽带重新拨号后主路由或运营商 DNS 继续返回旧 IP 时,内网访问不再时好时坏,流量也不进入代理
- 地区访问控制改用 BusyBox 兼容的访客提取方式(原 `grep -P` 在 BusyBox 上无法运行,扫描实际取不到任何 IP),并跳过局域网、回环、链路本地和运营商 NAT 地址
### v2026.10.14-3
- 修复 v2026.10.14-2 的登录防护进程匹配过宽:命令行中恰好含有登录防护命令文本的调用方会被当成登录防护一并结束,可能导致更新中断并自动回滚;现在只匹配登录防护的实际启动命令
- 包含 v2026.10.14-2 的全部修复:停止或重启登录防护时不再遗留 `logread -f`、`tail -f` 子进程
@@ -34,6 +34,24 @@ o = s:option(Flag, "http_redirect", "启用 HTTP 跳转")
o.default = "0"
o.rmempty = false
local lan_ip_auto = uc:get("network", "lan", "ipaddr")
if type(lan_ip_auto) == "table" then lan_ip_auto = lan_ip_auto[1] end
lan_ip_auto = lan_ip_auto and lan_ip_auto:match("^(%d+%.%d+%.%d+%.%d+)")
s = m:section(NamedSection, "global", "global", "局域网直连解析",
"让局域网设备把本页的代理域名直接解析到本机 LAN 地址:访问不经过公网 IP、主路由回流和上游 DNS 缓存,也不进入代理;公网解析不变。" ..
"仅对使用本机 DNS(dnsmasq)的设备生效,且要求公网端口映射没有改端口号。")
s.anonymous = true
o = s:option(Flag, "lan_dns", "启用局域网直连解析")
o.default = "0"
o.rmempty = false
o = s:option(Value, "lan_ip", "解析到的 LAN 地址", "留空自动使用 LAN 口 IPv4 地址。")
o.datatype = "ip4addr"
o.placeholder = lan_ip_auto and ("自动(" .. lan_ip_auto .. ")") or "自动"
o:depends("lan_dns", "1")
-- ================================
-- 通配符域名
-- ================================
+20
View File
@@ -0,0 +1,20 @@
## 新增功能
- 局域网直连解析(反向代理页,默认关闭):宽带重新拨号后,DDNS 会在几分钟内更新公网记录,但主路由或运营商 DNS 可能在 TTL 过期后仍继续返回旧 IP,内网访问因此时好时坏。开启后,minigate 在每次启动和重载时把已生成站点的具体域名写入 `/tmp/hosts/minigate`,解析到本机 LAN 地址(可手动指定),并只在内容变化时通知 dnsmasq 重新加载。内网访问不再经过公网 IP、主路由回流和上游 DNS 缓存,也不进入代理;公网解析不受影响。
## 修复内容
- 地区访问控制的访客提取使用了 `grep -P`,BusyBox grep 不支持该参数,扫描实际取不到任何 IP;172.16.0.0/12 的排除规则在基础正则下也不生效。现在改用 sed 提取 IPv4 访客,并跳过局域网、回环、链路本地和运营商 NAT(100.64.0.0/10)地址。
## 影响说明
- 局域网直连解析只对使用本机 dnsmasq 的设备生效;浏览器自行开启的安全 DNS(DoH)不受影响。
- 内网访问使用与公网相同的端口,公网端口映射改了端口号(例如外网 443 映射到内网 2000)时不要开启。
- 停止 MiniGate、关闭开关或卸载时会移除 `/tmp/hosts/minigate`。
## 安装与兼容性
- opkg 安装包:`minigate_2026.10.17_all.ipk`。
- 源码安装包:`minigate-v2026.10.17-src.tar.gz`。
- 继续附带 `luci-app-minigate-v2026.10.17-src.tar.gz`,兼容旧更新器识别的源码包名。
- 本次发布不会自动安装到软路由;请在 LuCI 中确认更新内容后手动更新。
+1
View File
@@ -2,6 +2,7 @@ config global 'global'
option enabled '0'
option ipv6_listen '0'
option http_redirect '0'
option lan_dns '0'
config acme 'acme'
option enabled '0'
+31 -12
View File
@@ -1,10 +1,10 @@
#!/bin/sh
# MiniGate GeoFence - 地区访问控制
# 检查反代访问日志,非允许地区的 IP 用防火墙封禁
LOGFILE="/var/log/minigate-geofence.log"
CACHE_DIR="/tmp/minigate_geo"
LOGFILE="${MINIGATE_GEOFENCE_LOG:-/var/log/minigate-geofence.log}"
CACHE_DIR="${MINIGATE_GEO_CACHE_DIR:-/tmp/minigate_geo}"
BLOCK_SET="minigate_blocked"
ACCESS_LOG="/var/log/minigate-access.log"
ACCESS_LOG="${MINIGATE_ACCESS_LOG:-/var/log/minigate-access.log}"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [GEOFENCE] $*" >> "$LOGFILE"; }
@@ -98,22 +98,37 @@ clean_cache() {
[ -d "$CACHE_DIR" ] && find "$CACHE_DIR" -type f -mmin +1440 -delete 2>/dev/null
}
# 局域网、回环、链路本地和运营商 NAT 地址没有归属地,局域网直连时访客就是内网 IP,不参与地区判断
is_private_ip() {
case "$1" in
0.*|10.*|127.*|169.254.*|192.168.*) return 0 ;;
172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
100.6[4-9].*|100.[7-9][0-9].*|100.1[01][0-9].*|100.12[0-7].*) return 0 ;;
esac
return 1
}
# BusyBox grep 不支持 -P,用 sed 取 IPv4 访客地址
extract_public_ips() {
local ip
sed -n 's/.*"client":"\([0-9]\{1,3\}\(\.[0-9]\{1,3\}\)\{3\}\)".*/\1/p' "$ACCESS_LOG" 2>/dev/null | \
sort -u | while read -r ip; do
is_private_ip "$ip" || echo "$ip"
done
}
# 主逻辑:扫描 access log 中的新 IP
scan_and_block() {
local enabled=$(uci -q get minigate.global.geo_enabled)
[ "$enabled" != "1" ] && return 0
local allowed=$(uci -q get minigate.global.geo_allowed)
[ -z "$allowed" ] && { log "未配置允许地区,跳过"; return 0; }
init_block_set
# 提取所有外网 IP(排除内网)
local ips=$(grep -oP '"client":"[^"]*"' "$ACCESS_LOG" 2>/dev/null | \
grep -oP '[\d\.]+' | \
grep -v '^192\.168\.' | grep -v '^10\.' | grep -v '^172\.(1[6-9]|2[0-9]|3[01])\.' | grep -v '^127\.' | \
sort -u)
local ips=$(extract_public_ips)
local checked=0 blocked=0
for ip in $ips; do
# 已经封禁的跳过
@@ -169,6 +184,10 @@ flush_blocks() {
log "已清除所有封禁"
}
if [ "${MINIGATE_GEOFENCE_LIBRARY_ONLY:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi
case "${1:-scan}" in
scan) scan_and_block ;;
unblock) unblock_ip "$2" ;;
+99 -6
View File
@@ -11,6 +11,8 @@ ACCESS_LOG="${MINIGATE_ACCESS_LOG:-/var/log/minigate-access.log}"
SOCKET_DIR="${MINIGATE_SOCKET_DIR:-/var/run/minigate}"
STREAM_MODULE="${MINIGATE_STREAM_MODULE:-/usr/lib/nginx/modules/ngx_stream_module.so}"
ACCESS_LOG_MAX="${MINIGATE_ACCESS_LOG_MAX:-1048576}"
# OpenWrt 的 dnsmasq 以 addn-hosts=/tmp/hosts 读取整个目录,收到 SIGHUP 时重新加载
LAN_HOSTS_FILE="${MINIGATE_LAN_HOSTS_FILE:-/tmp/hosts/minigate}"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [PROXY] $*" >> "$LOGFILE"; }
@@ -375,6 +377,89 @@ generate_sites() {
rm -f /tmp/minigate_stream_*.tmp
}
# === 局域网直连解析 ===
# 内网设备把本机反代的域名直接解析到 LAN 地址:不依赖公网 IP、回流 NAT 和上游 DNS 缓存,
# 流量留在局域网内,也不会进入代理。公网解析仍由 DDNS 维护。
is_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF != 4 { exit 1 }
{ for (i = 1; i <= 4; i++) if ($i !~ /^[0-9]+$/ || $i > 255) exit 1 }
'
}
# 手动指定优先;留空时取 LAN 接口 IPv4,再退回 network.lan.ipaddr(可能是列表或带 /前缀)
get_lan_ip() {
local ip
ip=$(uci -q get minigate.global.lan_ip)
[ -n "$ip" ] || ip=$(ubus call network.interface.lan status 2>/dev/null | \
jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null)
[ -n "$ip" ] || ip=$(uci -q get network.lan.ipaddr)
ip=${ip%% *}
ip=${ip%%/*}
is_ipv4 "$ip" && echo "$ip"
}
# 以生成的站点配置为准,只取实际由 nginx 提供的具体域名(跳过默认站点 _ 和通配符)
list_site_domains() {
cat "$SITES_DIR"/*.conf 2>/dev/null | \
sed -n 's/^[[:space:]]*server_name[[:space:]]\{1,\}\([^;]*\);.*/\1/p' | \
tr ' ' '\n' | grep -E '^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$' | sort -u
}
send_hup() {
kill -HUP "$@"
}
reload_dnsmasq_hosts() {
local pids
pids=$(pidof dnsmasq 2>/dev/null)
[ -n "$pids" ] && send_hup $pids >/dev/null 2>&1
return 0
}
remove_lan_hosts() {
[ -f "$LAN_HOSTS_FILE" ] || return 0
rm -f "$LAN_HOSTS_FILE"
reload_dnsmasq_hosts
log "局域网直连解析已移除"
}
# 内容未变时不重载 dnsmasq,避免每次保存都清空整个 DNS 缓存
sync_lan_hosts() {
local ip domains domain content
if [ "$(uci -q get minigate.global.lan_dns)" != "1" ]; then
remove_lan_hosts
return 0
fi
ip=$(get_lan_ip)
if [ -z "$ip" ]; then
log "局域网直连解析:未取得有效的 LAN IPv4 地址,已跳过"
remove_lan_hosts
return 0
fi
domains=$(list_site_domains)
if [ -z "$domains" ]; then
remove_lan_hosts
return 0
fi
content="# MiniGate 局域网直连解析,由 proxy.sh 自动生成"
for domain in $domains; do
content="${content}
${ip} ${domain}"
done
[ -f "$LAN_HOSTS_FILE" ] && [ "$(cat "$LAN_HOSTS_FILE")" = "$content" ] && return 0
mkdir -p "$(dirname "$LAN_HOSTS_FILE")" && printf '%s\n' "$content" > "$LAN_HOSTS_FILE" || {
log "局域网直连解析:写入 ${LAN_HOSTS_FILE} 失败"
return 1
}
reload_dnsmasq_hosts
log "局域网直连解析 -> ${ip}: $(echo $domains)"
}
do_stop() {
if [ -f "$PID_FILE" ]; then
local pid=$(cat "$PID_FILE" 2>/dev/null)
@@ -409,7 +494,8 @@ start_prepared() {
do_start() {
[ -z "$(which nginx 2>/dev/null)" ] && { log "nginx未安装"; return 1; }
prepare_config || return 1
start_prepared
start_prepared || return 1
sync_lan_hosts
}
do_reload() {
@@ -419,13 +505,15 @@ do_reload() {
grep -q '^stream {' "$NGINX_CONF" 2>/dev/null && new_mux=1
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE" 2>/dev/null) 2>/dev/null; then
if [ "$old_mux" != "$new_mux" ]; then
start_prepared
start_prepared || return 1
else
kill -HUP $(cat "$PID_FILE") && log "已重载"
kill -HUP $(cat "$PID_FILE") || return 1
log "已重载"
fi
else
start_prepared
start_prepared || return 1
fi
sync_lan_hosts
}
# /var/log 在内存中且 nginx 不会自行轮转。访问日志保留最近记录到 .1 后原地清空:
@@ -443,11 +531,16 @@ trim_logs() {
done
}
if [ "${MINIGATE_PROXY_LIBRARY_ONLY:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi
case "$1" in
start) do_start ;;
stop) do_stop ;;
stop) do_stop; remove_lan_hosts ;;
reload) do_reload ;;
test) prepare_config ;;
trim-logs) trim_logs ;;
*) echo "用法: $0 {start|stop|reload|test|trim-logs}" ;;
lan-dns) sync_lan_hosts ;;
*) echo "用法: $0 {start|stop|reload|test|trim-logs|lan-dns}" ;;
esac
Loaded 100 of 107 files, more files were not shown because too many files have changed in this diff. Show more