Skip to content

Keep display preferences per-browser, and remember each user's view - #2332

Closed
cyrus104 wants to merge 3 commits into
lissy93:masterfrom
cyrus104:upstream/per-user-display-prefs
Closed

cyrus104 wants to merge 3 commits into
lissy93:masterfrom
cyrus104:upstream/per-user-display-prefs

Conversation

@cyrus104

@cyrus104 cyrus104 commented Sep 4, 2026

Copy link
Copy Markdown

Problem

Dashy holds two copies of the config in the Vuex store:

  • state.config — what's rendered right now
  • state.configSource — what gets written back to conf.yml, and is therefore shared by every user of the instance

The quick-pickers for theme, layout and item size wrote to both, plus localStorage. Nothing looks wrong until someone saves: Save to Disk, from the config editor or the "You're using a local config" banner, serialises configSource — which by then carries whatever theme, layout and item size that user happened to be viewing with. Those land in the shared conf.yml. On success carefullyClearLocalStorage() drops their local override, so the value is now purely file-backed, and every other user (having no override of their own) falls back to the file and inherits it.

The result: on a shared instance, one person changing their theme and saving any unrelated config edit silently changes the theme for everybody.

Separately, the view (default / minimal / workspace) had no per-browser persistence at all. theme, layout, iconSize and language are all remembered per browser via configScope() / readLocalOverrides(), but the view is resolved by the router's beforeEnter guard reading appConfig.startingView directly, before that local-override layer is ever consulted. So users sharing an instance could each keep their own look, but not their own view.

Changes

Two commits, reviewable independently.

1. Keep personal display preferences out of the shared configsrc/store.js, +8/-3

patchAppConfigField already receives a storageKey, and it is passed only by the personal quick-pickers (SET_THEME, SET_ITEM_LAYOUT, SET_ITEM_SIZE, SET_LANGUAGE); authored edits such as UPDATE_CUSTOM_CSS pass nothing. That existing argument becomes the discriminator — when present, update state.config and localStorage, then return without touching configSource:

state.config = { ...state.config, appConfig: { ...state.config.appConfig, [key]: value } };
if (storageKey) {
  localStorage.setItem(storageKey, value);
  return;
}
state.configSource = { ... };

Rendering is unchanged — the getters read state.config. Authoring an instance-wide default from the interactive editor is unchanged too, since theme, layout and iconSize getters already bypass localStorage while state.editMode is on.

2. Remember the view each user picks, per browser — +49/-17

  • defaults.js: new localStorageKeys.STARTING_VIEW ('startingView')
  • ConfigHelpers.js: resolveStartingView(stored, configured) and rememberStartingView(view), next to the existing VIEW_META; clearScopedLocalConfig() removes the key
  • router.js: the landing guard consults the stored pick first, then appConfig.startingView, then home
  • OptionsPanel.vue and ViewSwitcher.vue: both view switchers record an explicit pick

Only an explicit pick from a switcher is stored, so following a link or bookmark to /minimal doesn't quietly rewrite someone's landing view. An unrecognised stored value falls through to the configured value rather than breaking routing, and Config → Reset Local Settings clears it alongside the existing theme and layout overrides.

Behaviour

Before After
Theme / layout / item size Per browser, until someone saves to disk — then instance-wide Per browser, always
View Instance-wide only Per browser, falling back to appConfig.startingView
conf.yml on disk Absorbs whoever saved last Only ever the authored config

Compatibility

No config format change. Existing conf.yml files work untouched, appConfig.startingView still sets the default for anyone who hasn't picked a view, and the legacy default alias for home is still accepted. The new localStorage key is additive, and its absence is the pre-existing behaviour.

Testing

Verified manually against a running instance, in two browser profiles:

  • A user's theme survives a reload, and is not visible in a second profile
  • With a theme picked, Save to Disk writes conf.yml with the authored theme intact — the viewing user's choice does not appear in the file
  • A fresh visitor lands on appConfig.startingView; after picking Minimal, / lands on /minimal for that browser only, while a second profile still lands on the configured view
  • Reset Local Settings returns both the theme and the view to the config's values

Each of the two behaviours was also confirmed to regress when its change is reverted, so the checks above distinguish the fix from the prior behaviour rather than passing either way.

Existing suite: 536 tests pass, lint clean.

Picking a theme, layout or item size wrote the value into `configSource`
as well as localStorage. `configSource` is what gets written back to
conf.yml, so the next save to disk - from the config editor, or the
"Save Changes to Disk" button on the local config banner - pushed one
user's personal choice out as the default for everybody else.

`patchAppConfigField` already takes a `storageKey`, passed only by the
quick-pickers (theme, layout, item size, language) and omitted by
authored edits such as custom CSS. Use it as the discriminator: with a
key, update the rendered config and localStorage and stop there.
The view (default/minimal/workspace) had no per-browser persistence: it
was resolved by a router guard reading appConfig.startingView from the
shared config, before the local-override layer is consulted. Theme,
layout and item size already persist per browser, so people sharing an
instance could each keep their own look but not their own view.

An explicit pick from either view switcher is now stored under a
`startingView` localStorage key and wins over appConfig.startingView.
Only an explicit pick is stored, so following a link to /minimal doesn't
quietly rewrite someone's landing view, and an unrecognised stored value
falls back rather than breaking routing. Reset Local Settings clears it
alongside the existing theme and layout overrides.
@cyrus104
cyrus104 requested a review from lissy93 as a code owner September 4, 2026 00:19
@netlify

netlify Bot commented Sep 4, 2026

Copy link
Copy Markdown

Deploy Preview for dashy-dev ready!

Name Link
🔨 Latest commit be16203
🔍 Latest deploy log https://app.netlify.com/projects/dashy-dev/deploys/6a9a10303f0d3e0008321802
😎 Deploy Preview https://deploy-preview-2332--dashy-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

`patchAppConfigField` can't be reached from a test while it lives in
store.js: importing the store pulls ConfigHelpers, whose self-executing
`config` block constructs a ConfigAccumulator that reads `$store.state`
before the store module has finished initialising. Move the function to
its own module so it can be imported directly, and cover both branches -
a personal preference stays out of configSource, an authored edit still
reaches it.

Also covers resolveStartingView and rememberStartingView: the stored pick
winning over the configured one, the legacy 'default' alias, and an
unrecognised stored value falling back rather than breaking routing.
@cyrus104

cyrus104 commented Sep 4, 2026

Copy link
Copy Markdown
Author

I'm in a fight with my coworkers over the theme, layout, and compactness. Even though I set it up, and imported all of our internal services into the dashboard they love to make all the graphical changes that annoy me, I would mind if they didn't change it every 20 minutes to mess with each other too. I do have it locked down so only I can make config changes.

The changes are as simple as I could make them.

@lissy93

lissy93 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Hmm, so if you've disabled config, then it should not override the theme/layout when someone makes a change locally, already.

Edit: Ah you're just talking about remembering the starting view, right?

@cyrus104

cyrus104 commented Sep 4, 2026 via email

Copy link
Copy Markdown
Author

@lissy93

lissy93 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Yeah, I'm just trying to understand what this PR does, and why it's needed. As it's not super clear to me from the code or description.

@cyrus104

cyrus104 commented Sep 4, 2026 via email

Copy link
Copy Markdown
Author

@lissy93

lissy93 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

The expected behaviour is:

  • Themes and layout changes remembered locally in your browser. So that each user can set their own.
  • If you then go and edit config from browser, these will get carried over
  • Users who aren't admins, or don't have edit access cannot change any UI stuff for any other users. Just their own, locally.

@cyrus104

cyrus104 commented Sep 4, 2026 via email

Copy link
Copy Markdown
Author

@lissy93

lissy93 commented Sep 4, 2026

Copy link
Copy Markdown
Owner

I understand what you mean now. Yeah, it's a bit of a design flaw 🫤...

I wanted users to be able to change visual aspects of their dashboard and make local changes, even when they don't have admin write access.

But then if an admin spends ages tweaking their design, before then going into edit and clicking "Save to Disk", then I think they'd expect their changes to get saved centrally.

I'm not really sure what the solution is 🤔
It's not quite as simple as just adding a checkbox, as it would change how the store and state system works.
And I don't want to just ask AI to solve it, because it'll write 500+ lines of code I won't be able to maintain into the future 😅 I want to keep things clean and readable, so as to not introduce bugs.

@cyrus104

cyrus104 commented Sep 4, 2026 via email

Copy link
Copy Markdown
Author

@lissy93 lissy93 closed this Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants