Restyle the startup UI and add a generic startup activity line

Restyle the startup/migration holding page to match the Jellyfin dark theme,
with the inline wordmark logo, a gradient spinner and a recolored startup log
tree, and move the Morestachio template rendering into a reusable
StartupUiRenderer.

Add a curated, non-identifying "current activity" line to the always-visible
header (for example "Initializing server" or "Running migration X of Y"),
reported from the startup flow and the migration service so it never leaks
server details to unauthenticated clients. Move the log download into a
"Download logs" link in the log panel header, and show only the header, with
no log hints, to non-local clients.
This commit is contained in:
Joshua M. Boniface
2026-06-21 23:09:55 -04:00
parent 4e80648fd3
commit 0046adda29
6 changed files with 366 additions and 127 deletions

View File

@@ -14,7 +14,6 @@ using Jellyfin.Server.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -25,9 +24,6 @@ using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Morestachio;
using Morestachio.Framework.IO.SingleStream;
using Morestachio.Rendering;
using Serilog;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@@ -44,7 +40,8 @@ public sealed class SetupServer : IDisposable
private readonly ILoggerFactory _loggerFactory;
private readonly IConfiguration _startupConfiguration;
private readonly ServerConfigurationManager _configurationManager;
private IRenderer? _startupUiRenderer;
private static volatile string _currentActivity = StartupActivity.Starting;
private StartupUiRenderer? _startupUiRenderer;
private IHost? _startupServer;
private bool _disposed;
private bool _isUnhealthy;
@@ -76,6 +73,12 @@ public sealed class SetupServer : IDisposable
internal static ConcurrentQueue<StartupLogTopic>? LogQueue { get; set; } = new();
/// <summary>
/// Gets a generic, non-identifying summary of what startup is currently doing. This is shown in the
/// always-visible header of the startup UI to unauthenticated clients, so it never contains server specific details.
/// </summary>
internal static string CurrentActivity => _currentActivity;
/// <summary>
/// Gets a value indicating whether Startup server is currently running.
/// </summary>
@@ -87,64 +90,9 @@ public sealed class SetupServer : IDisposable
/// <returns>A Task.</returns>
public async Task RunAsync()
{
var fileTemplate = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
_startupUiRenderer = (await ParserOptionsBuilder.New()
.WithTemplate(fileTemplate)
.WithFormatter(
(Version version, int arg) =>
{
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
return version.ToString(arg);
},
"ToString")
.WithFormatter(
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
{
if (children.Any())
{
var maxLevel = logEntry.LogLevel;
var stack = new Stack<StartupLogTopic>(children);
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
{
maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
foreach (var child in logEntry.Children)
{
stack.Push(child);
}
}
return maxLevel;
}
return logEntry.LogLevel;
},
"FormatLogLevel")
.WithFormatter(
(LogLevel logLevel) =>
{
switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
case LogLevel.None:
return "success";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "danger";
case LogLevel.Critical:
return "danger-strong";
}
return string.Empty;
},
"ToString")
.BuildAndParseAsync()
.ConfigureAwait(false))
.CreateCompiledRenderer();
ReportActivity(StartupActivity.Starting);
_startupUiRenderer = await StartupUiRenderer.CreateAsync(
Path.Combine(AppContext.BaseDirectory, "ServerSetupApp", "index.mstemplate.html")).ConfigureAwait(false);
ThrowIfDisposed();
var retryAfterValue = TimeSpan.FromSeconds(5);
@@ -257,13 +205,14 @@ public sealed class SetupServer : IDisposable
new Dictionary<string, object>()
{
{ "isInReportingMode", _isUnhealthy },
{ "currentActivity", CurrentActivity },
{ "retryValue", retryAfterValue },
{ "version", version },
{ "logs", startupLogEntries },
{ "networkManagerReady", networkManager is not null },
{ "localNetworkRequest", networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress) }
},
new ByteCounterStream(context.Response.BodyWriter.AsStream(), IODefaults.FileStreamBufferSize, true, _startupUiRenderer.ParserOptions))
context.Response.BodyWriter.AsStream())
.ConfigureAwait(false);
});
});
@@ -309,6 +258,16 @@ public sealed class SetupServer : IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
}
/// <summary>
/// Reports the current startup activity shown to all clients in the startup UI header.
/// Only pass generic, non-identifying text from <see cref="StartupActivity"/>.
/// </summary>
/// <param name="activity">A generic description such as <see cref="StartupActivity.ApplyingMigrations"/>.</param>
internal static void ReportActivity(string activity)
{
_currentActivity = activity;
}
internal void SoftStop()
{
_isUnhealthy = true;

View File

@@ -0,0 +1,44 @@
using System.Globalization;
namespace Jellyfin.Server.ServerSetupApp;
/// <summary>
/// A curated vocabulary of generic, non-identifying descriptions of what the server is doing during startup.
/// These are shown in the always-visible header of the startup UI to <b>unauthenticated</b> clients, so every
/// value must stay generic and must never contain server specific details (paths, names, plugin or migration ids, counts of items, etc.).
/// </summary>
public static class StartupActivity
{
/// <summary>The default state before any work has been reported.</summary>
public const string Starting = "Starting up";
/// <summary>Validating that the configured storage locations are usable.</summary>
public const string CheckingStorage = "Checking storage";
/// <summary>Bringing up the migration subsystem and running early startup checks.</summary>
public const string Initializing = "Initializing server";
/// <summary>Preparing the system for migrations (e.g. taking safety backups).</summary>
public const string PreparingMigrations = "Preparing migrations";
/// <summary>Applying database/system migrations without a known count.</summary>
public const string ApplyingMigrations = "Applying migrations";
/// <summary>Restoring from a backup.</summary>
public const string RestoringBackup = "Restoring backup";
/// <summary>Bringing up core services and plugins.</summary>
public const string InitializingServices = "Initializing services";
/// <summary>Running the final startup tasks.</summary>
public const string FinishingStartup = "Finishing startup";
/// <summary>
/// Builds a generic "Running migration X of Y" description. Only the numeric position and total are exposed.
/// </summary>
/// <param name="current">The 1-based index of the migration currently running.</param>
/// <param name="total">The total number of migrations in this batch.</param>
/// <returns>A generic progress description.</returns>
public static string Migration(int current, int total)
=> string.Format(CultureInfo.InvariantCulture, "Running migration {0} of {1}", current, total);
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using Morestachio;
using Morestachio.Framework.IO.SingleStream;
using Morestachio.Rendering;
namespace Jellyfin.Server.ServerSetupApp;
/// <summary>
/// Compiles and renders the startup UI Morestachio template.
/// Shared by the live <see cref="SetupServer"/> and the standalone startup UI preview tool so both
/// exercise the exact same template and formatters.
/// </summary>
public sealed class StartupUiRenderer
{
private readonly IRenderer _renderer;
private StartupUiRenderer(IRenderer renderer)
{
_renderer = renderer;
}
/// <summary>
/// Compiles the startup UI template located at <paramref name="templatePath"/>.
/// </summary>
/// <param name="templatePath">The full path to the <c>index.mstemplate.html</c> template.</param>
/// <returns>A ready to use <see cref="StartupUiRenderer"/>.</returns>
public static async Task<StartupUiRenderer> CreateAsync(string templatePath)
{
var fileTemplate = await File.ReadAllTextAsync(templatePath).ConfigureAwait(false);
var renderer = (await ParserOptionsBuilder.New()
.WithTemplate(fileTemplate)
.WithFormatter(
(Version version, int arg) =>
{
// version type does not for some stupid reason implement IFormattable which morestachio relies on for ToString support therefor we need to do it manually.
return version.ToString(arg);
},
"ToString")
.WithFormatter(
(StartupLogTopic logEntry, IEnumerable<StartupLogTopic> children) =>
{
if (children.Any())
{
var maxLevel = logEntry.LogLevel;
var stack = new Stack<StartupLogTopic>(children);
while (maxLevel != LogLevel.Error && stack.Count > 0 && (logEntry = stack.Pop()) is not null) // error is the highest inherted error level.
{
maxLevel = maxLevel < logEntry.LogLevel ? logEntry.LogLevel : maxLevel;
foreach (var child in logEntry.Children)
{
stack.Push(child);
}
}
return maxLevel;
}
return logEntry.LogLevel;
},
"FormatLogLevel")
.WithFormatter(
(LogLevel logLevel) =>
{
switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
case LogLevel.None:
return "success";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "danger";
case LogLevel.Critical:
return "danger-strong";
}
return string.Empty;
},
"ToString")
.BuildAndParseAsync()
.ConfigureAwait(false))
.CreateCompiledRenderer();
return new StartupUiRenderer(renderer);
}
/// <summary>
/// Renders the template with the provided model into the target stream.
/// </summary>
/// <param name="model">The values made available to the template.</param>
/// <param name="output">The stream the rendered HTML is written to.</param>
/// <returns>A Task.</returns>
public Task RenderAsync(IDictionary<string, object> model, Stream output)
{
return _renderer.RenderAsync(
model,
new ByteCounterStream(output, IODefaults.FileStreamBufferSize, true, _renderer.ParserOptions));
}
}

View File

@@ -1,8 +1,10 @@
<!DOCTYPE html>
<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}}
@@ -10,8 +12,36 @@
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;
}
* {
font-family: sans-serif;
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);
padding: 2.5rem 1rem 4rem;
}
.flex-row {
@@ -32,46 +62,122 @@
align-content: normal;
}
.container {
max-width: 52rem;
margin: 0 auto;
}
header {
height: 5rem;
width: 100%;
margin-bottom: 2rem;
}
header svg {
height: 3rem;
width: 9rem;
margin-right: 1rem;
header .logo {
height: 3.25rem;
width: auto;
margin-bottom: 1.75rem;
}
/* ol.action-list {
list-style-type: none;
position: relative;
} */
.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;
}
ol.action-list * {
font-family: monospace;
font-weight: 300;
font-size: clamp(18px, 100vw / var(--width), 20px);
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.8;
line-height: 1.9;
-webkit-text-size-adjust: none;
}
/*
ol.action-list li {
padding-top: .5rem;
}
ol.action-list li::before {
position: absolute;
left: -0.8em;
font-size: 1.1em;
} */
/* Attribution as heavily inspired by: https://iamkate.com/code/tree-views/ */
.action-list {
--spacing: 1.4rem;
--radius: 14px;
--radius: 12px;
margin: 0;
padding-left: 0.5rem;
width: 100%;
}
.action-list li {
@@ -86,7 +192,7 @@
}
.action-list ul li {
border-left: 2px solid #ddd;
border-left: 2px solid var(--jf-border);
}
.action-list ul li:last-child {
@@ -101,13 +207,14 @@
left: -2px;
width: calc(var(--spacing) + 2px);
height: calc(var(--spacing) + 1px);
border: solid #ddd;
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,
@@ -120,25 +227,19 @@
}
.action-list summary:focus-visible {
outline: 1px dotted #000;
outline: 1px dotted var(--jf-accent);
}
.action-list li::after,
.action-list summary::before {
content: '';
/* 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: calc(var(--spacing) / 2 - var(--radius) + 4px);
left: calc(var(--spacing) - var(--radius) - -5px);
}
.action-list summary::before {
z-index: 1;
/* background: #696 url('expand-collapse.svg') 0 0; */
}
.action-list details[open]>summary::before {
background-position: calc(-2 * var(--radius)) 0;
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,
@@ -148,7 +249,7 @@
ol.action-list li span.danger-strong-item {
text-decoration-style: solid;
text-decoration-color: red;
text-decoration-color: #e5484d;
text-decoration-line: underline;
}
@@ -169,20 +270,41 @@
</head>
<body>
<div>
<header class="flex-row">
<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}}
<p>Jellyfin Server {{version.ToString(2)}} still starting. Please wait.</p>
<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&hellip; {{currentActivity}}</p>
</div>
{{#ELSE}}
<p>Jellyfin Server {{version.ToString(2)}} has encountered an error and was not able to start.</p>
<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}}
{{#IF localNetworkRequest}}
<p style="margin-left: 1rem;">You can download the current log file <a href='/startup/logger'
target="_blank">here</a>.</p>
{{/IF}}
</header>
{{#DECLARE LogEntry |--}}
@@ -205,21 +327,17 @@
{{--| /DECLARE}}
{{#IF localNetworkRequest}}
<div class="flex-col">
<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>
<ol class="action-list">
{{#FOREACH log IN logs.Reverse()}}
{{#IMPORT 'LogEntry' #WITH log}}
{{/FOREACH}}
</ol>
</div>
{{#ELSE}}
{{#IF networkManagerReady}}
<p>Please visit this page from your local network to view detailed startup logs.</p>
{{#ELSE}}
<p>Initializing network settings. Please wait.</p>
{{/ELSE}}
{{/IF}}
{{/ELSE}}
{{/IF}}
</div>
</body>