mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 10:30:57 +01:00
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.
458 lines
19 KiB
HTML
458 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="color-scheme" content="dark" />
|
|
<title>
|
|
{{#IF isInReportingMode}}
|
|
❌
|
|
{{/IF}}
|
|
Jellyfin Startup
|
|
</title>
|
|
<style>
|
|
:root {
|
|
--jf-bg: #101010;
|
|
--jf-bg-accent: #181818;
|
|
--jf-surface: #202020;
|
|
--jf-border: rgba(255, 255, 255, 0.09);
|
|
--jf-text: #ffffff;
|
|
--jf-text-muted: rgba(255, 255, 255, 0.7);
|
|
--jf-text-dim: rgba(255, 255, 255, 0.45);
|
|
--jf-accent-start: #aa5cc3;
|
|
--jf-accent-end: #00a4dc;
|
|
--jf-accent: #00a4dc;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
font-family: "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
min-height: 100%;
|
|
}
|
|
|
|
body {
|
|
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);
|
|
/* Bottom padding doubles as the minimum gap the log viewport keeps from the window bottom. */
|
|
padding: 2.5rem 1rem 50px;
|
|
}
|
|
|
|
.flex-row {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: nowrap;
|
|
justify-content: center;
|
|
align-items: center;
|
|
align-content: normal;
|
|
}
|
|
|
|
.flex-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-wrap: nowrap;
|
|
justify-content: center;
|
|
align-items: center;
|
|
align-content: normal;
|
|
}
|
|
|
|
.container {
|
|
max-width: 52rem;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
header {
|
|
width: 100%;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
header .logo {
|
|
height: 3.25rem;
|
|
width: auto;
|
|
margin-bottom: 1.75rem;
|
|
}
|
|
|
|
.status-card {
|
|
width: 100%;
|
|
background-color: var(--jf-surface);
|
|
border: 1px solid var(--jf-border);
|
|
border-radius: 0.6rem;
|
|
padding: 1.25rem 1.5rem;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.status-card .status-text {
|
|
margin: 0;
|
|
font-size: 1.05rem;
|
|
font-weight: 500;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.status-card.is-error {
|
|
border-color: rgba(229, 72, 77, 0.5);
|
|
background-color: rgba(229, 72, 77, 0.08);
|
|
}
|
|
|
|
.spinner {
|
|
flex: 0 0 auto;
|
|
width: 1.4rem;
|
|
height: 1.4rem;
|
|
border-radius: 50%;
|
|
border: 3px solid rgba(255, 255, 255, 0.14);
|
|
border-top-color: var(--jf-accent);
|
|
animation: spin 0.9s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.error-mark {
|
|
flex: 0 0 auto;
|
|
font-size: 1.4rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.logs-panel {
|
|
width: 100%;
|
|
margin-top: 2rem;
|
|
background-color: var(--jf-surface);
|
|
border: 1px solid var(--jf-border);
|
|
border-radius: 0.6rem;
|
|
padding: 1rem 1.5rem 1.25rem;
|
|
}
|
|
|
|
.logs-panel .logs-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
margin: 0.25rem 0 0.75rem;
|
|
}
|
|
|
|
.logs-panel h2 {
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--jf-text-dim);
|
|
margin: 0;
|
|
}
|
|
|
|
.logs-panel .download-logs {
|
|
flex: 0 0 auto;
|
|
color: var(--jf-accent);
|
|
text-decoration: none;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.logs-panel .download-logs:hover {
|
|
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;
|
|
font-size: clamp(13px, 100vw / var(--width), 15px);
|
|
font-feature-settings: 'onum', 'pnum';
|
|
line-height: 1.9;
|
|
-webkit-text-size-adjust: none;
|
|
}
|
|
|
|
/* Attribution as heavily inspired by: https://iamkate.com/code/tree-views/ */
|
|
.action-list {
|
|
--spacing: 1.4rem;
|
|
--radius: 12px;
|
|
margin: 0;
|
|
padding-left: 0.5rem;
|
|
width: 100%;
|
|
}
|
|
|
|
.action-list li {
|
|
display: block;
|
|
position: relative;
|
|
padding-left: calc(2 * var(--spacing) - var(--radius) - 1px);
|
|
}
|
|
|
|
.action-list ul {
|
|
margin-left: calc(var(--radius) - var(--spacing));
|
|
padding-left: 0;
|
|
}
|
|
|
|
.action-list ul li {
|
|
border-left: 2px solid var(--jf-border);
|
|
}
|
|
|
|
.action-list ul li:last-child {
|
|
border-color: transparent;
|
|
}
|
|
|
|
.action-list ul li::before {
|
|
content: '';
|
|
display: block;
|
|
position: absolute;
|
|
top: calc(var(--spacing) / -2);
|
|
left: -2px;
|
|
width: calc(var(--spacing) + 2px);
|
|
height: calc(var(--spacing) + 1px);
|
|
border: solid var(--jf-border);
|
|
border-width: 0 0 2px 2px;
|
|
}
|
|
|
|
.action-list summary {
|
|
display: block;
|
|
cursor: pointer;
|
|
color: var(--jf-text);
|
|
}
|
|
|
|
.action-list summary::marker,
|
|
.action-list summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.action-list summary:focus {
|
|
outline: none;
|
|
}
|
|
|
|
.action-list summary:focus-visible {
|
|
outline: 1px dotted var(--jf-accent);
|
|
}
|
|
|
|
/* Status icon, placed in the left gutter and aligned to the first line of the entry. */
|
|
.action-list li::after {
|
|
display: block;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: calc(2 * var(--spacing) - var(--radius) - 1px - 0.5rem);
|
|
text-align: right;
|
|
line-height: 1.9;
|
|
font-size: 0.95em;
|
|
}
|
|
|
|
.action-list li.danger-item::after,
|
|
.action-list li.danger-strong-item::after {
|
|
content: '❌';
|
|
}
|
|
|
|
ol.action-list li span.danger-strong-item {
|
|
text-decoration-style: solid;
|
|
text-decoration-color: #e5484d;
|
|
text-decoration-line: underline;
|
|
}
|
|
|
|
ol.action-list li.warn-item::after {
|
|
content: '⚠️';
|
|
}
|
|
|
|
ol.action-list li.success-item::after {
|
|
content: '✅';
|
|
}
|
|
|
|
ol.action-list li.info-item::after {
|
|
content: '🔹';
|
|
}
|
|
|
|
/* End Attribution */
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="container">
|
|
<header class="flex-col">
|
|
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 251 72" fill="none" role="img" aria-label="Jellyfin">
|
|
<g clip-path="url(#a)">
|
|
<path fill="url(#b)" d="M24.212 49.158C22.66 46.042 32.838 27.588 36 27.588c3.167.002 13.323 18.488 11.788 21.57-1.534 3.082-22.025 3.116-23.576 0" />
|
|
<path fill="url(#c)" fill-rule="evenodd" d="M.482 64.995C-4.195 55.605 26.477 0 36 0c9.533 0 40.153 55.713 35.527 64.995s-66.368 9.39-71.045 0m12.254-8.148c3.064 6.152 43.518 6.084 46.548 0 3.03-6.086-17.032-42.586-23.275-42.586S9.671 50.694 12.736 56.847" clip-rule="evenodd" />
|
|
<path fill="#fff" d="M225.22 56c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.219-.218c-.054-.107-.054-.247-.054-.527V26.8c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.183c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v2.895a7.9 7.9 0 0 1 3.419-3.254q2.261-1.103 5.074-1.103 3.308 0 5.845 1.434a10.1 10.1 0 0 1 4.026 4.026q1.434 2.536 1.434 5.9V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.218c-.107.055-.247.055-.527.055h-5.625c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527V38.408q0-2.978-1.709-4.688-1.654-1.764-4.357-1.764-2.702 0-4.412 1.764-1.654 1.766-1.654 4.688V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm-11.54-33.363c-.28 0-.42 0-.527-.055a.5.5 0 0 1-.218-.218c-.055-.107-.055-.247-.055-.527v-6.121c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527v6.12c0 .28 0 .42-.054.528a.5.5 0 0 1-.219.218c-.107.055-.247.055-.527.055zm0 33.363c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.219c-.055-.107-.055-.247-.055-.527V26.8c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h5.624c.28 0 .42 0 .527.055a.5.5 0 0 1 .219.218c.054.107.054.247.054.527v28.4c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-16.712-.054c.107.054.247.054.527.054h5.625c.28 0 .42 0 .526-.054a.5.5 0 0 0 .219-.219c.055-.107.055-.247.055-.527V32.452h5.872c.28 0 .42 0 .527-.054a.5.5 0 0 0 .219-.219c.054-.107.054-.247.054-.527V26.8c0-.28 0-.42-.054-.527a.5.5 0 0 0-.219-.218c-.107-.055-.247-.055-.527-.055h-5.872v-.992q0-2.261 1.323-3.31 1.379-1.102 3.75-1.102.454 0 .939.044c.345.031.518.047.634-.004a.48.48 0 0 0 .241-.22c.061-.111.061-.274.061-.6V15.39c0-.304 0-.457-.061-.589a.7.7 0 0 0-.248-.284c-.122-.078-.261-.097-.537-.136a14.5 14.5 0 0 0-1.966-.126q-5.184 0-8.273 2.812t-3.088 7.942V26H186.53c-.3 0-.451 0-.58.05a.75.75 0 0 0-.296.205c-.091.104-.143.244-.248.526l-7.43 19.9-7.483-19.903c-.105-.28-.158-.42-.249-.524a.75.75 0 0 0-.296-.205c-.129-.049-.279-.049-.578-.049h-5.769c-.394 0-.591 0-.717.083a.5.5 0 0 0-.213.314c-.031.147.041.33.186.697L174.281 56l-.661 1.6q-.883 1.874-2.041 3.033-1.103 1.158-3.584 1.158-.883 0-1.875-.166a13 13 0 0 1-.73-.1c-.389-.066-.584-.099-.709-.053a.47.47 0 0 0-.26.22c-.066.116-.066.298-.066.663v4.329c0 .243 0 .365.045.481a.7.7 0 0 0 .189.266c.095.081.194.116.392.185q.684.24 1.47.351 1.158.22 2.371.22 4.246 0 7.059-2.426 2.867-2.37 4.577-6.728l10.517-26.58h5.72V55.2c0 .28 0 .42.055.527a.5.5 0 0 0 .218.219M154.363 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527V55.2c0 .28 0 .42-.055.527a.5.5 0 0 1-.218.219c-.107.054-.247.054-.527.054zm-11.621 0c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.219-.219c-.054-.107-.054-.247-.054-.527V15.054c0-.28 0-.42.054-.527a.5.5 0 0 1 .219-.219c.107-.054.247-.054.527-.054h5.624c.28 0 .42 0 .527.054a.5.5 0 0 1 .219.219c.054.107.054.247.054.527V55.2c0 .28 0 .42-.054.527a.5.5 0 0 1-.219.219c-.107.054-.247.054-.527.054zm-18.132.662q-4.632-.001-8.107-2.096a14.6 14.6 0 0 1-5.404-5.68q-1.93-3.585-1.93-7.942 0-4.522 1.93-7.996 1.985-3.53 5.349-5.57 3.42-2.04 7.61-2.04 4.688 0 7.942 2.04 3.253 1.986 4.963 5.294 1.71 3.309 1.709 7.335 0 .828-.11 1.654-.031.45-.12.841c-.037.165-.055.247-.115.33a.55.55 0 0 1-.208.168c-.095.04-.194.04-.393.04h-21.057q.33 3.309 2.537 5.294 2.205 1.986 5.459 1.985 2.482 0 4.191-1.047a8.2 8.2 0 0 0 2.206-1.986c.241-.316.362-.474.484-.542a.6.6 0 0 1 .352-.083c.139.006.296.083.608.236l4.269 2.094c.239.118.359.176.431.275a.52.52 0 0 1 .098.298c0 .122-.058.231-.172.45q-1.432 2.742-4.526 4.607-3.419 2.04-7.996 2.04m-.552-25.368q-2.702 0-4.687 1.654-1.93 1.6-2.537 4.577h14.118q-.22-2.757-2.151-4.466-1.875-1.765-4.743-1.765M90.801 56c-.28 0-.42 0-.527-.054a.5.5 0 0 1-.218-.218C90 55.62 90 55.48 90 55.2v-5.294c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.218c.107-.055.247-.055.527-.055h1.572q2.646 0 4.19-1.489 1.6-1.545 1.6-4.08V15.715c0-.28 0-.42.055-.527a.5.5 0 0 1 .218-.219c.107-.054.247-.054.527-.054h5.956c.28 0 .42 0 .527.054a.5.5 0 0 1 .218.219c.055.107.055.247.055.527v27.546q0 3.804-1.655 6.672-1.599 2.868-4.632 4.467-2.979 1.6-7.06 1.6z" />
|
|
</g>
|
|
<defs>
|
|
<linearGradient id="b" x1="12" x2="71.999" y1="30.001" y2="63.002" gradientUnits="userSpaceOnUse">
|
|
<stop stop-color="#aa5cc3" />
|
|
<stop offset="1" stop-color="#00a4dc" />
|
|
</linearGradient>
|
|
<linearGradient id="c" x1="12" x2="71.999" y1="29.999" y2="63.001" gradientUnits="userSpaceOnUse">
|
|
<stop stop-color="#aa5cc3" />
|
|
<stop offset="1" stop-color="#00a4dc" />
|
|
</linearGradient>
|
|
<clipPath id="a">
|
|
<path fill="#fff" d="M0 0h251v72H0z" />
|
|
</clipPath>
|
|
</defs>
|
|
</svg>
|
|
|
|
{{^IF isInReportingMode}}
|
|
<div class="status-card">
|
|
<div class="spinner" aria-hidden="true"></div>
|
|
<p class="status-text">Jellyfin Server {{version.ToString(2)}} is still starting. Please wait… {{currentActivity}}</p>
|
|
</div>
|
|
{{#ELSE}}
|
|
<div class="status-card is-error">
|
|
<span class="error-mark" aria-hidden="true">❌</span>
|
|
<p class="status-text">Jellyfin Server {{version.ToString(2)}} has encountered an error and was not able to start.</p>
|
|
</div>
|
|
{{/ELSE}}
|
|
{{/IF}}
|
|
</header>
|
|
|
|
{{#DECLARE LogEntry |--}}
|
|
{{#LET children = Children}}
|
|
<li class="{{FormatLogLevel(children).ToString()}}-item">
|
|
{{--| #IF children.Count > 0}}
|
|
<details open>
|
|
<summary>{{DateOfCreation}} - {{Content}}</summary>
|
|
<ul class="action-list">
|
|
{{--| #EACH children |-}}
|
|
{{#IMPORT 'LogEntry'}}
|
|
{{--| /EACH |-}}
|
|
</ul>
|
|
</details>
|
|
{{--| #ELSE |-}}
|
|
<span class="{{FormatLogLevel(children).ToString()}}-item">{{DateOfCreation}} - {{Content}}</span>
|
|
{{--| /ELSE |--}}
|
|
{{--| /IF |-}}
|
|
</li>
|
|
{{--| /DECLARE}}
|
|
|
|
{{#IF localNetworkRequest}}
|
|
<div class="logs-panel">
|
|
<div class="logs-header">
|
|
<h2>Startup log</h2>
|
|
<a class="download-logs" href='/startup/logger' target="_blank">Download full logs</a>
|
|
</div>
|
|
<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>
|
|
|
|
</html>
|