mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-04 06:53:56 +08:00
🤞 Sync 2026-10-17 10:45:37
This commit is contained in:
1 parent
1849cc6156
commit
315f273687
107 files changed
+3810
-1215
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" ] && {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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 服务端公钥"
|
||||
|
||||
@@ -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 伺服器公鑰"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -27,3 +27,5 @@ frontend/.env.*.local
|
||||
*.orig
|
||||
*.rej
|
||||
/*.patch
|
||||
|
||||
# frontend/demo-dist/ is intentionally committed for the Hub demo.
|
||||
@@ -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
|
||||
|
||||
### 추가
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
-- ================================
|
||||
-- 通配符域名
|
||||
-- ================================
|
||||
|
||||
@@ -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 中确认更新内容后手动更新。
|
||||
@@ -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'
|
||||
|
||||
@@ -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" ;;
|
||||
|
||||
@@ -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
Reference in new issue
Block a user