aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2026-06-21 23:09:55 -0400
committerJoshua M. Boniface <joshua@boniface.me>2026-06-22 00:00:38 -0400
commit0046adda29b4d99cbdf6b215d14539c08e96ab3e (patch)
treec8f05e5e68f6b1aa563bfa8f03a4451177f59c37
parent4e80648fd31e914be455525c39c6cacaeb8f4b67 (diff)
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.
-rw-r--r--Jellyfin.Server/Migrations/JellyfinMigrationService.cs3
-rw-r--r--Jellyfin.Server/Program.cs6
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs87
-rw-r--r--Jellyfin.Server/ServerSetupApp/StartupActivity.cs44
-rw-r--r--Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs109
-rw-r--r--Jellyfin.Server/ServerSetupApp/index.mstemplate.html244
6 files changed, 366 insertions, 127 deletions
diff --git a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
index 9bf927bb95..a10be76e05 100644
--- a/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
+++ b/Jellyfin.Server/Migrations/JellyfinMigrationService.cs
@@ -215,8 +215,11 @@ internal class JellyfinMigrationService
logger.LogInformation("There are {Pending} migrations for stage {Stage}.", pendingCodeMigrations.Length, stage);
migrations = pendingMigrations.OrderBy(e => e.Key).ToArray();
+ var migrationIndex = 0;
foreach (var item in migrations)
{
+ // Surface generic "Running migration X of Y" progress in the always-visible startup UI header.
+ SetupServer.ReportActivity(StartupActivity.Migration(++migrationIndex, migrations.Length));
var migrationLogger = logger.With(_loggerFactory.CreateLogger(item.Migration.GetType().Name)).BeginGroup($"{item.Key}");
try
{
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index af0d424aad..2b20ee4314 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -133,10 +133,12 @@ namespace Jellyfin.Server
}
}
+ SetupServer.ReportActivity(StartupActivity.CheckingStorage);
StorageHelper.TestCommonPathsForStorageCapacity(appPaths, StartupLogger.Logger.With(_loggerFactory.CreateLogger<Startup>()).BeginGroup($"Storage Check"));
StartupHelpers.PerformStaticInitialization();
+ SetupServer.ReportActivity(StartupActivity.Initializing);
await ApplyStartupMigrationAsync(appPaths, startupConfig, options).ConfigureAwait(false);
do
@@ -195,6 +197,7 @@ namespace Jellyfin.Server
if (!string.IsNullOrWhiteSpace(_restoreFromBackup))
{
+ SetupServer.ReportActivity(StartupActivity.RestoringBackup);
await appHost.ServiceProvider.GetService<IBackupService>()!.RestoreBackupAsync(_restoreFromBackup).ConfigureAwait(false);
_restoreFromBackup = null;
_restartOnShutdown = true;
@@ -202,9 +205,12 @@ namespace Jellyfin.Server
}
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(appHost.ServiceProvider);
+ SetupServer.ReportActivity(StartupActivity.PreparingMigrations);
await jellyfinMigrationService.PrepareSystemForMigration(_logger).ConfigureAwait(false);
+ SetupServer.ReportActivity(StartupActivity.ApplyingMigrations);
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
+ SetupServer.ReportActivity(StartupActivity.InitializingServices);
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
_appHost = appHost;
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 37bb1abe71..893272590e 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -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;
@@ -77,6 +74,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>
public bool IsAlive { get; internal set; }
@@ -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;
diff --git a/Jellyfin.Server/ServerSetupApp/StartupActivity.cs b/Jellyfin.Server/ServerSetupApp/StartupActivity.cs
new file mode 100644
index 0000000000..5baaf1d40a
--- /dev/null
+++ b/Jellyfin.Server/ServerSetupApp/StartupActivity.cs
@@ -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);
+}
diff --git a/Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs b/Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
new file mode 100644
index 0000000000..db07b9d8c1
--- /dev/null
+++ b/Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
@@ -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));
+ }
+}
diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
index 5706ce1fac..38cb5cea9e 100644
--- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
+++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
@@ -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;
+ }
- ol.action-list * {
- font-family: monospace;
- font-weight: 300;
- font-size: clamp(18px, 100vw / var(--width), 20px);
- font-feature-settings: 'onum', 'pnum';
- line-height: 1.8;
- -webkit-text-size-adjust: none;
+ .status-card .status-text {
+ margin: 0;
+ font-size: 1.05rem;
+ font-weight: 500;
+ line-height: 1.5;
}
- /*
- ol.action-list li {
- padding-top: .5rem;
+ .status-card.is-error {
+ border-color: rgba(229, 72, 77, 0.5);
+ background-color: rgba(229, 72, 77, 0.08);
}
- ol.action-list li::before {
- position: absolute;
- left: -0.8em;
- font-size: 1.1em;
- } */
+ .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: 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: 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>