refactor: unify restart gating and update availability sync

This commit is contained in:
Peter Steinberger
2026-02-19 10:00:27 +01:00
parent 18179fc2c1
commit b4dbe03298
25 changed files with 288 additions and 41 deletions

View File

@@ -17,13 +17,6 @@
padding: 10px 16px;
}
.update-banner code {
background: rgba(239, 68, 68, 0.15);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.update-banner__btn {
margin-left: 8px;
border-color: var(--danger);

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { GATEWAY_EVENT_UPDATE_AVAILABLE } from "../../../src/gateway/events.js";
import { connectGateway } from "./app-gateway.ts";
type GatewayClientMock = {
@@ -79,6 +80,7 @@ function createHost() {
refreshSessionsAfterChat: new Set<string>(),
execApprovalQueue: [],
execApprovalError: null,
updateAvailable: null,
} as unknown as Parameters<typeof connectGateway>[0];
}
@@ -126,6 +128,38 @@ describe("connectGateway", () => {
expect(host.eventLogBuffer[0]?.event).toBe("presence");
});
it("applies update.available only from active client", () => {
const host = createHost();
connectGateway(host);
const firstClient = gatewayClientInstances[0];
expect(firstClient).toBeDefined();
connectGateway(host);
const secondClient = gatewayClientInstances[1];
expect(secondClient).toBeDefined();
firstClient.emitEvent({
event: GATEWAY_EVENT_UPDATE_AVAILABLE,
payload: {
updateAvailable: { currentVersion: "1.0.0", latestVersion: "9.9.9", channel: "latest" },
},
});
expect(host.updateAvailable).toBeNull();
secondClient.emitEvent({
event: GATEWAY_EVENT_UPDATE_AVAILABLE,
payload: {
updateAvailable: { currentVersion: "1.0.0", latestVersion: "2.0.0", channel: "latest" },
},
});
expect(host.updateAvailable).toEqual({
currentVersion: "1.0.0",
latestVersion: "2.0.0",
channel: "latest",
});
});
it("ignores stale client onClose callbacks after reconnect", () => {
const host = createHost();

View File

@@ -1,3 +1,7 @@
import {
GATEWAY_EVENT_UPDATE_AVAILABLE,
type GatewayUpdateAvailableEventPayload,
} from "../../../src/gateway/events.js";
import { CHAT_SESSIONS_ACTIVE_MINUTES, flushChatQueueForEvent } from "./app-chat.ts";
import type { EventLogEntry } from "./app-events.ts";
import {
@@ -26,7 +30,13 @@ import type { GatewayEventFrame, GatewayHelloOk } from "./gateway.ts";
import { GatewayBrowserClient } from "./gateway.ts";
import type { Tab } from "./navigation.ts";
import type { UiSettings } from "./storage.ts";
import type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from "./types.ts";
import type {
AgentsListResult,
PresenceEntry,
HealthSnapshot,
StatusSummary,
UpdateAvailable,
} from "./types.ts";
type GatewayHost = {
settings: UiSettings;
@@ -54,7 +64,7 @@ type GatewayHost = {
refreshSessionsAfterChat: Set<string>;
execApprovalQueue: ExecApprovalRequest[];
execApprovalError: string | null;
updateAvailable: { currentVersion: string; latestVersion: string; channel: string } | null;
updateAvailable: UpdateAvailable | null;
};
type SessionDefaultsSnapshot = {
@@ -270,6 +280,12 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
if (resolved) {
host.execApprovalQueue = removeExecApproval(host.execApprovalQueue, resolved.id);
}
return;
}
if (evt.event === GATEWAY_EVENT_UPDATE_AVAILABLE) {
const payload = evt.payload as GatewayUpdateAvailableEventPayload | undefined;
host.updateAvailable = payload?.updateAvailable ?? null;
}
}
@@ -279,7 +295,7 @@ export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
presence?: PresenceEntry[];
health?: HealthSnapshot;
sessionDefaults?: SessionDefaultsSnapshot;
updateAvailable?: { currentVersion: string; latestVersion: string; channel: string };
updateAvailable?: UpdateAvailable;
}
| undefined;
if (snapshot?.presence && Array.isArray(snapshot.presence)) {

View File

@@ -221,7 +221,7 @@ export type AppViewState = {
logsLimit: number;
logsMaxBytes: number;
logsAtBottom: boolean;
updateAvailable: { currentVersion: string; latestVersion: string; channel: string } | null;
updateAvailable: import("./types.js").UpdateAvailable | null;
client: GatewayBrowserClient | null;
refreshSessionsAfterChat: Set<string>;
connect: () => void;

View File

@@ -300,11 +300,7 @@ export class OpenClawApp extends LitElement {
@state() cronRuns: CronRunLogEntry[] = [];
@state() cronBusy = false;
@state() updateAvailable: {
currentVersion: string;
latestVersion: string;
channel: string;
} | null = null;
@state() updateAvailable: import("./types.js").UpdateAvailable | null = null;
@state() skillsLoading = false;
@state() skillsReport: SkillStatusReport | null = null;

View File

@@ -1,3 +1,5 @@
export type UpdateAvailable = import("../../../src/infra/update-startup.js").UpdateAvailable;
export type ChannelsStatusSnapshot = {
ts: number;
channelOrder: string[];