Clicking a recent error opens the log on that line and acks it, so acknowledged means seen
This commit is contained in:
@@ -701,6 +701,14 @@ mark { background: #5d4037; color: #ffcc80; border-radius: 2px; }
|
||||
color: #555; border-color: #333; flex-shrink: 0; }
|
||||
.vv-ack-btn:hover { color: #aaa; border-color: #555; }
|
||||
.vv-err-line { color: #888; font-size: 11px; word-break: break-all; line-height: 1.4; }
|
||||
/* The error text opens the log at that line. Underlined on hover only — a permanently decorated
|
||||
line of log output is harder to read, and this block exists to be read first. */
|
||||
.vv-err-line-open { cursor: pointer; }
|
||||
.vv-err-line-open:hover { color: #ccc; text-decoration: underline; text-decoration-color: #4a4a4a; }
|
||||
/* The line the operator was sent to. Left bar rather than a full highlight: the log is dense and
|
||||
a filled background over one wrapped line reads as a block of colour, not a pointer. */
|
||||
.vv-log-jump { background: #23303a; color: #dfe8ee; border-left: 3px solid #4a7ea0;
|
||||
padding-left: 5px; margin-left: -8px; display: inline-block; width: calc(100% + 3px); }
|
||||
|
||||
/* Locks */
|
||||
.vv-locks-list { display: flex; flex-direction: column; }
|
||||
|
||||
@@ -1356,7 +1356,14 @@ function vvBackToSuggestions() {
|
||||
requestAnimationFrame(vvFitRight);
|
||||
}
|
||||
|
||||
// Text of the log line to keep marked, and whether the next render should scroll to it. Cleared
|
||||
// by anything that changes what is on screen — a different job, a search — so a mark can never
|
||||
// outlive the thing it was pointing at.
|
||||
let vvLogMark = null;
|
||||
let vvLogMarkScroll = false;
|
||||
|
||||
function vvOpenRight(id) {
|
||||
vvLogMark = null; vvLogMarkScroll = false;
|
||||
if (vvActiveId) {
|
||||
const old = document.querySelector('[data-id="' + CSS.escape(vvActiveId) + '"]');
|
||||
if (old) old.querySelector('.vv-job-row').classList.remove('vv-row-selected');
|
||||
@@ -1390,8 +1397,18 @@ function vvFetchRight() {
|
||||
const display = content.trim() ? (invert ? content.split('\n').reverse().join('\n') : content) : '(no log yet)';
|
||||
pre._raw = display;
|
||||
const term = document.getElementById('vv-log-search')?.value.trim() ?? '';
|
||||
if (term) vvFilterLog(term); else pre.textContent = display;
|
||||
if (autoScroll) {
|
||||
// Captured before the call, which consumes the flag: this render is the one that centres.
|
||||
const willCentre = !!vvLogMark && vvLogMarkScroll;
|
||||
let marked = false;
|
||||
if (term) vvFilterLog(term);
|
||||
else if (vvLogMark) { marked = vvMarkLogLine(pre, display, invert); if (!marked) pre.textContent = display; }
|
||||
else pre.textContent = display;
|
||||
// A jump that has just placed the line mid-view must not be immediately undone by the
|
||||
// auto-scroll-to-bottom every other render wants; later polls keep wherever the operator
|
||||
// has scrolled to while reading it.
|
||||
if (marked) {
|
||||
if (!willCentre) requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
|
||||
} else if (autoScroll) {
|
||||
pre.scrollTop = invert ? 0 : pre.scrollHeight;
|
||||
} else {
|
||||
requestAnimationFrame(() => { pre.scrollTop = savedScroll; });
|
||||
@@ -2671,6 +2688,71 @@ function vvErrIsAcked(script, ts) {
|
||||
return parseInt(localStorage.getItem('vv-ack-' + script) || '0') >= ts;
|
||||
}
|
||||
|
||||
// Opens the offending line in the log, and acknowledges it in the same click.
|
||||
//
|
||||
// Acking here is safe in a way a bare Ack button is not: the operator cannot dismiss this without
|
||||
// the line being put in front of them, so "acknowledged" means it was seen rather than cleared.
|
||||
// That is the only reason the two actions belong on one click.
|
||||
//
|
||||
// The line is located by its text rather than by a line number. board.php scans the last 200
|
||||
// lines and log.php serves the last 200, so an index would agree only while the log sat still —
|
||||
// and these are logs of things that are still running. Matching the text survives the window
|
||||
// moving underneath it, and when the line has genuinely scrolled out of the tail the log still
|
||||
// opens, just without the mark.
|
||||
function vvErrOpenAtLine(script, ts, lineText, el) {
|
||||
const search = document.getElementById('vv-log-search');
|
||||
// A filter would hide the surrounding lines, which are the reason for opening the log at all.
|
||||
if (search && search.value.trim()) search.value = '';
|
||||
vvOpenRight(script + '.sh');
|
||||
vvLogMark = lineText;
|
||||
vvLogMarkScroll = true;
|
||||
vvAckError(script, ts, el);
|
||||
}
|
||||
|
||||
// Marks one line in the rendered log and, the first time, scrolls it to the middle. Re-applied on
|
||||
// every poll so a live log does not blink the mark away while it is being read.
|
||||
function vvMarkLogLine(pre, display, invert) {
|
||||
const want = (vvLogMark || '').trim();
|
||||
if (!want) return false;
|
||||
const lines = display.split('\n');
|
||||
|
||||
// Scanned in file order from the end, because that is the occurrence board.php reported: it
|
||||
// walks the log backwards and stops at the first hit. Taking the first match instead would mark
|
||||
// an earlier copy of a line that repeats — "Findings: 1", "✗ failed" — and quietly point at a
|
||||
// previous run in the same file. Inverting the view reverses file order, so the direction has
|
||||
// to follow it.
|
||||
const n = lines.length;
|
||||
const seek = test => {
|
||||
for (let k = 0; k < n; k++) {
|
||||
const i = invert ? k : n - 1 - k;
|
||||
if (test(lines[i])) return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
let idx = seek(l => l.trim() === want);
|
||||
// board.php truncates the line at 220 characters, so an exact match is not guaranteed.
|
||||
if (idx < 0) idx = seek(l => l.includes(want));
|
||||
if (idx < 0 && want.length > 40) idx = seek(l => l.includes(want.slice(0, 40)));
|
||||
if (idx < 0) return false;
|
||||
|
||||
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
pre.innerHTML = lines.map((l, i) => i === idx
|
||||
? '<mark class="vv-log-jump" id="vv-log-jump-hit">' + esc(l) + '</mark>'
|
||||
: esc(l)).join('\n');
|
||||
|
||||
if (vvLogMarkScroll) {
|
||||
vvLogMarkScroll = false;
|
||||
const hit = document.getElementById('vv-log-jump-hit');
|
||||
if (hit) requestAnimationFrame(() => {
|
||||
// Measured against the pre rather than scrollIntoView(), which would also scroll the page.
|
||||
const rel = hit.getBoundingClientRect().top - pre.getBoundingClientRect().top + pre.scrollTop;
|
||||
pre.scrollTop = Math.max(0, rel - pre.clientHeight / 2);
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function vvAckError(script, ts, btn) {
|
||||
localStorage.setItem('vv-ack-' + script, ts);
|
||||
const row = btn.closest('.vv-err-row');
|
||||
@@ -2706,6 +2788,11 @@ function vvUpdateErrors(errors) {
|
||||
let html = '<div class="vv-errors-list">';
|
||||
for (const e of unacked) {
|
||||
const jobId = "'" + (e.script + '.sh').replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'";
|
||||
// JSON.stringify rather than the hand-rolled quote escaping above: this one is embedded in an
|
||||
// attribute alongside the error text, which is arbitrary log output and not to be trusted to
|
||||
// contain no quotes.
|
||||
const scriptJs = vvEscHtml(JSON.stringify(e.script));
|
||||
const lineJs = vvEscHtml(JSON.stringify(e.line));
|
||||
const label = e.script.split('/').pop().replace(/_/g, ' ');
|
||||
html += '<div class="vv-err-row">'
|
||||
+ '<div class="vv-err-top">'
|
||||
@@ -2713,7 +2800,10 @@ function vvUpdateErrors(errors) {
|
||||
+ '<span class="vv-err-age">' + vvFmtAge(now - e.ts) + '</span>'
|
||||
+ '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(\'' + e.script.replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'," + e.ts + ',this)">Ack</button>'
|
||||
+ '</div>'
|
||||
+ '<div class="vv-err-line">' + vvEscHtml(e.line) + '</div>'
|
||||
+ '<div class="vv-err-line vv-err-line-open"'
|
||||
+ ' title="Open the log at this line — also acknowledges it"'
|
||||
+ ' onclick="vvErrOpenAtLine(' + scriptJs + ',' + e.ts + ',' + lineJs + ',this)">'
|
||||
+ vvEscHtml(e.line) + '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
body.innerHTML = html + '</div>';
|
||||
@@ -2775,6 +2865,9 @@ function vvValidateCronExpr(expr) {
|
||||
function vvFilterLog(term) {
|
||||
const pre = document.getElementById('vv-log-pre');
|
||||
const raw = pre._raw || '';
|
||||
// Searching replaces the jumped-to line as the thing being looked for; leaving the mark set
|
||||
// would put it back on the next poll and fight the filter.
|
||||
if (term.trim()) { vvLogMark = null; vvLogMarkScroll = false; }
|
||||
if (!term.trim()) { pre.textContent = raw; return; }
|
||||
const esc = s => s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
const escRe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
Reference in New Issue
Block a user