installing a plugin resets its saved layout

a layout stores appType STRINGS. rename a panel key between versions and every saved layout still names
the old one; normalizeLayout swaps each unknown appType for allowed[0] — whichever panel the plugin lists
first — and WorkspaceView persists the repair.

so renaming headscale-* to offscale-* rewrote the owner's layout into three copies of the server picker
and saved it. ServersView never mounted, there was no register form on the page at all, and it survived a
server restart, a sidecar restart and a plugin reinstall because nothing on disk was wrong. found by
counting elements in a real browser: 3 links, 0 buttons.

install is the only place that knows a plugin's panels may have just changed shape, so that is where the
stale row dies — for every user, since the damage is not per-user and install is owner-only. nothing is
lost: a plugin screen is locked, so the layout is the author's design rather than someone's arrangement,
and useDashboardState re-seeds from defaultLayout on the next load.

does NOT fix the underlying trap: renaming a panel key is still a silent data migration for any screen
that is not a plugin. see TODO.
This commit is contained in:
2026-08-17 13:24:27 +00:00
parent 2c11e42717
commit ed3806f998
3 changed files with 42 additions and 0 deletions
+18
View File
@@ -1,4 +1,5 @@
import {
deleteScreenForAllUsers,
getDockPaths,
getPluginInstall,
recordPluginInstall,
@@ -272,6 +273,23 @@ export async function installPlugin(
await recordPluginInstall(appName, plugin.manifest.version);
await step(steps, onStep, `recorded at ${plugin.manifest.version}`);
/*
Throw away every saved layout for this plugin's screen, for every user.
A layout holds `appType` STRINGS. Rename a panel key between two versions and every saved layout still
names the old one; `normalizeLayout` then swaps each unknown appType for `allowed[0]` — whichever panel
the plugin lists first — and `WorkspaceView` persists the repair. Renaming `headscale-*` to `offscale-*`
therefore rewrote the owner's layout into three copies of the picker and saved it, which survived a
server restart, a sidecar restart and a reinstall, because nothing on disk was wrong.
Install is the only place that knows a plugin's panels may have just changed shape, so it is where the
stale layout dies. Nothing is lost: a plugin screen is `locked`, so the layout is the author's design
rather than an arrangement someone made, and `useDashboardState` re-seeds from `defaultLayout` on the
next load.
*/
const clearedLayouts = await deleteScreenForAllUsers(`plugin/${appName}`);
if (clearedLayouts) await step(steps, onStep, `layout: reset for ${clearedLayouts} user(s)`);
// The dock tile's artwork, copied where the browser can fetch it. Reuses the app store's publisher —
// `<dir>/assets/` → `public/plugins/<appName>/` — because it already solves the parts that are not
// obvious: the `/plugins/*` route is dynamic precisely so a first install does not show a broken