mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 21:13:00 +08:00
This commit is contained in:
1 parent
c13653dc17
commit
77ae924426
29 files changed
+1215
-429
No files matched your search
@@ -11,5 +11,5 @@
|
||||
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
|
||||
*/
|
||||
return baseclass.extend({
|
||||
styleText: "\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-display-bar-label {\n\tflex: 0 0 auto;\n\tfoLine truncated
|
||||
styleText: "\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-display-bar-label {\n\tflex: 0 0 auto;\n\tfoLine truncated
|
||||
});
|
||||
@@ -670,6 +670,25 @@
|
||||
color: var(--text-color-high);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.fwlive-summary {
|
||||
margin: 0 0 10px;
|
||||
padding: 8px 12px;
|
||||
background: var(--background-color-low);
|
||||
border: 1px solid var(--border-color-medium);
|
||||
border-radius: 3px;
|
||||
color: var(--text-color-high);
|
||||
}
|
||||
.fwlive-summary-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
.fwlive-summary-body {
|
||||
margin: 8px 0 0;
|
||||
font: inherit;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.fwlive-tint-warn {
|
||||
display: none;
|
||||
color: var(--warn-color-high);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* callbacks - {} (unused; present for API consistency)
|
||||
*
|
||||
* renderRows(host, state, callbacks) → void
|
||||
* host - <tbody> element (cleared and rebuilt; element itself is kept)
|
||||
* host - <tbody> element (kept; normal polls reuse keyed rows)
|
||||
* state - shallow copy: { rows, columns, viewMode, messageLayout,
|
||||
* expandedRowId, rowTint, showHostnames,
|
||||
* hostnameCache, firewallBackend }
|
||||
@@ -24,8 +24,9 @@
|
||||
* Internals (not a second public contract): columnLabel, columnCellClass,
|
||||
* flowCell, buildColumnCell.
|
||||
*
|
||||
* Modules must not mutate state. host contents are cleared then rebuilt
|
||||
* (idempotent replace). Does not touch #fwlive-scroll or #fwlive-empty.
|
||||
* Modules must not mutate state. Forced renders clear and rebuild the host;
|
||||
* normal polls reuse unchanged keyed rows and only build changed rows. Does
|
||||
* not touch #fwlive-scroll or #fwlive-empty.
|
||||
*/
|
||||
|
||||
function columnLabel(col) {
|
||||
@@ -255,49 +256,146 @@ function renderThead(host, state, _callbacks) {
|
||||
}
|
||||
}
|
||||
|
||||
function rowRenderKey(row, state, columns) {
|
||||
return JSON.stringify([
|
||||
row.id,
|
||||
row.timestamp,
|
||||
row.action,
|
||||
row.rule_hint,
|
||||
row.rule_label,
|
||||
row.interface_in,
|
||||
row.interface_out,
|
||||
row.direction,
|
||||
row.proto,
|
||||
row.src,
|
||||
row.sport,
|
||||
row.dst,
|
||||
row.dport,
|
||||
row.flags,
|
||||
row.length,
|
||||
row.message,
|
||||
columns,
|
||||
state.viewMode,
|
||||
state.messageLayout,
|
||||
state.expandedRowId === row.id,
|
||||
!!state.rowTint,
|
||||
!!state.showHostnames,
|
||||
state.firewallBackend
|
||||
]);
|
||||
}
|
||||
|
||||
function rowClass(index, row, state, callbacks) {
|
||||
return [
|
||||
index % 2 ? 'fwlive-row-alt' : '',
|
||||
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
|
||||
state.expandedRowId === row.id ? 'fwlive-row-expanded' : '',
|
||||
state.rowTint ? callbacks.actionRowTintClass(row.action) : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function buildRow(row, index, state, columns, callbacks) {
|
||||
const cells = [];
|
||||
for (let c = 0; c < columns.length; c++)
|
||||
cells.push(buildColumnCell(columns[c], row, state, callbacks));
|
||||
|
||||
const tr = E(
|
||||
'tr',
|
||||
{
|
||||
'class': rowClass(index, row, state, callbacks),
|
||||
'click': state.viewMode === 'simple' ? (ev) => callbacks.onRowClick(row.id, ev) : null
|
||||
},
|
||||
cells
|
||||
);
|
||||
tr._fwliveRowId = String(row.id);
|
||||
tr._fwliveRowKey = rowRenderKey(row, state, columns);
|
||||
return tr;
|
||||
}
|
||||
|
||||
function buildExpansionRow(row, state, columns) {
|
||||
const expansion = E('tr', { 'class': 'fwlive-msg-expand' }, [
|
||||
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') || '—'
|
||||
])
|
||||
])
|
||||
]);
|
||||
expansion._fwliveExpansionFor = String(row.id);
|
||||
expansion._fwliveExpansionKey = rowRenderKey(row, state, columns);
|
||||
return expansion;
|
||||
}
|
||||
|
||||
function renderAllRows(host, rows, state, columns, callbacks) {
|
||||
host.innerHTML = '';
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
host.appendChild(buildRow(row, i, state, columns, callbacks));
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === row.id)
|
||||
host.appendChild(buildExpansionRow(row, state, columns));
|
||||
}
|
||||
}
|
||||
|
||||
function canReuseRows(host) {
|
||||
return (
|
||||
host &&
|
||||
host.childNodes &&
|
||||
typeof host.childNodes.length === 'number' &&
|
||||
typeof host.insertBefore === 'function' &&
|
||||
typeof host.removeChild === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function renderRows(host, state, callbacks) {
|
||||
const rows = state.rows || [];
|
||||
const columns = state.columns || [];
|
||||
|
||||
host.innerHTML = '';
|
||||
if (state.forceRender || !canReuseRows(host)) {
|
||||
renderAllRows(host, rows, state, columns, callbacks);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingRows = new Map();
|
||||
const existingExpansions = new Map();
|
||||
const oldChildren = Array.prototype.slice.call(host.childNodes);
|
||||
for (let i = 0; i < oldChildren.length; i++) {
|
||||
const child = oldChildren[i];
|
||||
if (child._fwliveRowId != null) existingRows.set(child._fwliveRowId, child);
|
||||
else if (child._fwliveExpansionFor != null)
|
||||
existingExpansions.set(child._fwliveExpansionFor, child);
|
||||
}
|
||||
|
||||
const desired = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const r = rows[i];
|
||||
const rowClass = [
|
||||
i % 2 ? 'fwlive-row-alt' : '',
|
||||
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
|
||||
state.expandedRowId === r.id ? 'fwlive-row-expanded' : '',
|
||||
state.rowTint ? callbacks.actionRowTintClass(r.action) : ''
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
const cells = [];
|
||||
for (let c = 0; c < columns.length; c++)
|
||||
cells.push(buildColumnCell(columns[c], r, state, callbacks));
|
||||
const row = rows[i];
|
||||
const id = String(row.id);
|
||||
const key = rowRenderKey(row, state, columns);
|
||||
let tr = existingRows.get(id);
|
||||
if (!tr || tr._fwliveRowKey !== key) tr = buildRow(row, i, state, columns, callbacks);
|
||||
else tr.setAttribute('class', rowClass(i, row, state, callbacks));
|
||||
tr._fwliveRowId = id;
|
||||
tr._fwliveRowKey = key;
|
||||
desired.push(tr);
|
||||
|
||||
const tr = E(
|
||||
'tr',
|
||||
{
|
||||
'class': rowClass,
|
||||
'click': state.viewMode === 'simple' ? (ev) => callbacks.onRowClick(r.id, ev) : null
|
||||
},
|
||||
cells
|
||||
);
|
||||
host.appendChild(tr);
|
||||
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === r.id) {
|
||||
host.appendChild(
|
||||
E('tr', { 'class': 'fwlive-msg-expand' }, [
|
||||
E('td', { 'colspan': String(columns.length) }, [
|
||||
E('div', { 'class': 'fwlive-msg-expand-label' }, [_('Message')]),
|
||||
E('pre', { 'class': 'fwlive-msg-expand-body' }, [
|
||||
log.formatMessageDisplay(r.message, 'wrap') || '—'
|
||||
])
|
||||
])
|
||||
])
|
||||
);
|
||||
if (state.viewMode === 'simple' && state.expandedRowId === row.id) {
|
||||
let expansion = existingExpansions.get(id);
|
||||
if (!expansion || expansion._fwliveExpansionKey !== key)
|
||||
expansion = buildExpansionRow(row, state, columns);
|
||||
desired.push(expansion);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < desired.length; i++) {
|
||||
const current = host.childNodes[i] || null;
|
||||
if (current !== desired[i]) host.insertBefore(desired[i], current);
|
||||
}
|
||||
|
||||
const used = new Set(desired);
|
||||
const currentChildren = Array.prototype.slice.call(host.childNodes);
|
||||
for (let i = 0; i < currentChildren.length; i++) {
|
||||
if (!used.has(currentChildren[i])) host.removeChild(currentChildren[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
|
||||
@@ -102,6 +102,12 @@ return view.extend({
|
||||
resumeMerge: false,
|
||||
pollFn: null,
|
||||
pollDataInFlight: false,
|
||||
/* Every refresh trigger goes through this coordinator. A request made
|
||||
* while one is active is coalesced into one follow-up, never overlapped. */
|
||||
pollRequestPromise: null,
|
||||
pollRequestQueued: false,
|
||||
pollRequestWaiters: [],
|
||||
pollRequestQueuedWaiters: [],
|
||||
/* Layer 2 — visibility / RTT cadence / shed surfacing. */
|
||||
pollEpoch: 0,
|
||||
pollCadenceSec: 1,
|
||||
@@ -111,6 +117,10 @@ return view.extend({
|
||||
serverTruncated: 0,
|
||||
serverShed: null,
|
||||
degradedSampling: false,
|
||||
/* Layer 2 summary fallback — rows remain available behind an explicit toggle. */
|
||||
summaryMode: false,
|
||||
summaryRowsShown: false,
|
||||
summaryData: null,
|
||||
resolveLoadShed: false,
|
||||
visibilityBound: false,
|
||||
renderRaf: 0,
|
||||
@@ -769,6 +779,14 @@ return view.extend({
|
||||
|
||||
this.notePollRtt(rtt, errored);
|
||||
this.updateAdaptiveBanner();
|
||||
if (this.clientBackoffEnabled() && rtt > constants.POLL_RTT_SLOW_MS) {
|
||||
if (!this.summaryMode) this.enterSummaryMode(reply.summary);
|
||||
else {
|
||||
this.summaryData =
|
||||
reply.summary && typeof reply.summary === 'object' ? reply.summary : null;
|
||||
this.renderSummary();
|
||||
}
|
||||
}
|
||||
|
||||
const batch = this.normalizePollBatch(raw);
|
||||
this.lastPollNewEvents = batch.pollNew;
|
||||
@@ -884,6 +902,7 @@ return view.extend({
|
||||
|
||||
notePollRtt(ms, errored) {
|
||||
if (!this.clientBackoffEnabled()) {
|
||||
if (this.summaryMode) this.leaveSummaryMode();
|
||||
this.degradedSampling = false;
|
||||
/* Drop any partial streak so a slow sample from before the
|
||||
* adaptive:0 window cannot trip degraded on re-enable. */
|
||||
@@ -905,6 +924,7 @@ return view.extend({
|
||||
const cadence = this.cadenceForKind(kind);
|
||||
this.degradedSampling = cadence === constants.POLL_CADENCE_SLOW_S;
|
||||
if (cadence !== this.pollCadenceSec) this.setPollCadence(cadence);
|
||||
if (kind === 'fast' && this.summaryMode) this.leaveSummaryMode();
|
||||
},
|
||||
|
||||
stopPollingForHidden() {
|
||||
@@ -919,11 +939,12 @@ return view.extend({
|
||||
resumePollingAfterVisible() {
|
||||
this.bumpPollEpoch();
|
||||
this.resetRttHistory();
|
||||
/* In-flight poll from the hidden epoch must not apply after catch-up. */
|
||||
this.pollDataInFlight = false;
|
||||
if (!this.pollFn) this.pollFn = this.pollData.bind(this);
|
||||
this.setPollCadence(this.pollCadenceSec || constants.POLL_CADENCE_FAST_S);
|
||||
this.pollData();
|
||||
/* An old hidden-epoch request may still be on the wire. Queue the
|
||||
* catch-up behind it; stale-reply checks discard its application, while
|
||||
* the coordinator prevents a second server request from overlapping. */
|
||||
this.requestPoll();
|
||||
},
|
||||
|
||||
onVisibilityChange() {
|
||||
@@ -992,6 +1013,93 @@ return view.extend({
|
||||
}
|
||||
},
|
||||
|
||||
summaryListText(label, values) {
|
||||
const lines = [label + ':'];
|
||||
if (!Array.isArray(values) || !values.length) {
|
||||
lines.push(_('none'));
|
||||
return lines;
|
||||
}
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const item = values[i];
|
||||
if (!item || typeof item.value !== 'string') continue;
|
||||
const count = Number.isFinite(Number(item.count)) ? Number(item.count) : 0;
|
||||
lines.push('%s — %d'.format(item.value, count));
|
||||
}
|
||||
if (lines.length === 1) lines.push(_('none'));
|
||||
return lines;
|
||||
},
|
||||
|
||||
renderSummary() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
const body = document.getElementById('fwlive-summary-body');
|
||||
if (!card || !body) return;
|
||||
const summary = this.summaryData;
|
||||
const lines = [_('Showing a compact summary of the top of the shown sample.')];
|
||||
if (!summary || typeof summary !== 'object') {
|
||||
lines.push(_('Summary data is unavailable; show rows to inspect the sample.'));
|
||||
} else if (summary.truncated) {
|
||||
lines.push(_('Summary was bounded before all categories could be included.'));
|
||||
} else {
|
||||
this.summaryListText(_('Top talkers'), summary.top_talkers).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
this.summaryListText(_('Top drops'), summary.top_drops).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
this.summaryListText(_('Top rules'), summary.top_rules).forEach((line) =>
|
||||
lines.push(line)
|
||||
);
|
||||
}
|
||||
body.textContent = lines.join('\n');
|
||||
this.updateSummaryUi();
|
||||
},
|
||||
|
||||
updateSummaryUi() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
const scroll = document.getElementById('fwlive-scroll');
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
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' : '';
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.textContent = this.summaryRowsShown ? _('Hide rows') : _('Show rows');
|
||||
toggle.setAttribute('aria-pressed', this.summaryRowsShown ? 'true' : 'false');
|
||||
}
|
||||
},
|
||||
|
||||
enterSummaryMode(summary) {
|
||||
this.summaryMode = true;
|
||||
this.summaryRowsShown = false;
|
||||
this.summaryData = summary && typeof summary === 'object' ? summary : null;
|
||||
this.renderSummary();
|
||||
},
|
||||
|
||||
leaveSummaryMode() {
|
||||
if (!this.summaryMode) return;
|
||||
this.summaryMode = false;
|
||||
this.summaryRowsShown = false;
|
||||
this.summaryData = null;
|
||||
this.updateSummaryUi();
|
||||
this.renderRows(true);
|
||||
},
|
||||
|
||||
onSummaryRowsToggle() {
|
||||
if (!this.summaryMode) return;
|
||||
this.summaryRowsShown = !this.summaryRowsShown;
|
||||
this.updateSummaryUi();
|
||||
if (this.summaryRowsShown) this.renderRows(true);
|
||||
},
|
||||
|
||||
scheduleRenderRows(force) {
|
||||
const doForce = !!force;
|
||||
const epoch = this.pollEpoch;
|
||||
@@ -1193,7 +1301,7 @@ return view.extend({
|
||||
if (!wasPaused && this.paused) {
|
||||
this.pauseBufferLoading = true;
|
||||
this.updateStatus();
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.catch(function () {})
|
||||
.finally(
|
||||
function () {
|
||||
@@ -1209,7 +1317,7 @@ return view.extend({
|
||||
/* Merge pause buffer with the first live poll — do not replace. */
|
||||
this.resumeMerge = true;
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch === this.pollEpoch) this.renderRows(true);
|
||||
@@ -1232,7 +1340,7 @@ return view.extend({
|
||||
if (!this.paused) this.renderRows(true);
|
||||
else this.updateStatus();
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
@@ -1533,6 +1641,7 @@ return view.extend({
|
||||
{
|
||||
rows: rows.slice(),
|
||||
columns: this.activeColumns().slice(),
|
||||
forceRender: !!force,
|
||||
viewMode: this.viewMode,
|
||||
messageLayout: this.messageLayout,
|
||||
expandedRowId: this.expandedRowId,
|
||||
@@ -1641,14 +1750,82 @@ return view.extend({
|
||||
|
||||
const tintSel = document.getElementById('fwlive-row-tint');
|
||||
if (tintSel) tintSel.addEventListener('change', this.onRowTintPaletteChange.bind(this));
|
||||
|
||||
const summaryRows = document.getElementById('fwlive-summary-rows');
|
||||
if (summaryRows) summaryRows.addEventListener('click', this.onSummaryRowsToggle.bind(this));
|
||||
},
|
||||
|
||||
async pollData() {
|
||||
if (this.isTabHidden()) return;
|
||||
if (this.pollDataInFlight) return;
|
||||
return this.requestPoll();
|
||||
},
|
||||
|
||||
requestPoll() {
|
||||
if (this.isTabHidden()) return Promise.resolve();
|
||||
|
||||
const waiter = {};
|
||||
const promise = new Promise(function (resolve) {
|
||||
waiter.resolve = resolve;
|
||||
});
|
||||
|
||||
if (this.pollRequestPromise) {
|
||||
/* Keep one pending refresh intent. The latest view state is read when
|
||||
* that follow-up begins, so Pause/Resume/Limit changes coalesce safely. */
|
||||
this.pollRequestQueued = true;
|
||||
this.pollRequestQueuedWaiters.push(waiter);
|
||||
return promise;
|
||||
}
|
||||
|
||||
if (this.pollRequestQueued) {
|
||||
this.pollRequestQueuedWaiters.push(waiter);
|
||||
this.startPollRequest(this.pollRequestQueuedWaiters);
|
||||
this.pollRequestQueuedWaiters = [];
|
||||
this.pollRequestQueued = false;
|
||||
return promise;
|
||||
}
|
||||
|
||||
this.startPollRequest([waiter]);
|
||||
return promise;
|
||||
},
|
||||
|
||||
startPollRequest(waiters) {
|
||||
const epoch = this.pollEpoch;
|
||||
this.pollDataInFlight = true;
|
||||
const run = this.runPollRequest(epoch);
|
||||
this.pollRequestPromise = run;
|
||||
this.pollRequestWaiters = waiters;
|
||||
run.then(
|
||||
function (value) {
|
||||
this.finishPollRequest(run, value);
|
||||
}.bind(this),
|
||||
function () {
|
||||
/* runPollRequest normally absorbs local failures so the poll loop
|
||||
* remains alive; settle waiters even if a future change rejects. */
|
||||
this.finishPollRequest(run);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
finishPollRequest(run, value) {
|
||||
if (this.pollRequestPromise !== run) return;
|
||||
const waiters = this.pollRequestWaiters;
|
||||
this.pollRequestWaiters = [];
|
||||
this.pollRequestPromise = null;
|
||||
this.pollDataInFlight = false;
|
||||
for (let i = 0; i < waiters.length; i++) {
|
||||
waiters[i].resolve(value);
|
||||
}
|
||||
|
||||
/* If visibility changed while the request was active, retain the
|
||||
* queued intent until the visible catch-up can start it. */
|
||||
if (this.pollRequestQueued && !this.isTabHidden()) {
|
||||
const queued = this.pollRequestQueuedWaiters;
|
||||
this.pollRequestQueuedWaiters = [];
|
||||
this.pollRequestQueued = false;
|
||||
this.startPollRequest(queued);
|
||||
}
|
||||
},
|
||||
|
||||
async runPollRequest(epoch) {
|
||||
try {
|
||||
try {
|
||||
await this.fetchEntries();
|
||||
@@ -1661,7 +1838,11 @@ return view.extend({
|
||||
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
|
||||
/* Pause freezes row rendering but polling remains active for health and
|
||||
* cadence state; summary mode can therefore appear while rows are paused
|
||||
* and stays behind the explicit Show rows control. */
|
||||
if (this.paused) this.updateStatus();
|
||||
else if (this.summaryMode) this.renderSummary();
|
||||
else this.scheduleRenderRows(!!this.pendingForceRender);
|
||||
|
||||
try {
|
||||
@@ -1669,11 +1850,10 @@ return view.extend({
|
||||
} catch (e) {
|
||||
/* resolve unavailable — show IPs */
|
||||
}
|
||||
} finally {
|
||||
/* Only the owning epoch clears — a stale hidden-era poll must not
|
||||
* drop the catch-up guard after resume. Resume clears
|
||||
* explicitly before starting the catch-up poll. */
|
||||
if (epoch === this.pollEpoch) this.pollDataInFlight = false;
|
||||
} catch (e) {
|
||||
/* Keep the coordinator promise settling so a queued refresh cannot
|
||||
* be stranded by an unexpected local rendering failure. */
|
||||
this.lastPollError = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1706,7 +1886,7 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
return Promise.all([this.loadRulesMap(), this.loadLoggingStatus()]).then(() =>
|
||||
this.fetchEntries()
|
||||
this.requestPoll()
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1842,6 +2022,28 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'id': 'fwlive-flood', 'class': 'fwlive-flood' }, ['']),
|
||||
E('div', { 'id': 'fwlive-adaptive', 'class': 'fwlive-adaptive' }, ['']),
|
||||
E(
|
||||
'div',
|
||||
{ 'id': 'fwlive-summary', 'class': 'fwlive-summary', 'style': 'display:none' },
|
||||
[
|
||||
E('div', { 'class': 'fwlive-summary-head' }, [
|
||||
E('strong', {}, [_('Summary mode')]),
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'id': 'fwlive-summary-rows',
|
||||
'class': 'cbi-button fwlive-btn-ghost',
|
||||
'type': 'button',
|
||||
'aria-pressed': 'false'
|
||||
},
|
||||
[_('Show rows')]
|
||||
)
|
||||
]),
|
||||
E('pre', { 'id': 'fwlive-summary-body', 'class': 'fwlive-summary-body' }, [
|
||||
''
|
||||
])
|
||||
]
|
||||
),
|
||||
E('div', { 'id': 'fwlive-display-drawer', 'class': 'fwlive-display-bar' }, [
|
||||
E('span', { 'class': 'fwlive-display-bar-label' }, [_('Display options')]),
|
||||
E('div', { 'class': 'fwlive-display-controls' }, [
|
||||
|
||||
Reference in new issue
Block a user