mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 10:30:57 +01:00
Make the startup log a bounded, soft-refreshing scrolling viewport
Order the startup log oldest-to-newest inside a height-bounded panel that scrolls internally and never extends past the bottom of the window. Refresh it with a background fetch that swaps the log list in place instead of reloading the whole page, preserving the user's scroll position and only following to the bottom when they are already there. A full page reload now happens only on the final transition to the running server or to the error state.
This commit is contained in:
@@ -41,7 +41,8 @@
|
||||
background-color: var(--jf-bg);
|
||||
background-image: radial-gradient(circle at 50% -10%, var(--jf-bg-accent), var(--jf-bg) 60%);
|
||||
color: var(--jf-text);
|
||||
padding: 2.5rem 1rem 4rem;
|
||||
/* Bottom padding doubles as the minimum gap the log viewport keeps from the window bottom. */
|
||||
padding: 2.5rem 1rem 50px;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
@@ -162,6 +163,25 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Bounded, scrollable log viewport that scales with the window height. */
|
||||
.logs-panel .logs-scroll {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 0.25rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--jf-border) transparent;
|
||||
}
|
||||
|
||||
.logs-panel .logs-scroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.logs-panel .logs-scroll::-webkit-scrollbar-thumb {
|
||||
background-color: var(--jf-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ol.action-list * {
|
||||
font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", Menlo, Consolas, monospace;
|
||||
font-weight: 400;
|
||||
@@ -314,7 +334,7 @@
|
||||
<details open>
|
||||
<summary>{{DateOfCreation}} - {{Content}}</summary>
|
||||
<ul class="action-list">
|
||||
{{--| #EACH children.Reverse() |-}}
|
||||
{{--| #EACH children |-}}
|
||||
{{#IMPORT 'LogEntry'}}
|
||||
{{--| /EACH |-}}
|
||||
</ul>
|
||||
@@ -332,22 +352,106 @@
|
||||
<h2>Startup log</h2>
|
||||
<a class="download-logs" href='/startup/logger' target="_blank">Download full logs</a>
|
||||
</div>
|
||||
<ol class="action-list">
|
||||
{{#FOREACH log IN logs.Reverse()}}
|
||||
{{#IMPORT 'LogEntry' #WITH log}}
|
||||
{{/FOREACH}}
|
||||
</ol>
|
||||
<div class="logs-scroll" id="logs-scroll">
|
||||
<ol class="action-list">
|
||||
{{#FOREACH log IN logs}}
|
||||
{{#IMPORT 'LogEntry' #WITH log}}
|
||||
{{/FOREACH}}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{{/IF}}
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var reporting = false;
|
||||
{{#IF isInReportingMode}}
|
||||
reporting = true;
|
||||
{{/IF}}
|
||||
var intervalMs = {{ retryValue.TotalMilliseconds }};
|
||||
|
||||
var box = document.getElementById('logs-scroll');
|
||||
var panel = box ? box.closest('.logs-panel') : null;
|
||||
|
||||
function px(el, prop) {
|
||||
return el ? (parseFloat(getComputedStyle(el)[prop]) || 0) : 0;
|
||||
}
|
||||
|
||||
// Size the log viewport so the panel never runs past the bottom of the window
|
||||
// (keeping the body's bottom padding as the margin). Recomputed on resize.
|
||||
function fit() {
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
var topFromDoc = box.getBoundingClientRect().top + window.scrollY;
|
||||
var below = px(panel, 'paddingBottom') + px(document.body, 'paddingBottom');
|
||||
box.style.maxHeight = Math.max(window.innerHeight - topFromDoc - below, 120) + 'px';
|
||||
}
|
||||
|
||||
function nearBottom(el) {
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight < 24;
|
||||
}
|
||||
|
||||
fit();
|
||||
window.addEventListener('resize', fit);
|
||||
window.addEventListener('load', fit);
|
||||
if (box) {
|
||||
box.scrollTop = box.scrollHeight; // start pinned to the newest entry
|
||||
}
|
||||
|
||||
// In the terminal error state the page is static, so stop here.
|
||||
if (reporting) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Soft refresh: pull the page in the background and swap the log list + activity line in
|
||||
// place, so polling never disturbs where the user has scrolled. Only follow to the bottom
|
||||
// when the user is already there. A real reload happens only on the final transition.
|
||||
function poll() {
|
||||
fetch(window.location.href, { cache: 'no-store' }).then(function (resp) {
|
||||
if (resp.ok) {
|
||||
// The real server is now answering (HTTP 200) -> load the actual app.
|
||||
window.location.reload();
|
||||
return null;
|
||||
}
|
||||
return resp.text();
|
||||
}).then(function (html) {
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
// Startup failed and the page switched to the error view -> reload to render it.
|
||||
if (doc.querySelector('.status-card.is-error')) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
var newStatus = doc.querySelector('.status-text');
|
||||
var curStatus = document.querySelector('.status-text');
|
||||
if (newStatus && curStatus) {
|
||||
curStatus.innerHTML = newStatus.innerHTML;
|
||||
}
|
||||
|
||||
if (box) {
|
||||
var newList = doc.querySelector('#logs-scroll .action-list');
|
||||
var curList = box.querySelector('.action-list');
|
||||
if (newList && curList) {
|
||||
var stick = nearBottom(box);
|
||||
var prevTop = box.scrollTop;
|
||||
curList.replaceWith(document.importNode(newList, true));
|
||||
fit();
|
||||
box.scrollTop = stick ? box.scrollHeight : prevTop;
|
||||
}
|
||||
}
|
||||
}).catch(function () {
|
||||
// Server is mid-transition (port rebinding); just try again on the next tick.
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(poll, intervalMs);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
{{^IF isInReportingMode}}
|
||||
<script>
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, {{ retryValue.TotalMilliseconds }});
|
||||
</script>
|
||||
{{/IF}}
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user