aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua M. Boniface <joshua@boniface.me>2026-06-25 00:42:31 -0400
committerJoshua M. Boniface <joshua@boniface.me>2026-06-25 00:42:31 -0400
commit31070e8208e973728b4bfe470cbbd6ca1d14c048 (patch)
tree20c82a7d1043a36747891dd91e35bd2f96de353a
parent2c98ad99db3b30f332a7a5a72c8d799207e952db (diff)
Add a cancelable redirect handoff and drop the transitional migration status
When the server finishes starting, show "Jellyfin started successfully" with a 5-second "Redirecting in N…" countdown and a Cancel button instead of reloading immediately. Cancel stops the countdown and the background refresh so the startup output can be reviewed, and offers a "Continue to Jellyfin" button to reload manually. The buttons use the web client's emby-button styling. Also drop the transitional "Applying migrations" activity: it only showed briefly while the pending migration set was read, or for the whole step when nothing was pending, so startup now goes from "Preparing migrations" straight into "Running migration X of Y".
-rw-r--r--Jellyfin.Server/Program.cs3
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs2
-rw-r--r--Jellyfin.Server/ServerSetupApp/StartupActivity.cs3
-rw-r--r--Jellyfin.Server/ServerSetupApp/index.mstemplate.html127
4 files changed, 127 insertions, 8 deletions
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 2b20ee4314..12f92efb35 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -207,7 +207,8 @@ namespace Jellyfin.Server
var jellyfinMigrationService = ActivatorUtilities.CreateInstance<JellyfinMigrationService>(appHost.ServiceProvider);
SetupServer.ReportActivity(StartupActivity.PreparingMigrations);
await jellyfinMigrationService.PrepareSystemForMigration(_logger).ConfigureAwait(false);
- SetupServer.ReportActivity(StartupActivity.ApplyingMigrations);
+ // "Preparing migrations" carries through the DB read; per-migration progress is reported
+ // as "Running migration X of Y" from inside the step once the pending set is known.
await jellyfinMigrationService.MigrateStepAsync(JellyfinMigrationStageTypes.CoreInitialisation, appHost.ServiceProvider).ConfigureAwait(false);
SetupServer.ReportActivity(StartupActivity.InitializingServices);
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
index 893272590e..598de5aa5f 100644
--- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -262,7 +262,7 @@ public sealed class SetupServer : IDisposable
/// 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>
+ /// <param name="activity">A generic description such as <see cref="StartupActivity.PreparingMigrations"/>.</param>
internal static void ReportActivity(string activity)
{
_currentActivity = activity;
diff --git a/Jellyfin.Server/ServerSetupApp/StartupActivity.cs b/Jellyfin.Server/ServerSetupApp/StartupActivity.cs
index 5baaf1d40a..888cc617d4 100644
--- a/Jellyfin.Server/ServerSetupApp/StartupActivity.cs
+++ b/Jellyfin.Server/ServerSetupApp/StartupActivity.cs
@@ -21,9 +21,6 @@ public static class StartupActivity
/// <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";
diff --git a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
index cc37a8b4dd..9c12762c31 100644
--- a/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
+++ b/Jellyfin.Server/ServerSetupApp/index.mstemplate.html
@@ -126,6 +126,63 @@
color: var(--jf-error);
}
+ /* Buttons — matching the web client's emby-button styles. */
+ .jf-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0.9em 1em;
+ border: 0;
+ border-radius: 0.2em;
+ font-family: inherit;
+ font-size: inherit;
+ font-weight: 600;
+ line-height: 1.35;
+ cursor: pointer;
+ outline: none;
+ text-decoration: none;
+ transition: 0.2s;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ }
+
+ .jf-button-primary {
+ background: var(--jf-primary);
+ color: rgba(0, 0, 0, 0.87);
+ }
+
+ .jf-button-primary:hover,
+ .jf-button-primary:focus {
+ background: var(--jf-primary-dark);
+ }
+
+ .jf-button-secondary {
+ background: #424242;
+ color: var(--jf-text-secondary);
+ }
+
+ .jf-button-secondary:hover,
+ .jf-button-secondary:focus {
+ background: #616161;
+ }
+
+ /* Redirect countdown shown once the server is ready. */
+ .redirect-bar {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 1em;
+ margin-bottom: 1.5em;
+ text-align: center;
+ }
+
+ .redirect-countdown {
+ color: var(--jf-text-secondary);
+ }
+
/* Material (MDL) spinner — the same one the web client uses while loading. */
.mdl-spinner {
position: relative;
@@ -491,8 +548,8 @@
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();
+ // The real server is now answering (HTTP 200) -> offer to continue to the app.
+ onServerReady();
return null;
}
return resp.text();
@@ -530,7 +587,71 @@
});
}
- setInterval(poll, intervalMs);
+ // The server finished starting. Stop polling and present a cancelable countdown so the
+ // user can either ride the redirect into the app or stay to review the startup output.
+ function onServerReady() {
+ clearInterval(pollTimer);
+
+ var status = document.querySelector('.status');
+ var statusText = document.querySelector('.status-text');
+ var spinner = document.querySelector('.mdl-spinner');
+ if (spinner) {
+ spinner.style.display = 'none';
+ }
+ if (status) {
+ status.classList.add('is-success');
+ }
+ if (statusText) {
+ statusText.textContent = 'Jellyfin started successfully.';
+ }
+ if (!status) {
+ window.location.reload();
+ return;
+ }
+
+ var bar = document.createElement('div');
+ bar.className = 'redirect-bar';
+ var countdownText = document.createElement('span');
+ countdownText.className = 'redirect-countdown';
+ var cancelButton = document.createElement('button');
+ cancelButton.type = 'button';
+ cancelButton.className = 'jf-button jf-button-secondary';
+ cancelButton.textContent = 'Cancel';
+ bar.appendChild(countdownText);
+ bar.appendChild(cancelButton);
+ status.insertAdjacentElement('afterend', bar);
+
+ var remaining = 5;
+ function renderCountdown() {
+ countdownText.textContent = 'Redirecting in ' + remaining + '…';
+ }
+ renderCountdown();
+ var countdown = setInterval(function () {
+ remaining -= 1;
+ if (remaining <= 0) {
+ clearInterval(countdown);
+ window.location.reload();
+ return;
+ }
+ renderCountdown();
+ }, 1000);
+
+ // Cancel stops both the redirect and the refreshing, and offers a manual continue.
+ cancelButton.addEventListener('click', function () {
+ clearInterval(countdown);
+ bar.innerHTML = '';
+ var continueButton = document.createElement('button');
+ continueButton.type = 'button';
+ continueButton.className = 'jf-button jf-button-primary';
+ continueButton.textContent = 'Continue to Jellyfin';
+ continueButton.addEventListener('click', function () {
+ window.location.reload();
+ });
+ bar.appendChild(continueButton);
+ });
+ }
+
+ var pollTimer = setInterval(poll, intervalMs);
})();
</script>
</body>