Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7f13cadc | ||
|
|
ed3806f998 |
@@ -254,6 +254,20 @@ write**, orphaning a pty per reload; Running Shells has **404'd since 2026-07-31
|
|||||||
error boundaries** anywhere in the repo; and `dashboards.id` is a **global** primary key fed by
|
error boundaries** anywhere in the repo; and `dashboards.id` is a **global** primary key fed by
|
||||||
`slugify(name)`, so two members naming a dashboard the same thing collide.
|
`slugify(name)`, so two members naming a dashboard the same thing collide.
|
||||||
|
|
||||||
|
- [ ] **Renaming a panel key is a silent data migration, and the repair is destructive.** A saved layout
|
||||||
|
holds `appType` strings. `normalizeLayout` replaces any appType not in the allow-list with the
|
||||||
|
caller's `fallback`, and `WorkspaceView.tsx:97` writes that back — so a renamed key turns every
|
||||||
|
saved layout into N copies of whichever panel is `allowed[0]`, permanently, on one page load.
|
||||||
|
Renaming `headscale-*` → `offscale-*` did exactly this: `/offscale` rendered three server pickers,
|
||||||
|
`ServersView` never mounted, and it survived every restart because the code was correct and the
|
||||||
|
data was not (2026-08-17, `ed3806f9`).
|
||||||
|
|
||||||
|
Plugin screens are now cleared at install, which covers plugins and nothing else. The general fix
|
||||||
|
is a rule about wholesale mismatch: **a layout in which NO panel has a recognised appType is
|
||||||
|
residue from older code — discard it and use the screen's default**, rather than collapsing it.
|
||||||
|
`fallback: allowed[0]` is arbitrary (it is whatever `panels.ts` happens to list first) and should
|
||||||
|
probably not be a per-panel substitution at all.
|
||||||
|
|
||||||
## Code editor
|
## Code editor
|
||||||
|
|
||||||
- [ ] **The owner wants to go deeper here — ideas pending (noted 2026-08-15).** Raised right after the file
|
- [ ] **The owner wants to go deeper here — ideas pending (noted 2026-08-15).** Raised right after the file
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
setDashboardPanelState,
|
setDashboardPanelState,
|
||||||
upsertScreen,
|
upsertScreen,
|
||||||
deleteScreen,
|
deleteScreen,
|
||||||
|
deleteScreenForAllUsers,
|
||||||
upsertDefaults,
|
upsertDefaults,
|
||||||
setDefaultsPanelState,
|
setDefaultsPanelState,
|
||||||
} from './queries';
|
} 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)));
|
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 ──
|
// ── Defaults ──
|
||||||
|
|
||||||
type UpsertDefaultsData = {
|
type UpsertDefaultsData = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
deleteScreenForAllUsers,
|
||||||
getDockPaths,
|
getDockPaths,
|
||||||
getPluginInstall,
|
getPluginInstall,
|
||||||
recordPluginInstall,
|
recordPluginInstall,
|
||||||
@@ -272,6 +273,23 @@ export async function installPlugin(
|
|||||||
await recordPluginInstall(appName, plugin.manifest.version);
|
await recordPluginInstall(appName, plugin.manifest.version);
|
||||||
await step(steps, onStep, `recorded at ${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 —
|
// 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
|
// `<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
|
// obvious: the `/plugins/*` route is dynamic precisely so a first install does not show a broken
|
||||||
|
|||||||
Reference in New Issue
Block a user