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;