From 221850d1880302531a91a4684aebe46db26ecbc9 Mon Sep 17 00:00:00 2001 From: SandObserver <260779319+SandObserver@users.noreply.github.com> Date: Sun, 9 Aug 2026 16:12:37 -0400 Subject: [PATCH] Feature: land a fresh install on Settings instead of an empty dashboard A new install has no items, and the setup prompt fires on first load, so both Skip and Set returned the user to a dashboard with nothing on it and nothing to act on. Both paths now go to Settings when there is nothing to show, which is where the first app or widget is added. The dashboard itself stays genuinely empty: no placeholder items, and opening it directly afterwards is unchanged. The decision is a pure function so it can be tested without a DOM. --- CHANGELOG.md | 6 ++++++ ui/js/dashboard-logic.js | 16 ++++++++++++++++ ui/js/dashboard.js | 13 +++++++++++-- ui/test/dashboard-logic.test.mjs | 27 ++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) 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)}`); + } +});