Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions ui/js/dashboard-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
13 changes: 11 additions & 2 deletions ui/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
};

Expand All @@ -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';
Expand Down
27 changes: 26 additions & 1 deletion ui/test/dashboard-logic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)}`);
}
});
Loading