mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-06-28 10:30:57 +01:00
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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
44
Jellyfin.Server/ServerSetupApp/StartupActivity.cs
Normal file
44
Jellyfin.Server/ServerSetupApp/StartupActivity.cs
Normal 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);
|
||||
}
|
||||
109
Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
Normal file
109
Jellyfin.Server/ServerSetupApp/StartupUiRenderer.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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… {{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>
|
||||
|
||||
Reference in New Issue
Block a user