mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-11 17:00:22 +00:00
* 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
99 lines
3.0 KiB
TypeScript
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);
|
|
}
|