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:
@@ -6,6 +6,7 @@ export {
|
||||
setDashboardPanelState,
|
||||
upsertScreen,
|
||||
deleteScreen,
|
||||
deleteScreenForAllUsers,
|
||||
upsertDefaults,
|
||||
setDefaultsPanelState,
|
||||
} from './queries';
|
||||
|
||||
@@ -210,6 +210,29 @@ export async function deleteScreen(userId: number, name: string): Promise<void>
|
||||
await db.delete(screens).where(and(eq(screens.userId, userId), eq(screens.name, name)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget one screen's saved layout for EVERY user. Returns how many rows went.
|
||||
*
|
||||
* For plugin installs, and the reason is a bug that cost an evening. A layout stores `appType` STRINGS and
|
||||
* the registry resolves them at render; rename a panel key and every saved layout still names the old one.
|
||||
* `normalizeLayout` then replaces each unknown appType with the caller's fallback — `allowed[0]`, whichever
|
||||
* panel the plugin happens to list first — and `WorkspaceView` writes the repair back. So renaming
|
||||
* `headscale-*` to `offscale-*` silently rewrote the owner's layout into three copies of the first panel
|
||||
* and saved it. Restarting, reinstalling and rebuilding all changed nothing, because the code was right and
|
||||
* the data was wrong.
|
||||
*
|
||||
* Deleting is safe rather than merely convenient: a plugin screen is `locked`, so the layout is the
|
||||
* author's design and not a workspace anyone arranged. There is no user intent in the row to lose, and
|
||||
* `useDashboardState` re-seeds from the plugin's own `defaultLayout` on the next load.
|
||||
*
|
||||
* Not scoped to a user, because the damage is not either — install is owner-only and the rows belong to
|
||||
* everyone who ever opened the screen.
|
||||
*/
|
||||
export async function deleteScreenForAllUsers(name: string): Promise<number> {
|
||||
const rows = await db.delete(screens).where(eq(screens.name, name)).returning({ id: screens.id });
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
// ── Defaults ──
|
||||
|
||||
type UpsertDefaultsData = {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user