mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-12 23:27:56 +08:00
🍕 Sync 2026-09-14 23:30:37
This commit is contained in:
1 parent
0603b094cf
commit
81fe4cd866
214 files changed
+10806
-2630
No files matched your search
@@ -18,6 +18,13 @@ return baseclass.extend({
|
||||
FETCH_LINES_MAX: 2000 /* ubus poll / logd ring cap (~2000 lines ≈ typical ring) */,
|
||||
/* DOM budget: ~250 new/updated rows painted per second on typical LuCI routers */
|
||||
RENDER_CAP_PER_SEC: 250,
|
||||
/* #306 Layer 2 — measured client RTT → poll cadence (seconds). */
|
||||
POLL_CADENCE_FAST_S: 1,
|
||||
POLL_CADENCE_MID_S: 2,
|
||||
POLL_CADENCE_SLOW_S: 5,
|
||||
POLL_RTT_FAST_MS: 300,
|
||||
POLL_RTT_SLOW_MS: 1500,
|
||||
POLL_RTT_STREAK: 3,
|
||||
VIEW_MODES: ['simple', 'detailed'],
|
||||
COLUMN_SETS: {
|
||||
simple: ['action', 'time', 'iface', 'flow', 'proto', 'rule'],
|
||||
|
||||
@@ -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
|
||||
});
|
||||
@@ -660,6 +660,16 @@
|
||||
color: var(--text-color-high);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.fwlive-adaptive {
|
||||
display: none;
|
||||
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);
|
||||
font-size: 0.92em;
|
||||
}
|
||||
.fwlive-tint-warn {
|
||||
display: none;
|
||||
color: var(--warn-color-high);
|
||||
|
||||
@@ -35,8 +35,8 @@ const callFwliveRules = rpc.declare({
|
||||
const callFwliveResolve = rpc.declare({
|
||||
object: 'fwlive',
|
||||
method: 'resolve',
|
||||
params: ['addresses'],
|
||||
expect: { names: {} }
|
||||
params: ['addresses']
|
||||
/* Full reply kept so disabled:"load" reaches the view (#306 Layer 2). */
|
||||
});
|
||||
|
||||
const callFwliveLoggingStatus = rpc.declare({
|
||||
@@ -102,12 +102,25 @@ return view.extend({
|
||||
resumeMerge: false,
|
||||
pollFn: null,
|
||||
pollDataInFlight: false,
|
||||
/* #306 Layer 2 — visibility / RTT cadence / shed surfacing. */
|
||||
pollEpoch: 0,
|
||||
pollCadenceSec: 1,
|
||||
rttStreakKind: null,
|
||||
rttStreakCount: 0,
|
||||
serverAdaptive: 1,
|
||||
serverTruncated: 0,
|
||||
serverShed: null,
|
||||
degradedSampling: false,
|
||||
resolveLoadShed: false,
|
||||
visibilityBound: false,
|
||||
renderRaf: 0,
|
||||
filterInputTimer: null,
|
||||
messageLayout: 'wrap',
|
||||
renderBucket: constants.RENDER_CAP_PER_SEC,
|
||||
renderBucketMs: 0,
|
||||
floodSuppressed: false,
|
||||
pendingForceRender: false,
|
||||
pendingRenderEpoch: null,
|
||||
lastPollNewEvents: 0,
|
||||
showHostnames: false,
|
||||
rowTint: constants.DEFAULT_ROW_TINT,
|
||||
@@ -702,7 +715,11 @@ return view.extend({
|
||||
async fetchEntries() {
|
||||
if (!this.sessionSeen) this.sessionSeen = new Set();
|
||||
|
||||
const epoch = this.pollEpoch;
|
||||
const resumeMerge = !!this.resumeMerge;
|
||||
const t0 = this.nowMs();
|
||||
let reply;
|
||||
let errored = false;
|
||||
try {
|
||||
/* Raw logd lines, not post-filter rows. Fetch a multiple of the
|
||||
* display limit so mixed syslog still fills the table; pause
|
||||
@@ -714,25 +731,44 @@ return view.extend({
|
||||
addresses: [String(fetchLines)]
|
||||
});
|
||||
} catch (e) {
|
||||
this.lastPollError = true;
|
||||
return;
|
||||
errored = true;
|
||||
reply = null;
|
||||
}
|
||||
/* Full poll object (no rpc expect strip). error → banner; log → rows. */
|
||||
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
|
||||
const rtt = this.nowMs() - t0;
|
||||
|
||||
if (!reply || typeof reply !== 'object' || Array.isArray(reply)) {
|
||||
this.lastPollError = true;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
return;
|
||||
}
|
||||
if (reply.error) {
|
||||
this.lastPollError = true;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
return;
|
||||
}
|
||||
const raw = reply.log;
|
||||
if (!Array.isArray(raw)) {
|
||||
this.lastPollError = true;
|
||||
this.notePollRtt(rtt, true);
|
||||
this.updateAdaptiveBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPollError = false;
|
||||
this.serverAdaptive = reply.adaptive === 0 || reply.adaptive === false ? 0 : 1;
|
||||
this.serverTruncated = reply.truncated ? 1 : 0;
|
||||
this.serverShed =
|
||||
reply.shed && typeof reply.shed === 'object' && !Array.isArray(reply.shed)
|
||||
? reply.shed
|
||||
: null;
|
||||
|
||||
this.notePollRtt(rtt, errored);
|
||||
this.updateAdaptiveBanner();
|
||||
|
||||
const batch = this.normalizePollBatch(raw);
|
||||
this.lastPollNewEvents = batch.pollNew;
|
||||
@@ -740,10 +776,13 @@ return view.extend({
|
||||
/* Oldest-first ring buffer; filteredRows() reverses for newest-first display. */
|
||||
this.entries = buffer.applyFetchedEntries(this.entries, batch.rows, {
|
||||
paused: this.paused,
|
||||
resumeMerge: this.resumeMerge,
|
||||
resumeMerge: resumeMerge,
|
||||
rowLimit: this.rowLimit,
|
||||
fetchLinesMax: constants.FETCH_LINES_MAX
|
||||
});
|
||||
/* A stale request returns above. Keep this obligation until a current
|
||||
* request has actually applied the merged batch. */
|
||||
if (resumeMerge) this.resumeMerge = false;
|
||||
},
|
||||
|
||||
rememberSessionId(id) {
|
||||
@@ -772,11 +811,217 @@ return view.extend({
|
||||
const cap = this.ingestCap();
|
||||
if (this.entries.length >= cap && cap > 0) bits.push(_('buffer full'));
|
||||
if (this.floodSuppressed) bits.push(_('render paused (high rate)'));
|
||||
if (this.degradedSampling && this.serverAdaptive !== 0) bits.push(_('Degraded — sampling'));
|
||||
if (this.serverTruncated && this.serverAdaptive !== 0) bits.push(_('truncated'));
|
||||
if (this.resolveLoadShed && this.serverAdaptive !== 0)
|
||||
bits.push(_('resolve paused (load)'));
|
||||
if (!this.paused && !this.followLive)
|
||||
bits.push(_('scroll frozen — scroll to top to follow live'));
|
||||
return bits.length ? ' — ' + bits.join(', ') : '';
|
||||
},
|
||||
|
||||
nowMs() {
|
||||
if (typeof performance !== 'undefined' && performance.now) return performance.now();
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
isTabHidden() {
|
||||
return typeof document !== 'undefined' && !!document.hidden;
|
||||
},
|
||||
|
||||
/* Classify one RTT sample into hysteresis buckets (#306).
|
||||
* error and >1.5s share the slow/degraded streak so mixed failures still trip N=3. */
|
||||
rttKindFromMs(ms, errored) {
|
||||
if (errored) return 'slow';
|
||||
if (!(ms >= 0)) return 'slow';
|
||||
if (ms < constants.POLL_RTT_FAST_MS) return 'fast';
|
||||
if (ms <= constants.POLL_RTT_SLOW_MS) return 'mid';
|
||||
return 'slow';
|
||||
},
|
||||
|
||||
cadenceForKind(kind) {
|
||||
if (kind === 'fast') return constants.POLL_CADENCE_FAST_S;
|
||||
if (kind === 'mid') return constants.POLL_CADENCE_MID_S;
|
||||
return constants.POLL_CADENCE_SLOW_S;
|
||||
},
|
||||
|
||||
resetRttHistory() {
|
||||
this.rttStreakKind = null;
|
||||
this.rttStreakCount = 0;
|
||||
},
|
||||
|
||||
bumpPollEpoch() {
|
||||
this.pollEpoch = (this.pollEpoch || 0) + 1;
|
||||
return this.pollEpoch;
|
||||
},
|
||||
|
||||
clientBackoffEnabled() {
|
||||
return this.serverAdaptive !== 0;
|
||||
},
|
||||
|
||||
setPollCadence(sec) {
|
||||
const next = sec > 0 ? sec : constants.POLL_CADENCE_FAST_S;
|
||||
if (!this.pollFn) {
|
||||
this.pollCadenceSec = next;
|
||||
return;
|
||||
}
|
||||
if (this.isTabHidden()) {
|
||||
this.pollCadenceSec = next;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
poll.remove(this.pollFn);
|
||||
} catch (e) {
|
||||
/* poll gone */
|
||||
}
|
||||
this.pollCadenceSec = next;
|
||||
try {
|
||||
poll.add(this.pollFn, next);
|
||||
} catch (e) {
|
||||
/* poll gone */
|
||||
}
|
||||
},
|
||||
|
||||
notePollRtt(ms, errored) {
|
||||
if (!this.clientBackoffEnabled()) {
|
||||
this.degradedSampling = false;
|
||||
/* Drop any partial streak so a slow sample from before the
|
||||
* adaptive:0 window cannot trip degraded on re-enable. */
|
||||
this.resetRttHistory();
|
||||
if (this.pollCadenceSec !== constants.POLL_CADENCE_FAST_S)
|
||||
this.setPollCadence(constants.POLL_CADENCE_FAST_S);
|
||||
return;
|
||||
}
|
||||
|
||||
const kind = this.rttKindFromMs(ms, errored);
|
||||
if (kind === this.rttStreakKind) this.rttStreakCount++;
|
||||
else {
|
||||
this.rttStreakKind = kind;
|
||||
this.rttStreakCount = 1;
|
||||
}
|
||||
|
||||
if (this.rttStreakCount < constants.POLL_RTT_STREAK) return;
|
||||
|
||||
const cadence = this.cadenceForKind(kind);
|
||||
this.degradedSampling = cadence === constants.POLL_CADENCE_SLOW_S;
|
||||
if (cadence !== this.pollCadenceSec) this.setPollCadence(cadence);
|
||||
},
|
||||
|
||||
stopPollingForHidden() {
|
||||
if (!this.pollFn) return;
|
||||
try {
|
||||
poll.remove(this.pollFn);
|
||||
} catch (e) {
|
||||
/* poll gone */
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
onVisibilityChange() {
|
||||
if (this.isTabHidden()) {
|
||||
this.stopPollingForHidden();
|
||||
this.bumpPollEpoch();
|
||||
return;
|
||||
}
|
||||
this.resumePollingAfterVisible();
|
||||
},
|
||||
|
||||
bindVisibility() {
|
||||
if (this.visibilityBound) return;
|
||||
if (typeof document === 'undefined' || !document.addEventListener) return;
|
||||
this.visibilityBound = true;
|
||||
this.visibilityHandler = function () {
|
||||
this.onVisibilityChange();
|
||||
}.bind(this);
|
||||
document.addEventListener('visibilitychange', this.visibilityHandler);
|
||||
},
|
||||
|
||||
unbindVisibility() {
|
||||
if (
|
||||
typeof document !== 'undefined' &&
|
||||
document.removeEventListener &&
|
||||
this.visibilityHandler
|
||||
) {
|
||||
try {
|
||||
document.removeEventListener('visibilitychange', this.visibilityHandler);
|
||||
} catch (e) {
|
||||
/* document gone */
|
||||
}
|
||||
}
|
||||
this.visibilityHandler = null;
|
||||
this.visibilityBound = false;
|
||||
},
|
||||
|
||||
updateAdaptiveBanner() {
|
||||
const el = document.getElementById('fwlive-adaptive');
|
||||
if (!el) return;
|
||||
if (!el.style) el.style = { display: '' };
|
||||
|
||||
if (!this.clientBackoffEnabled()) {
|
||||
el.style.display = 'none';
|
||||
el.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (this.degradedSampling)
|
||||
parts.push(_('Degraded — sampling (slow poll RTT; cadence reduced).'));
|
||||
if (this.serverShed && this.serverShed.limit)
|
||||
parts.push(
|
||||
_('Server shedding — at most %d log lines per poll.').format(this.serverShed.limit)
|
||||
);
|
||||
else if (this.serverTruncated) parts.push(_('Server truncated this poll (adaptive cap).'));
|
||||
if (this.resolveLoadShed)
|
||||
parts.push(_('Hostname resolve paused while the router is under load.'));
|
||||
|
||||
if (parts.length) {
|
||||
el.style.display = 'block';
|
||||
el.textContent = parts.join(' ');
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
el.textContent = '';
|
||||
}
|
||||
},
|
||||
|
||||
scheduleRenderRows(force) {
|
||||
const doForce = !!force;
|
||||
const epoch = this.pollEpoch;
|
||||
if (typeof requestAnimationFrame !== 'function') {
|
||||
this.renderRows(doForce);
|
||||
return;
|
||||
}
|
||||
if (this.renderRaf) {
|
||||
this.pendingForceRender = this.pendingForceRender || doForce;
|
||||
this.pendingRenderEpoch = epoch;
|
||||
return;
|
||||
}
|
||||
this.renderRaf = requestAnimationFrame(
|
||||
function () {
|
||||
this.renderRaf = 0;
|
||||
const f = doForce || !!this.pendingForceRender;
|
||||
const pendingEpoch = this.pendingRenderEpoch;
|
||||
this.pendingForceRender = false;
|
||||
this.pendingRenderEpoch = null;
|
||||
if (epoch !== this.pollEpoch) {
|
||||
/* A newer epoch may have requested a render while this frame was
|
||||
* queued. Drop stale paint and requeue only that current request. */
|
||||
if (pendingEpoch === this.pollEpoch) this.scheduleRenderRows(f);
|
||||
return;
|
||||
}
|
||||
this.renderRows(f);
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
refillRenderBucket() {
|
||||
const now = Date.now();
|
||||
if (!this.renderBucketMs) this.renderBucketMs = now;
|
||||
@@ -828,6 +1073,7 @@ return view.extend({
|
||||
updateFloodBanner() {
|
||||
const el = document.getElementById('fwlive-flood');
|
||||
if (!el) return;
|
||||
if (!el.style) el.style = { display: '' };
|
||||
|
||||
if (this.floodSuppressed) {
|
||||
el.style.display = 'block';
|
||||
@@ -879,11 +1125,13 @@ return view.extend({
|
||||
if (this.lastPollError) {
|
||||
status.className = 'fwlive-status fwlive-status-error';
|
||||
status.textContent = _('Connection lost — retrying…') + suffix;
|
||||
this.updateAdaptiveBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
status.className = this.paused ? 'fwlive-status fwlive-status-paused' : 'fwlive-status';
|
||||
status.textContent = this.compactCountText(matchCount);
|
||||
this.updateAdaptiveBanner();
|
||||
},
|
||||
|
||||
readRowLimit() {
|
||||
@@ -960,13 +1208,13 @@ return view.extend({
|
||||
this.followLive = true;
|
||||
/* Merge pause buffer with the first live poll — do not replace (#43). */
|
||||
this.resumeMerge = true;
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
.then(() => this.renderRows(true))
|
||||
.finally(
|
||||
function () {
|
||||
this.resumeMerge = false;
|
||||
}.bind(this)
|
||||
);
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch === this.pollEpoch) this.renderRows(true);
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -983,8 +1231,11 @@ return view.extend({
|
||||
this.pendingForceRender = true;
|
||||
if (!this.paused) this.renderRows(true);
|
||||
else this.updateStatus();
|
||||
const epoch = this.pollEpoch;
|
||||
this.fetchEntries()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
if (this.paused) this.updateStatus();
|
||||
else this.renderRows(true);
|
||||
})
|
||||
@@ -1063,6 +1314,9 @@ return view.extend({
|
||||
const ips = this.collectIpsFromEntries(entries);
|
||||
const need = [];
|
||||
const now = Date.now();
|
||||
/* While the router sheds resolve load, hold the paused banner without
|
||||
* re-asking every poll — retry after the cooldown expires. */
|
||||
if (this.resolveLoadShed && now < (this.resolveShedUntil || 0)) return;
|
||||
|
||||
for (let i = 0; i < ips.length && need.length < 32; i++) {
|
||||
const ip = ips[i];
|
||||
@@ -1081,8 +1335,28 @@ return view.extend({
|
||||
const res = await callFwliveResolve({ addresses: need });
|
||||
if (gen !== this.resolveGeneration) return;
|
||||
|
||||
/* rpc.declare expect: { names: {} } already unwraps — res IS the map (#243). */
|
||||
const names = res || {};
|
||||
if (res && typeof res === 'object' && res.disabled === 'load') {
|
||||
this.resolveLoadShed = true;
|
||||
this.resolveShedUntil = Date.now() + 60000;
|
||||
this.updateAdaptiveBanner();
|
||||
return;
|
||||
}
|
||||
|
||||
this.resolveLoadShed = false;
|
||||
this.resolveShedUntil = 0;
|
||||
/* RPC-level failure (no_resolver, jshn_missing, invalid_input): the
|
||||
* reply carries no per-address signal, so return without failMark —
|
||||
* otherwise every address looks like "no PTR" and retries stall
|
||||
* for the failure TTL. */
|
||||
if (res && typeof res === 'object' && typeof res.error === 'string' && res.error)
|
||||
return;
|
||||
/* Full reply: names map under .names; legacy expect-unwrap was the map. */
|
||||
const names =
|
||||
res && typeof res === 'object' && res.names && typeof res.names === 'object'
|
||||
? res.names
|
||||
: res && typeof res === 'object' && !Array.isArray(res)
|
||||
? res
|
||||
: {};
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < need.length; i++) {
|
||||
@@ -1096,7 +1370,8 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) this.renderRows(true);
|
||||
this.updateAdaptiveBanner();
|
||||
if (updated) this.scheduleRenderRows(true);
|
||||
} catch (e) {
|
||||
/* resolve unavailable — show IPs */
|
||||
} finally {
|
||||
@@ -1221,7 +1496,7 @@ return view.extend({
|
||||
|
||||
renderRows(force) {
|
||||
const el = document.getElementById('fwlive-table');
|
||||
if (!el) return;
|
||||
if (!el || typeof el.querySelector !== 'function') return;
|
||||
|
||||
const body = el.querySelector('tbody');
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
@@ -1369,18 +1644,25 @@ return view.extend({
|
||||
},
|
||||
|
||||
async pollData() {
|
||||
if (this.isTabHidden()) return;
|
||||
if (this.pollDataInFlight) return;
|
||||
|
||||
const epoch = this.pollEpoch;
|
||||
this.pollDataInFlight = true;
|
||||
try {
|
||||
try {
|
||||
await this.fetchEntries();
|
||||
} catch (e) {
|
||||
/* fetchEntries already accounts the poll RTT for every rpc
|
||||
* outcome; a throw here is a local normalize/buffer bug, not
|
||||
* network slowness, so count nothing further. */
|
||||
this.lastPollError = true;
|
||||
}
|
||||
|
||||
if (epoch !== this.pollEpoch) return;
|
||||
|
||||
if (this.paused) this.updateStatus();
|
||||
else this.renderRows(!!this.pendingForceRender);
|
||||
else this.scheduleRenderRows(!!this.pendingForceRender);
|
||||
|
||||
try {
|
||||
await this.resolveHostnamesForEntries(this.filteredRows());
|
||||
@@ -1388,19 +1670,25 @@ return view.extend({
|
||||
/* resolve unavailable — show IPs */
|
||||
}
|
||||
} finally {
|
||||
this.pollDataInFlight = false;
|
||||
/* Only the owning epoch clears — a stale hidden-era poll must not
|
||||
* drop the catch-up guard after resume (Grok #331). Resume clears
|
||||
* explicitly before starting the catch-up poll. */
|
||||
if (epoch === this.pollEpoch) this.pollDataInFlight = false;
|
||||
}
|
||||
},
|
||||
|
||||
load() {
|
||||
this.bindVisibility();
|
||||
if (!this.pollFn) {
|
||||
this.pollFn = this.pollData.bind(this);
|
||||
poll.add(this.pollFn, 1);
|
||||
this.pollCadenceSec = constants.POLL_CADENCE_FAST_S;
|
||||
if (!this.isTabHidden()) poll.add(this.pollFn, this.pollCadenceSec);
|
||||
/* Best-effort teardown when leaving the page (LuCI SPA may full-reload). */
|
||||
if (typeof window !== 'undefined' && window.addEventListener) {
|
||||
window.addEventListener(
|
||||
'pagehide',
|
||||
function () {
|
||||
this.unbindVisibility();
|
||||
if (this.pollFn) {
|
||||
try {
|
||||
poll.remove(this.pollFn);
|
||||
@@ -1553,6 +1841,7 @@ return view.extend({
|
||||
)
|
||||
]),
|
||||
E('div', { 'id': 'fwlive-flood', 'class': 'fwlive-flood' }, ['']),
|
||||
E('div', { 'id': 'fwlive-adaptive', 'class': 'fwlive-adaptive' }, ['']),
|
||||
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