diff --git a/CHANGELOG.md b/CHANGELOG.md index 189ce736..043c0ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Finishing first-run setup on an install with nothing on the dashboard now + opens Settings, where the first app or widget is added, instead of an empty + dashboard. + ### Fixed - Disk Health, System Stats, Backup, Now Playing and the two Connections views diff --git a/ui/js/dashboard-logic.js b/ui/js/dashboard-logic.js index e82cdd49..e55e6717 100644 --- a/ui/js/dashboard-logic.js +++ b/ui/js/dashboard-logic.js @@ -21,3 +21,19 @@ export function configChanged(loaded, fetched) { function fingerprint(c) { return JSON.stringify(c?.items?.map(i => `${i?.id}|${i?.label}|${i?.href}`)) + JSON.stringify(c?.settings); } + +/** Where to send the browser once first-run setup is finished. + + A fresh install has no items, and the dashboard is where items are shown, so + landing there shows an empty page with nothing to act on. Admin is where the + first item gets added, and is the page the user would have to open anyway. + + Only the automatic step after setup. Opening `/` afterwards is not blocked, + and an empty dashboard reached deliberately stays empty: there are no + placeholder items to delete. + + @param {unknown} items the dashboard items the page was built from + @returns {string|null} a path to navigate to, or null to stay put */ +export function landingAfterSetup(items) { + return Array.isArray(items) && items.length === 0 ? '/admin' : null; +} diff --git a/ui/js/dashboard.js b/ui/js/dashboard.js index fad851b1..dd214aec 100644 --- a/ui/js/dashboard.js +++ b/ui/js/dashboard.js @@ -29,7 +29,7 @@ import { pwStrength, passwordMismatch } from '/js/password-strength.js?v=dab9978 import { sanitizeItemLinks } from '/js/link-url.js?v=19038560'; import { initUI, mkFolder, openFolderDesktop, openFolderMobile, buildMobile } from '/js/ui.js?v=07fd9e2d'; import { computeBadgeVisual } from '/js/badge-logic.js?v=d278c683'; -import { configChanged } from '/js/dashboard-logic.js?v=640430ba'; +import { configChanged, landingAfterSetup } from '/js/dashboard-logic.js?v=640430ba'; import { trapFocus } from '/js/dialog.js?v=4ff94595'; const MOB = innerWidth <= 768 || /iPhone|iPod|Android/i.test(navigator.userAgent); @@ -487,6 +487,11 @@ function showSetupPrompt() { try { await fetch('/api/auth/dismiss-setup', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); } catch {} + const to = landingAfterSetup(items); + if (to) { + location.href = to; + return; + } close(); }; @@ -502,7 +507,11 @@ function showSetupPrompt() { body: JSON.stringify({ password: pw.value }), }); if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || t('setup.failed')); - location.reload(); + /* set-password issues the session cookie before it answers, so Admin + loads signed in rather than bouncing to the login page. */ + const to = landingAfterSetup(items); + if (to) location.href = to; + else location.reload(); } catch (e) { err.textContent = e.message; err.style.display = 'block'; diff --git a/ui/test/dashboard-logic.test.mjs b/ui/test/dashboard-logic.test.mjs index 8696b34a..3152e449 100644 --- a/ui/test/dashboard-logic.test.mjs +++ b/ui/test/dashboard-logic.test.mjs @@ -13,7 +13,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { configChanged } from '../js/dashboard-logic.js'; +import { configChanged, landingAfterSetup } from '../js/dashboard-logic.js'; const loaded = { _rev: 4, @@ -96,3 +96,28 @@ test('a missing loaded config does not throw', () => { assert.doesNotThrow(() => configChanged(null, withRev(5))); assert.doesNotThrow(() => configChanged(undefined, { items: [], settings: {} })); }); + +/* Where first-run setup leaves the browser. + + A fresh install is { items: [], settings: {} } and the setup prompt always + fires, so before this both paths closed onto a dashboard with nothing on it + and no hint of what to do. There are deliberately no placeholder items to + land on, so the answer is to send the user where items are added. */ + +test('a fresh install lands on Admin', () => { + assert.equal(landingAfterSetup([]), '/admin'); +}); + +test('an install that already has items stays where it is', () => { + assert.equal(landingAfterSetup([{ id: 'a' }]), null); + assert.equal(landingAfterSetup([{ id: 'a' }, { id: 'b' }]), null); +}); + +/* The caller passes whatever the config response held. A config that failed to + parse, or one whose items is the wrong shape, must not redirect: the empty + dashboard is the honest thing to show when the config is not understood. */ +test('anything that is not a list leaves the browser alone', () => { + for (const v of [undefined, null, 0, '', 'items', {}, { length: 0 }]) { + assert.equal(landingAfterSetup(v), null, `should not redirect on ${JSON.stringify(v)}`); + } +});