🍉 Sync 2026-09-25 18:00:12

This commit is contained in:
github-actions[bot] committed 2026-09-25 18:00:12 +08:00
1 parent 7c17dda4d0
commit 2ee65a261e
135 files changed
+5726 -7799

No files matched your search

@@ -16,9 +16,32 @@
* Modules must not mutate state. host is cleared then rebuilt (idempotent replace).
*/
function chipFieldLabel(spec) {
switch (spec.key) {
case 'q':
return _('Search');
case 'action':
return _('Action');
case 'interface':
return _('Interface');
case 'proto':
return _('Proto');
case 'src':
return _('Source');
case 'dst':
return _('Destination');
case 'sport':
return _('Source port');
case 'dport':
return _('Destination port');
default:
return spec.label || spec.key;
}
}
function chipValueNodes(spec, val) {
const key = spec.key;
const label = spec.label || key;
const label = chipFieldLabel(spec);
const p = log.parseFilterValue(val);
if (!p.value) return [''];
@@ -12,7 +12,10 @@
* luciUrl, firewallZonesPath, firewallZonesUrl, firewallZonesLink
*
* Filter-aware helpers (require an onFilterClick callback):
* filterLink, addrFilterLink, ruleAdminPath, ruleAdminLink, ifaceLink
* filterLink, addrFilterLink, ruleAdminLink, ifaceLink
*
* Constant path helper:
* ruleAdminPath
*
* No host element — all functions return DOM nodes or strings.
* May require fwlive.log for formatCell and fwlive.hostname for cache reads.
@@ -90,7 +90,7 @@ return baseclass.extend({
CLASSIFY_SPEC: CLASSIFY_SPEC,
TCP_FLAG_TAIL:
/\b((?:SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
/\b((?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
NETFILTER_KV_GLUE: NETFILTER_KV_GLUE,
kvHas: function (msg, key) {
@@ -105,7 +105,8 @@ return baseclass.extend({
MAX_DATE_SECONDS: 8640000000000,
normalizeNetfilterMessage: function (message) {
return (message || '').replace(this.NETFILTER_KV_GLUE, '$1 ');
if (typeof message !== 'string') message = '';
return message.replace(this.NETFILTER_KV_GLUE, '$1 ');
},
parseKeyValueLog: function (message) {
@@ -69,6 +69,7 @@ function blockerCode(state) {
blockers.indexOf('nf_log_ipv6_missing') >= 0
)
return 'nf_log_missing';
if (blockers.length > 0) return 'unknown';
return '';
}
@@ -92,6 +93,63 @@ function appendLoggingNotice(host, state) {
host.appendChild(E('span', { 'class': 'fwlive-logging-notice' }, [state.loggingNotice]));
}
function appendBlockerStatus(host, blocker, st) {
if (blocker === 'no_wan_zone') {
host.appendChild(
E(
'span',
{ 'class': 'fwlive-logging-status' },
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
)
);
host.appendChild(links.firewallZonesLink());
return;
}
if (blocker === 'nf_log_missing') {
host.appendChild(
E('span', { 'class': 'fwlive-logging-status' }, [
_('WAN logging unavailable: missing kernel log modules')
])
);
return;
}
if (blocker === 'unknown') {
host.appendChild(
E('span', { 'class': 'fwlive-logging-status' }, [_('WAN logging unavailable')])
);
}
}
function appendWanLogDisableControl(host, state, callbacks) {
const st = state.loggingStatus;
const limit = st.wan_log_limit || _('default 10/minute');
const busy = !!state.loggingBusy;
const children = busy
? [_('Disabling…')]
: [
E('span', { 'class': 'fwlive-log-on-dot', 'aria-hidden': 'true' }, ['']),
E('span', { 'class': 'fwlive-log-label' }, [_('WAN logging on')]),
E('span', { 'class': 'fwlive-log-rate' }, [_('· %s').format(limit)])
];
host.appendChild(
E(
'button',
{
'class': 'cbi-button fwlive-log-merged',
'type': 'button',
'title': _('WAN logging on (%s). Click to disable.').format(limit),
'disabled': busy ? '' : null,
'click': function () {
callbacks.onDisable();
}
},
children
)
);
}
function renderToolbar(host, state, callbacks) {
host.innerHTML = '';
const st = state.loggingStatus;
@@ -103,54 +161,15 @@ function renderToolbar(host, state, callbacks) {
host.style.display = 'contents';
const blocker = blockerCode(state);
if (blocker === 'no_wan_zone') {
host.appendChild(
E(
'span',
{ 'class': 'fwlive-logging-status' },
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
)
);
host.appendChild(links.firewallZonesLink());
appendLoggingNotice(host, state);
return;
}
if (blocker) appendBlockerStatus(host, blocker, st);
if (blocker === 'nf_log_missing') {
host.appendChild(
E('span', { 'class': 'fwlive-logging-status' }, [
_('WAN logging unavailable: missing kernel log modules')
])
);
appendLoggingNotice(host, state);
return;
}
const limit = st.wan_log_limit || _('default 10/minute');
if (st.wan_log) {
const busy = !!state.loggingBusy;
const children = busy
? [_('Disabling…')]
: [
E('span', { 'class': 'fwlive-log-on-dot', 'aria-hidden': 'true' }, ['']),
E('span', { 'class': 'fwlive-log-label' }, [_('WAN logging on')]),
E('span', { 'class': 'fwlive-log-rate' }, [_('· %s').format(limit)])
];
host.appendChild(
E(
'button',
{
'class': 'cbi-button fwlive-log-merged',
'type': 'button',
'title': _('WAN logging on (%s). Click to disable.').format(limit),
'disabled': busy ? '' : null,
'click': function () {
callbacks.onDisable();
}
},
children
)
);
appendWanLogDisableControl(host, state, callbacks);
appendLoggingNotice(host, state);
return;
}
if (blocker) {
appendLoggingNotice(host, state);
return;
}
@@ -277,6 +296,16 @@ function buildEmptyStateNodes(state, callbacks) {
return nodes;
}
if (blocker === 'unknown') {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('WAN logging unavailable')]));
nodes.push(
E('p', {}, [
_('This router reported a logging blocker that Live View does not recognize yet.')
])
);
return nodes;
}
if (st && st.wan_log) {
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('Waiting for firewall events')]));
nodes.push(
@@ -312,7 +312,8 @@ function buildExpansionRow(row, state, columns) {
E('td', { 'colspan': String(columns.length) }, [
E('div', { 'class': 'fwlive-msg-expand-label' }, [_('Message')]),
E('pre', { 'class': 'fwlive-msg-expand-body' }, [
log.formatMessageDisplay(row.message, 'wrap') || '—'
/* oneline is uncapped; wrap ellipsizes at 240 */
log.formatMessageDisplay(row.message, 'oneline') || '—'
])
])
]);
@@ -156,6 +156,8 @@ return view.extend({
hostnameFailed: null,
resolveInFlight: false,
resolveGeneration: 0,
/* Coalesce hostname-cache paints deferred while tablePaused. */
resolvePaintPending: false,
lastPollError: false,
lastRulesError: null,
followLive: true,
@@ -166,6 +168,7 @@ return view.extend({
loggingStatus: null,
loggingBusy: false,
loggingNotice: '',
_loggingNoticeFromToggle: false,
/* Session-only dismiss of first-run consent (Not now without checkbox). */
consentDismissedSession: false,
_loggingToolbarSig: '',
@@ -212,7 +215,7 @@ return view.extend({
parts.push('maxraw=%s'.format(encodeURIComponent(this.manualFetchLines)));
}
if (this.viewMode === 'detailed') parts.push('view=detailed');
location.hash = parts.join('&');
history.replaceState(history.state, '', '#' + parts.join('&'));
},
hashEntries() {
@@ -739,7 +742,7 @@ return view.extend({
const status = await callFwliveLoggingStatus();
if (this.viewDisposed) return;
this.loggingStatus = status;
this.loggingNotice = '';
if (!this._loggingNoticeFromToggle) this.loggingNotice = '';
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
} catch (_e) {
if (this.viewDisposed) return;
@@ -761,17 +764,20 @@ return view.extend({
this.loggingBusy = true;
this.loggingNotice = '';
this._loggingNoticeFromToggle = false;
opts.initialUi();
try {
const res = await opts.call();
if (!res || !res.ok) {
this.loggingNotice = opts.failureNotice(res);
this._loggingNoticeFromToggle = true;
await this.loadLoggingStatus();
return;
}
this.loggingNotice = opts.successNotice(res);
this._loggingNoticeFromToggle = !!this.loggingNotice;
if (opts.onSuccess) opts.onSuccess(res);
if (this.loggingStatus && typeof opts.wanLog === 'boolean')
this.loggingStatus = Object.assign({}, this.loggingStatus, {
@@ -780,8 +786,11 @@ return view.extend({
await this.loadLoggingStatus();
} catch (_e) {
this.loggingNotice = opts.catchNotice();
this._loggingNoticeFromToggle = true;
await this.loadLoggingStatus();
} finally {
/* Survive only the refresh owned by this toggle. */
this._loggingNoticeFromToggle = false;
this.loggingBusy = false;
this.updateEmptyStateUi();
this.updateLoggingToolbarUi();
@@ -803,6 +812,14 @@ return view.extend({
return _(
'Another change is staged for the firewall; apply or revert it first.'
);
if (res && res.error === 'no_wan_zone')
return _('No WAN zone found; cannot toggle logging without one.');
if (res && res.error === 'lock_failed')
return _('Could not acquire the logging lock.');
if (res && res.error === 'baseline_snapshot_failed')
return _('Could not snapshot the current logging state.');
if (res && res.error === 'firewall_reload_failed')
return _('The firewall did not reload; saved and live logging may differ.');
return _('Could not enable logging.');
},
successNotice: (res) =>
@@ -826,6 +843,12 @@ return view.extend({
return _(
'Another change is staged for the firewall; apply or revert it first.'
);
if (res && res.error === 'no_wan_zone')
return _('No WAN zone found; cannot toggle logging without one.');
if (res && res.error === 'lock_failed')
return _('Could not acquire the logging lock.');
if (res && res.error === 'firewall_reload_failed')
return _('The firewall did not reload; saved and live logging may differ.');
return _('Could not disable logging.');
},
successNotice: (res) => (res.changed ? _('WAN drop/reject logging is off.') : ''),
@@ -1363,23 +1386,28 @@ return view.extend({
this.updateSummaryUi();
},
updateSummaryUi() {
const card = document.getElementById('fwlive-summary');
syncEmptyScrollVisibility(rowCount) {
const hideTable = this.summaryMode && !this.summaryRowsShown;
const scroll = document.getElementById('fwlive-scroll');
const empty = document.getElementById('fwlive-empty');
if (scroll) {
if (!scroll.style) scroll.style = { display: '' };
scroll.style.display = hideTable ? 'none' : '';
}
if (empty) {
if (!empty.style) empty.style = { display: '' };
empty.style.display = hideTable ? 'none' : rowCount ? 'none' : 'block';
}
},
updateSummaryUi() {
const card = document.getElementById('fwlive-summary');
const toggle = document.getElementById('fwlive-summary-rows');
if (card) {
if (!card.style) card.style = { display: '' };
card.style.display = this.summaryMode ? 'block' : 'none';
}
if (scroll) {
if (!scroll.style) scroll.style = { display: '' };
scroll.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
}
if (empty) {
if (!empty.style) empty.style = { display: '' };
empty.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
}
this.syncEmptyScrollVisibility(this.filteredRows().length);
if (toggle) {
toggle.textContent = this.summaryRowsShown ? _('Hide rows') : _('Show rows');
toggle.setAttribute('aria-pressed', this.summaryRowsShown ? 'true' : 'false');
@@ -1400,13 +1428,12 @@ return view.extend({
this.summaryData = null;
this.updateSummaryUi();
if (this.tablePaused) {
const empty = document.getElementById('fwlive-empty');
if (empty) {
const rows = this.filteredRows();
empty.style.display = rows.length ? 'none' : 'block';
}
this.syncEmptyScrollVisibility(this.filteredRows().length);
this.updateStatus();
} else this.renderRows(true);
} else {
this.resolvePaintPending = false;
this.renderRows(true);
}
},
onSummaryRowsToggle() {
@@ -1420,6 +1447,16 @@ return view.extend({
this.ensureRenderScheduler().schedule(!!force);
},
scheduleResolvePaint() {
if (this.tablePaused) {
this.resolvePaintPending = true;
this.updateStatus();
return;
}
this.resolvePaintPending = false;
this.scheduleRenderRows(true);
},
updateFloodBanner() {
const el = document.getElementById('fwlive-flood');
if (!el) return;
@@ -1601,7 +1638,10 @@ return view.extend({
this.requestPoll()
.then(() => {
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
if (epoch === this.currentPollEpoch()) this.renderRows(true);
if (epoch === this.currentPollEpoch()) {
this.resolvePaintPending = false;
this.renderRows(true);
}
})
.catch(function () {});
}
@@ -1767,7 +1807,7 @@ return view.extend({
}
this.updateAdaptiveBanner();
if (updated) this.scheduleRenderRows(true);
if (updated) this.scheduleResolvePaint();
} catch (_e) {
/* resolve unavailable — show IPs */
} finally {
@@ -1894,7 +1934,6 @@ return view.extend({
if (!el || typeof el.querySelector !== 'function') return;
const body = el.querySelector('tbody');
const empty = document.getElementById('fwlive-empty');
const scroll = document.getElementById('fwlive-scroll');
this.updateHash(this.readFilters());
@@ -1908,6 +1947,7 @@ return view.extend({
if (!paint) {
this.updateFloodBanner();
this.syncEmptyScrollVisibility(rows.length);
this.updateStatus(rows);
return;
}
@@ -1916,7 +1956,7 @@ return view.extend({
const prevScroll = scroll ? scroll.scrollTop : 0;
if (empty) empty.style.display = rows.length ? 'none' : 'block';
this.syncEmptyScrollVisibility(rows.length);
this.updateStatus(rows);
this.renderFilterChips();
@@ -2068,7 +2108,18 @@ return view.extend({
else if (this.summaryMode) {
this.renderSummary();
this.updateStatus();
} else this.scheduleRenderRows();
if (this.summaryRowsShown) {
const forceHostnamePaint = this.resolvePaintPending;
this.resolvePaintPending = false;
this.scheduleRenderRows(forceHostnamePaint);
}
} else {
/* Stale resume skips renderRows(true); catch-up polls must still
* flush coalesced hostname paints. */
const forceHostnamePaint = this.resolvePaintPending;
this.resolvePaintPending = false;
this.scheduleRenderRows(forceHostnamePaint);
}
try {
await this.resolveHostnamesForEntries(this.filteredRows());