Files
Netcatty/application/state/monitorPanelsStorage.ts
T
陈大猫 bace30578c feat: add synced custom host monitoring panels (#3703)
* feat: add synced custom host monitoring panels

* fix: preserve shared monitoring demand and complete terminal stats

* fix: transfer queued monitoring prompts when a window closes

* fix: keep reused hosts sampling while authentication is queued

* docs: record monitoring acceptance evidence

* fix: transfer pending monitoring auth after panel stop

* fix: release monitor owners in direct SSH runtime

* fix: keep worker monitoring sender compatibility

* fix: include monitoring configuration in automatic sync

* fix: disambiguate independent synced monitoring panel names

* test: assert existing panel order after cloud merge

* test: model browser locks and await monitoring state on Node 22
2026-10-17 15:05:30 +08:00

99 lines
3.0 KiB
TypeScript

import { mergeSyncPayloads } from "../../domain/syncMerge";
import { useSyncExternalStore } from "react";
import {
normalizeMonitorPanels,
rebaseMonitorPanel,
type MonitorPanel,
} from "../../domain/monitorPanels";
import { STORAGE_KEY_MONITOR_PANELS } from "../../infrastructure/config/storageKeys";
import {
localStorageAdapter,
LOCAL_STORAGE_ADAPTER_CHANGED_EVENT,
} from "../../infrastructure/persistence/localStorageAdapter";
import { withVaultImportLock } from "./vaultManagedImportLock";
export function readMonitorPanels(): MonitorPanel[] {
return normalizeMonitorPanels(
localStorageAdapter.read(STORAGE_KEY_MONITOR_PANELS),
);
}
export function writeMonitorPanels(value: unknown): void {
if (
!localStorageAdapter.write(
STORAGE_KEY_MONITOR_PANELS,
normalizeMonitorPanels(value),
)
)
throw new Error("monitor.saveFailed");
}
export async function saveMonitorPanel(
base: MonitorPanel | null,
next: MonitorPanel,
): Promise<void> {
return withVaultImportLock("vault", async () =>
writeMonitorPanels(rebaseMonitorPanel(readMonitorPanels(), base, next)),
);
}
export async function deleteMonitorPanel(id: string): Promise<void> {
return withVaultImportLock("vault", async () =>
writeMonitorPanels(readMonitorPanels().filter((p) => p.id !== id)),
);
}
/** Rebase a prepared cloud snapshot against edits committed while it was applying. */
export async function applyMonitorPanels(
value: unknown,
base: MonitorPanel[],
replace = false,
): Promise<void> {
return withVaultImportLock("vault", async () => {
const remote = normalizeMonitorPanels(value);
if (replace) {
writeMonitorPanels(remote);
return;
}
const payload = (panels: MonitorPanel[]) => ({
hosts: [],
keys: [],
snippets: [],
customGroups: [],
syncedAt: 0,
monitorPanels: panels,
});
writeMonitorPanels(
mergeSyncPayloads(
payload(base),
payload(readMonitorPanels()),
payload(remote),
).payload.monitorPanels ?? [],
);
});
}
let cachedRaw: string | null | undefined;
let cached: MonitorPanel[] = [];
function getSnapshot() {
const raw = localStorageAdapter.readString(STORAGE_KEY_MONITOR_PANELS);
if (raw !== cachedRaw) {
cachedRaw = raw;
cached = readMonitorPanels();
}
return cached;
}
function subscribe(listener: () => void) {
const handler = (event: Event) => {
const key =
event instanceof StorageEvent
? event.key
: (event as CustomEvent<{ key: string }>).detail?.key;
if (!key || key === STORAGE_KEY_MONITOR_PANELS) listener();
};
window.addEventListener("storage", handler);
window.addEventListener(LOCAL_STORAGE_ADAPTER_CHANGED_EVENT, handler);
return () => {
window.removeEventListener("storage", handler);
window.removeEventListener(LOCAL_STORAGE_ADAPTER_CHANGED_EVENT, handler);
};
}
export function useMonitorPanels(): MonitorPanel[] {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}