Stage 397: PR #2671 — fix: SSE reconnect jitter + cron run row inline expansion
Closes #2629 Closes #2661 Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,11 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Session-list SSE reconnects now use bounded jitter/backoff instead of a fixed 5-second retry, reducing reconnect bursts after restarts or network drops.
|
||||
- Expanded cron run rows now render the full output inline immediately; the truncated preview remains only for collapsed rows, and the full-output fallback no longer drops content when Markdown rendering is unavailable.
|
||||
|
||||
|
||||
## [v0.51.103] — 2026-05-21 — Release CA (stage-396 — 1-PR follow-on — Settings → Plugins distinguishes exclusive/provider activation)
|
||||
|
||||
|
||||
@@ -658,11 +658,14 @@ async function _loadRunContent(jobId, filename, runId){
|
||||
body.textContent = data.error;
|
||||
return;
|
||||
}
|
||||
const expanded = _cronExpansionGet(_cronRunExpandKey(jobId, filename));
|
||||
const output = expanded ? (data.content || data.snippet || '') : (data.snippet || data.content || '');
|
||||
body.classList.toggle('expanded', expanded);
|
||||
// Render markdown content using the same renderer as chat messages
|
||||
if (typeof renderMd === 'function') {
|
||||
body.innerHTML = renderMd(data.snippet || data.content);
|
||||
body.innerHTML = renderMd(output);
|
||||
} else {
|
||||
body.textContent = data.snippet || data.content;
|
||||
body.textContent = output;
|
||||
}
|
||||
const usageStrip = _formatCronRunUsageStrip(data.usage);
|
||||
if (usageStrip) {
|
||||
@@ -671,13 +674,15 @@ async function _loadRunContent(jobId, filename, runId){
|
||||
usage.textContent = usageStrip;
|
||||
body.appendChild(usage);
|
||||
}
|
||||
// Show "View full output" button if content was truncated
|
||||
if (data.content && data.snippet && data.content.length > data.snippet.length) {
|
||||
// Show "View full output" button only for collapsed previews. Expanded rows render the full body inline.
|
||||
if (!expanded && data.content && data.snippet && data.content.length > data.snippet.length) {
|
||||
const btn = document.createElement('button');
|
||||
btn.style.cssText = 'margin-top:8px;padding:4px 12px;border-radius:var(--radius-btn);border:1px solid var(--border-subtle);background:var(--surface-subtle);color:var(--text-secondary);cursor:pointer;font-size:12px';
|
||||
btn.textContent = t('cron_view_full_output') || 'View full output';
|
||||
btn.onclick = () => {
|
||||
body.innerHTML = renderMd ? renderMd(data.content) : '';
|
||||
_cronExpansionSet(_cronRunExpandKey(jobId, filename), true);
|
||||
body.classList.add('expanded');
|
||||
body.innerHTML = renderMd ? renderMd(data.content) : data.content;
|
||||
btn.remove();
|
||||
};
|
||||
body.appendChild(btn);
|
||||
|
||||
@@ -2112,6 +2112,16 @@ let _sessionEventsSSE = null;
|
||||
let _sessionEventsRefreshTimer = 0;
|
||||
let _sessionEventsReconnectTimer = 0;
|
||||
let _sessionEventsNeedsRefreshOnOpen = false;
|
||||
let _sessionEventsReconnectAttempt = 0;
|
||||
const _sessionEventsReconnectBaseMs = 5000;
|
||||
const _sessionEventsReconnectMaxMs = 30000;
|
||||
|
||||
function _sessionEventsReconnectDelayMs(){
|
||||
const attempt = Math.max(0, Number(_sessionEventsReconnectAttempt || 0));
|
||||
const base = Math.min(_sessionEventsReconnectMaxMs, _sessionEventsReconnectBaseMs * Math.pow(2, attempt));
|
||||
const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(base * 0.35)));
|
||||
return Math.min(_sessionEventsReconnectMaxMs, Math.floor(base * 0.75) + jitter);
|
||||
}
|
||||
let _sessionListRefreshInFlight = false;
|
||||
let _sessionListRefreshPendingReason = '';
|
||||
|
||||
@@ -2233,6 +2243,7 @@ function ensureSessionEventsSSE(){
|
||||
// Same-origin relative URL preserves subpath mounts and normal WebUI cookies.
|
||||
_sessionEventsSSE = new EventSource('api/sessions/events');
|
||||
_sessionEventsSSE.onopen = () => {
|
||||
_sessionEventsReconnectAttempt = 0;
|
||||
if(!_sessionEventsNeedsRefreshOnOpen) return;
|
||||
_sessionEventsNeedsRefreshOnOpen = false;
|
||||
void refreshSessionList('reconnect');
|
||||
@@ -2244,10 +2255,12 @@ function ensureSessionEventsSSE(){
|
||||
_sessionEventsNeedsRefreshOnOpen = true;
|
||||
_closeSessionEventsSSE();
|
||||
if(_sessionEventsReconnectTimer) return;
|
||||
const delayMs = _sessionEventsReconnectDelayMs();
|
||||
_sessionEventsReconnectAttempt = Math.min(_sessionEventsReconnectAttempt + 1, 6);
|
||||
_sessionEventsReconnectTimer = setTimeout(() => {
|
||||
_sessionEventsReconnectTimer = 0;
|
||||
ensureSessionEventsSSE();
|
||||
}, 5000);
|
||||
}, delayMs);
|
||||
};
|
||||
}catch(e){
|
||||
_closeSessionEventsSSE();
|
||||
|
||||
29
tests/test_issue2661_2629_frontend.py
Normal file
29
tests/test_issue2661_2629_frontend.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from pathlib import Path
|
||||
|
||||
SESSIONS_JS = Path("static/sessions.js").read_text(encoding="utf-8")
|
||||
PANELS_JS = Path("static/panels.js").read_text(encoding="utf-8")
|
||||
CHANGELOG = Path("CHANGELOG.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_session_events_reconnect_uses_jittered_backoff_not_fixed_delay():
|
||||
assert "function _sessionEventsReconnectDelayMs()" in SESSIONS_JS
|
||||
assert "Math.random()" in SESSIONS_JS
|
||||
assert "_sessionEventsReconnectMaxMs" in SESSIONS_JS
|
||||
assert "_sessionEventsReconnectAttempt = 0" in SESSIONS_JS
|
||||
ensure_fn = SESSIONS_JS[SESSIONS_JS.find("function ensureSessionEventsSSE()") :]
|
||||
assert "const delayMs = _sessionEventsReconnectDelayMs();" in ensure_fn
|
||||
assert "}, 5000);" not in ensure_fn
|
||||
|
||||
|
||||
def test_cron_expanded_run_renders_full_content_inline():
|
||||
assert "const expanded = _cronExpansionGet(_cronRunExpandKey(jobId, filename));" in PANELS_JS
|
||||
assert "const output = expanded ? (data.content || data.snippet || '') : (data.snippet || data.content || '');" in PANELS_JS
|
||||
assert "if (!expanded && data.content && data.snippet && data.content.length > data.snippet.length)" in PANELS_JS
|
||||
assert "_cronExpansionSet(_cronRunExpandKey(jobId, filename), true);" in PANELS_JS
|
||||
|
||||
|
||||
def test_changelog_mentions_session_and_cron_polish():
|
||||
unreleased = CHANGELOG.split("## [v0.51.103]", 1)[0]
|
||||
assert "bounded jitter/backoff" in unreleased
|
||||
assert "Expanded cron run rows" in unreleased
|
||||
assert "no longer drops content when Markdown rendering is unavailable" in unreleased
|
||||
Reference in New Issue
Block a user