fix: keep plugins loaded after signing in from the login screen

This commit is contained in:
LukeGus committed 2026-09-27 00:38:06 -05:00
1 parent 8043d6dbdd
commit 4423c4c8ef
3 files changed
+48 -12

No files matched your search

+1 -10
View File
@@ -134,10 +134,6 @@ function App() {
const [authUsername, setAuthUsername] = useState(stored?.username ?? "");
const [verifyRetryCount, setVerifyRetryCount] = useState(0);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Track whether fading-in came from a fresh login (vs. session verification on page load).
// When session-verified, Auth must not mount during the transition — it would trigger
// silent OIDC redirect and cause an infinite refresh loop.
const fadingInFromLoginRef = useRef(false);
// Dedupes concurrent handleLogout() calls within the same tick -- see
// handleLogout for why phase state alone isn't sufficient for this.
const loggingOutRef = useRef(false);
@@ -175,7 +171,6 @@ function App() {
// Non-fatal: WebSocket connections will fall back to cookie auth
}
}
fadingInFromLoginRef.current = false;
setPhase("loading-app");
await Promise.all([
startPluginRuntime()
@@ -241,7 +236,6 @@ function App() {
loggingOutRef.current = false;
clearDesktopManualLogout();
setAuthUsername(u);
fadingInFromLoginRef.current = true;
setPhase("loading-app");
void (async () => {
await Promise.all([
@@ -299,10 +293,7 @@ function App() {
const showApp =
phase === "idle-app" || phase === "fading-in" || phase === "fading-out";
const showAuth =
phase === "idle-auth" ||
(phase === "fading-in" && fadingInFromLoginRef.current) ||
phase === "fading-out";
const showAuth = phase === "idle-auth" || phase === "fading-out";
const appOpacity = phase === "idle-app" ? 1 : 0;
const authOpacity = phase === "idle-auth" ? 1 : 0;
+9 -2
View File
@@ -442,13 +442,20 @@ export async function startPreLoginPlugins(): Promise<void> {
} catch {
return;
}
if (list.length === 0) return;
if (list.length === 0 || started) return;
setPluginLocaleResolver((namespace, _language, file) => {
const summary = getPluginRecord(namespace)?.summary;
if (!summary) return Promise.resolve(null);
return currentDeps().loadLocale(summary, file);
});
await syncPlugins(list);
await enqueue(async () => {
// Once signed in the full sync owns the list, and reconciling this
// subset would unload everything else. Auth remounts during the
// post-login fade, so this runs after sign-in every time.
if (started) return;
await reconcile(list);
markPluginsSettled();
});
}
/**
+38
View File
@@ -7,6 +7,8 @@ import {
isPluginFrontendActive,
orderForActivation,
resetPluginLoader,
startPluginRuntime,
startPreLoginPlugins,
syncPlugins,
} from "@/plugin-host/loader";
import {
@@ -208,6 +210,42 @@ describe("plugin loader", () => {
expect(railIds()).not.toContain("headless-panel");
});
it("keeps every plugin when the pre-login list lands after sign-in", async () => {
modules.login = railPlugin("login");
modules.alpha = railPlugin("alpha");
plugins = [summary("login"), summary("alpha")];
let releasePreLogin: (value: Response) => void = () => {};
vi.spyOn(globalThis, "fetch").mockImplementation(
() =>
new Promise<Response>((resolve) => {
releasePreLogin = resolve;
}),
);
const preLogin = startPreLoginPlugins();
await startPluginRuntime();
releasePreLogin(new Response(JSON.stringify([summary("login")])));
await preLogin;
expect(isPluginFrontendActive("login")).toBe(true);
expect(isPluginFrontendActive("alpha")).toBe(true);
});
it("ignores a pre-login load started after sign-in", async () => {
modules.login = railPlugin("login");
modules.alpha = railPlugin("alpha");
plugins = [summary("login"), summary("alpha")];
vi.spyOn(globalThis, "fetch").mockImplementation(
async () => new Response(JSON.stringify([summary("login")])),
);
await startPluginRuntime();
await startPreLoginPlugins();
expect(isPluginFrontendActive("alpha")).toBe(true);
});
it("settles even when the plugin list cannot be fetched", async () => {
vi.spyOn(console, "error").mockImplementation(() => {});
configurePluginLoader({