mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 10:30:57 +01:00
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 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 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>
|