Skip to content

Commit 4a73f28

Browse files
committed
fix(ui): stop hidden plugin tab polling
1 parent b41634b commit 4a73f28

2 files changed

Lines changed: 79 additions & 3 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import type { GatewayBrowserClient, GatewayHelloOk } from "../../api/gateway.ts";
3+
import type { RouteId } from "../../app-route-paths.ts";
4+
import type { ApplicationContext, ApplicationGatewaySnapshot } from "../../app/context.ts";
5+
import { getLogbookState } from "./logbook-controller.ts";
6+
import { PluginPage } from "./plugin-page.ts";
7+
8+
describe("PluginPage", () => {
9+
it("stops a bundled view when its advertised descriptor disappears", async () => {
10+
const hello: GatewayHelloOk = {
11+
type: "hello-ok",
12+
protocol: 3,
13+
auth: { role: "operator", scopes: ["operator.write"] },
14+
controlUiTabs: [{ pluginId: "logbook", id: "logbook", label: "Logbook" }],
15+
};
16+
const client = {
17+
request: vi.fn(async (method: string) => {
18+
if (method === "logbook.status") {
19+
return {
20+
captureEnabled: true,
21+
capturePaused: false,
22+
captureIntervalSeconds: 30,
23+
analysisIntervalMinutes: 15,
24+
retentionDays: 30,
25+
pendingFrames: 0,
26+
analysisRunning: false,
27+
visionModelSource: "missing",
28+
today: "2026-07-05",
29+
todayCards: 0,
30+
timeZone: "UTC",
31+
};
32+
}
33+
if (method === "logbook.days") {
34+
return { days: [] };
35+
}
36+
return {
37+
day: "2026-07-05",
38+
cards: [],
39+
stats: { trackedMs: 0, distractionMs: 0, categories: [], apps: [] },
40+
};
41+
}),
42+
} as unknown as GatewayBrowserClient;
43+
const snapshot: ApplicationGatewaySnapshot = {
44+
client,
45+
connected: true,
46+
hello,
47+
assistantAgentId: null,
48+
sessionKey: "main",
49+
lastError: null,
50+
lastErrorCode: null,
51+
};
52+
const page = new PluginPage();
53+
page.pluginId = "logbook";
54+
page.tabId = "logbook";
55+
(page as unknown as { context: ApplicationContext<RouteId> }).context = {
56+
gateway: { snapshot, subscribe: () => () => undefined },
57+
} as unknown as ApplicationContext<RouteId>;
58+
59+
document.body.append(page);
60+
try {
61+
await vi.waitFor(() => {
62+
expect(getLogbookState(page).pollTimer).not.toBeNull();
63+
});
64+
65+
hello.controlUiTabs = [];
66+
page.requestUpdate();
67+
await page.updateComplete;
68+
69+
expect(getLogbookState(page).pollTimer).toBeNull();
70+
} finally {
71+
page.remove();
72+
}
73+
});
74+
});

ui/src/pages/plugin/plugin-page.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,14 @@ export class PluginPage extends LitElement {
6969

7070
override willUpdate() {
7171
const key = this.tabKey();
72+
const hasBundledDescriptor = this.tabInfo() !== undefined && key in BUNDLED_TAB_VIEWS;
7273
// Switching between plugin tabs reuses this element; the previous bundled
73-
// view must stop its background polling before the next one renders.
74-
if (this.bundledViewId !== null && this.bundledViewId !== key) {
74+
// view must stop its background polling before the next one renders. A
75+
// descriptor can also disappear in place after disablement or scope loss.
76+
if (this.bundledViewId !== null && (this.bundledViewId !== key || !hasBundledDescriptor)) {
7577
this.stopBundledView();
7678
}
77-
if (this.bundledViewId === null && key in BUNDLED_TAB_VIEWS) {
79+
if (this.bundledViewId === null && hasBundledDescriptor) {
7880
this.bundledViewId = key;
7981
void BUNDLED_TAB_VIEWS[key]().then((view) => {
8082
if (this.bundledViewId === this.tabKey()) {

0 commit comments

Comments
 (0)