Ctrl K
ComunidadGuías…

electron/home-window.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// The desktop window once the person's bots moved to My Cloud (one-home.mjs): every path that shows a window goes
// through show(), which opens the place asked for (My Cloud, or a team's Cloud from the verified session), waking My
// Cloud through the Admin and signing in again by itself, or this computer's own screen at HOME_SCREEN_PATH while that
// place can't be reached, its plan has a problem, or this device must sign in again. Until the move is done, and for
// someone with no Cloud, show() is exactly what the app did before, except that a team's Cloud opens the same way.
//
// Security: this computer's frozen copy is never shown once its bots moved, and never lifted for another account or a
// signed-out app (its automations would run twice). The page that counts as home is the main window's main frame at the
// shown place's verified origin, nothing else. A team is only ever one the Admin's session lists, paired through the
// Admin's team pairing (its claims are the Admin's), never an address a page supplied.
import { cloudHomeConnectUrl, cloudSetupOf, cloudUnreachableCause, cloudUnreachableText, machineSetup, readyCloudOrigin } from "./cloud-home.mjs";
import { cloudMachine, cloudState, myCloudOf, otherOwnClouds, ownCloud } from "./cloud-list.mjs";
import { asleepAnswer, BACKGROUND_HEADERS, openSleepingCloud, SIGNED_IN_MS } from "./cloud-power.mjs";
import nativeText from "./native-text.cjs";
import { awayIn, parseJournal, sameCloud } from "./one-home.mjs";

export const HOME_SCREEN_PATH = "/home";
/** The look at an awake My Cloud before the window moves there: under 3 s, so the offline screen shows within 3 s. */
export const LOOK_MS = 2_500;
/** A place whose page hasn't loaded this long after the window went there (it answered the look, then nothing) is the
 * offline screen, with Try again: never an endless spinner. */
export const OPEN_MS = 30_000;
export const RETRY_MS = Object.freeze([2_000, 4_000, 8_000, 15_000, 30_000]);
/** A brand-new Cloud's name can be missing for minutes after the Admin says it is ready: a resolver that was asked while
 * it was set up keeps that "no such name" (RFC 2308 negative caching; Chromium's own host cache does too). For a Cloud
 * ready within FRESH_MS, a name not found keeps the setup screen up and the window tries again on NAME_RETRY_MS, with
 * Chromium's host cache cleared each time, for NAME_WAIT_MS; after that it is the offline screen. */
export const FRESH_MS = 15 * 60_000;
export const NAME_WAIT_MS = 10 * 60_000;
export const NAME_RETRY_MS = Object.freeze([5_000, 10_000, 20_000, 40_000, 60_000]);
/** Chromium's net errors for a name it could not find (net/base/net_error_list.h): NAME_NOT_RESOLVED,
 * NAME_RESOLUTION_FAILED and the DNS range. */
export const nameNotFound = (code) => code === -105 || code === -137 || (code <= -800 && code > -900);
/** A new version of My Cloud's page waits for this long without typing before the window reloads it. */
export const QUIET_MS = 30_000;
export const VERSION_EVERY_MS = 60_000;
/** A home page that loads this soon after the window reloaded it for a newer version is that version. */
export const UPDATING_MS = 30_000;
/** While the startup screen is up, a wake that takes longer hands over to this computer's screen, which says the same. */
export const SPLASH_HANDOFF_MS = 6_000;
/** A team's Cloud ends each session within an hour of its pairing (T2), so the window signs in again before that, without
 * moving the page: every TEAM_RENEW_MS after a pairing, or TEAM_RENEW_EARLY_MS before the end its session reports. The
 * Admin refusing that pairing (404) means the person is no longer in the team. */
export const TEAM_RENEW_MS = 45 * 60_000;
export const TEAM_RENEW_EARLY_MS = 10 * 60_000;
export const TEAM_RENEW_RETRY_MS = 5 * 60_000;
const SPLASH_KEYS = Object.freeze({
  opening: ["native.splash.openingCloud", "native.splash.openingNamed"], waking: ["native.splash.wakingCloud", "native.splash.wakingNamed"],
});
/** The startup screen's line: My Cloud's words, or the team's name. */
export function splashText(status, name = null, t = nativeText.englishText.t) {
  const [cloud, named] = SPLASH_KEYS[status === "waking" ? "waking" : "opening"];
  return name ? t(named, { name }) : t(cloud);
}
export const SPLASH_TEXT = Object.freeze({ opening: splashText("opening"), waking: splashText("waking") });
const PLAN_PROBLEMS = new Set(["stopped", "payment-problem", "failed"]);
/** The screens of a listed Cloud that isn't running (`cloudScreen`): nothing to look again for, the session's word moves them. */
const NOT_RUNNING = new Set(["ended", "checkout", "payment"]);
const MOVING = new Set(["freeze", "final", "check"]);
const DEFAULT_TIMERS = {
  set: (work, ms) => { const timer = setTimeout(work, ms); timer.unref?.(); return timer; },
  clear: (timer) => clearTimeout(timer),
  every: (work, ms) => { const timer = setInterval(work, ms); timer.unref?.(); return timer; },
};

const originOf = (url) => { try { return new URL(url).origin; } catch { return null; } };

/** The Cloud this computer moved to, at the address the verified session gives My Cloud now when My Cloud is that Cloud by
 * its id or by an address it had before it moved to another provider, for the journal's own account; as kept otherwise. */
function followedCloud(kept, state) {
  const mine = myCloudOf(state);
  if (!mine?.origin || !kept.origin || mine.origin === kept.origin || (kept.account && kept.account !== state.account?.id)) return kept;
  return sameCloud(mine, kept) ? { ...kept, origin: mine.origin } : kept;
}

/**
 * Who the window opens. `journal`: one-home.json read back; `frozenCloud`: this computer's freeze marker when there is no
 * journal (its Cloud's origin, or true when it names none); `state`: the MausBot Cloud sign-in's state. An ended My Cloud
 * sends home only a computer that moved to that Cloud; a freeze that names another Cloud of the signed-in person's own list
 * is theirs, and opens that Cloud.
 * @returns {{ kind: "computer" } | { kind: "cloud", origin: string } | { kind: "signin", other?: true }}
 */
export function homeOf({ journal = null, frozenCloud = null, state = null }) {
  const kept = journal ? (awayIn(journal) ? journal.cloud : null)
    : frozenCloud ? { origin: typeof frozenCloud === "string" ? frozenCloud : null, account: null } : null;
  if (!kept) return { kind: "computer" };
  const moved = followedCloud(kept, state);
  const connected = state?.status === "connected";
  const signedInNow = state?.account?.id ?? null;
  // This computer moved to another account's Cloud: its frozen copy stays frozen, and this account's own Cloud is what opens.
  if (moved.account && signedInNow && moved.account !== signedInNow && state.status !== "signed-out" && state.status !== "connecting") {
    const own = connected && !state.machine?.hold ? state.machine?.origin ?? null : null;
    return own && own !== moved.origin ? { kind: "cloud", origin: own, otherAccount: true } : { kind: "signin", other: true };
  }
  if (connected && state.machine?.hold && (!moved.origin || !state.machine.origin || state.machine.origin === moved.origin)) return { kind: "computer" };
  const signedIn = state?.account?.id ?? null;
  if (!signedIn || state.status === "signed-out" || state.status === "connecting") return { kind: "signin" };
  if (moved.account && moved.account !== signedIn) return { kind: "signin", other: true };
  const theirs = connected ? state.machine?.origin ?? null : null;
  const origin = moved.origin ?? theirs;
  if (!origin) return { kind: "signin" };
  const listed = connected && Array.isArray(state.clouds) && state.clouds.some((cloud) => ownCloud(cloud) && cloud.origin === origin);
  if (!moved.account && theirs && theirs !== origin && !listed) return { kind: "signin", other: true };
  return { kind: "cloud", origin };
}

/** Nobody here can reach the bots: nobody is signed in, or the account signed in has no way to their Cloud, so the clean
 * workspace opens (clean-workspace.mjs). An account the Admin can't be asked about isn't that, nor a sign-in still being read:
 * those are a Cloud that can't be reached yet, never an empty workspace that reads as their bots being gone. */
export function noWayHome(worked, state) {
  if (worked.kind !== "signin" || state?.message === "restoring") return false;
  return worked.other === true || !state?.account?.id || ["signed-out", "connecting", "connected"].includes(state.status);
}

/** A place besides My Cloud: a team's Cloud, or another Cloud of the person's own (`own`), each the session's. */
export const otherPlace = (place) => place?.kind === "team" || place?.kind === "own";

/** A place's plan problem. A team's User can't do anything about it, so the screen sends them to its owner (`member`). */
const planScreen = (machine, place) => ({ screen: "plan", machine, ...(place?.kind === "team" && place.role === "member" ? { member: true } : {}) });

/**
 * This computer's screen for a Cloud the session's list says isn't running, so its address is never looked up: it has ended
 * (stopped too; `deleteAt`, when its kept files are deleted), its checkout wasn't finished, or its payment has a problem, each
 * named and with Remove from list while its owner may (`removable`); or its setup's step. Null for one that opens.
 */
export function cloudScreen(cloud) {
  const state = cloudState(cloud);
  if (!state) return null;
  if (state === "setup") return { screen: "setup", ...(cloud.setup?.step ? { step: cloud.setup.step } : {}) };
  return { screen: state, name: cloud.name, ...(state === "ended" && cloud.deleteAfter ? { deleteAt: cloud.deleteAfter } : {}), ...(cloud.removable ? { removable: true } : {}) };
}

/** Another Cloud of the person's own, asked for (`{ cloud }`) or remembered: the session's word, as placeOf reads a team. One
 * the connected session no longer lists is simply My Cloud again; while the Admin can't be asked it opens as it was saved. */
function ownPlaceOf(asked, remembered, state) {
  const id = asked.cloud, cloud = otherOwnClouds(state?.clouds).find((entry) => entry.id === id) ?? null;
  if (cloud) return { kind: "own", id, name: cloud.name, role: cloud.role, origin: cloud.origin, machine: cloudMachine(cloud) };
  if (state?.status === "connected") return { kind: "cloud" };
  const kept = remembered?.cloud === id ? remembered : asked;
  return kept?.origin ? { kind: "own", id, name: kept.name ?? "the Cloud", role: null, origin: kept.origin, machine: null } : { kind: "cloud" };
}

/**
 * The place a window is asked for. `ask`: "cloud" for My Cloud, `{ team }` for a team's Cloud, `{ cloud }` for another
 * Cloud of the person's own, or nothing for the place remembered from last time (`remembered`). A team is read from the
 * verified session (`state.teams`); one the connected session no longer lists is gone, and the answer is My Cloud with
 * the team's name in `left`, so the window can say so once. While the Admin can't be asked, a remembered team opens as
 * it was saved, on the window's own session.
 * @returns {{ kind: "cloud", left?: string | null } | { kind: "team" | "own", id: string, name: string, role: string | null, origin: string | null, machine: any }}
 */
export function placeOf({ ask = null, remembered = null, state = null }) {
  const asked = ask ?? remembered;
  if (asked && asked !== "cloud" && typeof asked.cloud === "string") return ownPlaceOf(asked, remembered, state);
  const id = asked && asked !== "cloud" && typeof asked.team === "string" ? asked.team : null;
  if (!id) return { kind: "cloud" };
  const team = (Array.isArray(state?.teams) ? state.teams : []).find((entry) => entry?.id === id) ?? null;
  if (team) return { kind: "team", id, name: team.name, role: team.role, origin: team.machine?.origin ?? null, machine: team.machine ?? null };
  if (state?.status === "connected") return { kind: "cloud", left: asked.name ?? (remembered?.team === id ? remembered.name : null) ?? null };
  const kept = remembered?.team === id ? remembered : asked;
  return kept?.origin ? { kind: "team", id, name: kept.name ?? "the team", role: null, origin: kept.origin, machine: null } : { kind: "cloud" };
}

/** When this app saw My Cloud become ready after seeing it set up, kept in `store` so a launch soon after knows too. */
export function createReadyClock({ store = null, now = Date.now } = {}) {
  let settingUp = false, kept;
  const read = () => {
    if (kept !== undefined) return kept;
    try { const value = store?.read(); kept = typeof value?.origin === "string" && Number.isSafeInteger(value.readyAt) ? { origin: value.origin, readyAt: value.readyAt } : null; }
    catch { kept = null; }
    return kept;
  };
  return {
    seen(state) {
      if (cloudSetupOf(state)) { settingUp = true; return; }
      const origin = readyCloudOrigin(state);
      if (!origin || !settingUp) return;
      settingUp = false;
      kept = { origin, readyAt: now() };
      try { store?.write(kept); } catch {}
    },
    fresh(origin) {
      const at = read();
      return Boolean(origin) && at?.origin === origin && now() - at.readyAt < FRESH_MS;
    },
  };
}

/** Only a same-origin path survives a sign-in: never another site, never a code. */
export function routeOf(url, origin) {
  try {
    const at = new URL(url);
    if (at.origin !== origin || at.pathname === "/pair") return null;
    const route = `${at.pathname}${at.search}`;
    return route === "/" ? null : route;
  } catch { return null; }
}

/**
 * @param {{
 *   journal(): unknown,
 *   frozen(): Promise<{ frozen: boolean, cloud?: string } | null>,
 *   account(): { state(): any, power(): any, wakeHome(): Promise<unknown>, setWaking(on: boolean): void, pairHome(cloudId?: string | null): Promise<{ origin: string, code: string, expiresAt: number }>,
 *     pairTeam?(teamId: string): Promise<{ origin: string, code: string, expiresAt: number }>, signInAgain(): Promise<unknown>, begin(): Promise<unknown>, signOut?(): Promise<unknown>, reopen?(): Promise<unknown> } | null,
 *   restored?(): Promise<unknown>,
 *   window(): { isDestroyed(): boolean, webContents: { id: number, getURL(): string, reload(): void } } | null,
 *   ensureWindow(): void,
 *   localOrigin(): string,
 *   cookieFetch: typeof fetch,
 *   power: { asleep(): boolean, hold(): void, woke(): void },
 *   go(url: string, origin: string | null, place: { kind: "cloud", origin: string } | { kind: "team" | "own", id: string, name: string, role: string | null, origin: string } | null): Promise<unknown>,
 *   showLocal(leaving: boolean): unknown,
 *   remembered?(): ({ team: string } | { cloud: string }) & { name?: string, origin?: string } | null,
 *   remember?(place: { team: string } | { cloud: string } | null): void,
 *   left?(name: string): void,
 *   drop?(place: { kind: "team" | "own", id: string, name: string }): void,
 *   splash?(): { status(text: string): void } | null,
 *   lastInput?(): number,
 *   publish?(view: unknown): void,
 *   rehomed?(home: { kind: string, origin?: string }): void,
 *   versionEveryMs?: number,
 *   quietMs?: number,
 *   now?(): number,
 *   timers?: { set(work: () => void, ms: number): unknown, clear(timer: unknown): void, every(work: () => void, ms: number): unknown },
 *   hosts?(): { clearHostResolverCache(): Promise<void>, resolveHost(host: string, options?: { cacheUsage?: "allowed" | "staleAllowed" | "disallowed" }): Promise<{ endpoints: unknown[] }> } | null,
 *   readyStore?: { read(): unknown, write(value: unknown): void },
 *   ownMove?(): { phase: string, name: string } | null,
 *   log?(line: string): void,
 *   t?(key: string, params?: Record<string, string | number>): string,
 * }} deps `ownMove()`: the move to a server of the person's own (own-home-main.mjs), whose freeze is never My Cloud's.
 * `hosts`: the window's Electron session, for its own resolver and host cache. `readyStore`: when My Cloud
 * became ready (createReadyClock). `go(url, origin, place)`: main's one way to move the window, to My Cloud or a team's Cloud (`origin`, with the
 * `place` it is) or this computer's screen (null). `showLocal(leaving)`: the window as it was before the move (this computer's page, or a saved server it
 * shows); `leaving`: it showed a place or this computer's screen until now, so it goes back to this computer's page.
 * `reopen(place, still)`: this computer's copy moved to a server of the person's own, so the window goes back to that
 * server (`origin`, `place`); false when it isn't saved here any more, doesn't answer, or `still()` says a newer `show()` took over.
 * `frozen()`: this computer's server's freeze marker. `splash()`: the startup screen while it is up. `remembered()`: the
 * team or other Cloud shown last time, with its name and origin as saved; `remember(place)`: what to open next time;
 * `left(name)`: the team shown is no longer this person's, so the window says so once; `drop(place)`: the Admin said so
 * (404 on its pairing), so the team (with that line) or the Cloud leaves the switcher now. `rehomed(home)`: home as worked
 * out changed.
 */
export function createHomeWindow(deps) {
  const { journal, frozen, account, restored = async () => {}, window, ensureWindow, localOrigin, cookieFetch, power, go, showLocal, clean = null,
    reopen = async () => false,
    remembered = () => null, remember = () => {}, left = () => {}, drop = (place) => { if (place.kind === "team") left(place.name); remember(null); },
    splash = () => null, lastInput = () => 0, publish = () => {}, rehomed = () => {}, versionEveryMs = VERSION_EVERY_MS, quietMs = QUIET_MS,
    now = Date.now, timers = DEFAULT_TIMERS, hosts = () => null, readyStore = null, ownMove = () => null, log = () => {}, t = nativeText.englishText.t } = deps;
  let home = { kind: "computer" }, screen = null, status = null, opens = 0, retry = null, tries = 0, loaded = null, newer = false, repairing = null;
  let recovering = null;
  /** The profile (clean-workspace.mjs) the person opened from the rail while My Cloud is home, until they pick a place. */
  let profileShown = null;
  /** My Cloud asked for before the move made it home (`{ open }`), until its page loads or the person goes elsewhere;
   * `naming`: since when, and how often, a brand-new My Cloud's name was not found. */
  let asked = null, naming = null;
  const clock = createReadyClock({ store: readyStore, now });
  /** The place the window was last sent to (My Cloud, or a team's Cloud), and the one being opened now. */
  let shown = null, opening = null, renewal = null, updatingAt = null;
  /** The window shows a place or this computer's screen, which this computer's page replaces once home is here again. */
  let away = false;
  const state = () => account()?.state() ?? null;
  const wanted = (token) => token === opens;
  const cloudOrigin = () => (home.kind === "cloud" ? home.origin : null);
  /** The Cloud at `origin` sleeps now: My Cloud by its gate (the Admin's word and its own answer), any other of the
   * person's Clouds by the Admin's word in the Clouds list. A request to a sleeping one would wake it. */
  const asleepAt = (origin) => (origin && origin === cloudOrigin() ? power.asleep() : account()?.powerOf?.(origin)?.state === "asleep");
  /** The origin of the place on screen, or on its way: a team's Cloud while one is shown, else My Cloud. */
  const placeOrigin = () => (otherPlace(shown) ? (signedOut() ? null : shown.origin) : cloudOrigin());
  const teamName = () => (otherPlace(opening) ? opening.name : null);
  const pair = (place) => (place.kind === "team" ? account()?.pairTeam?.(place.id) : place.kind === "own" ? account()?.pairHome(place.id) : account()?.pairHome());
  /** The same place still the one on screen (a newer show() replaces the object, not the place). */
  const still = (place) => shown?.kind === place.kind && shown.id === place.id && shown.origin === place.origin;
  const clearRenewal = () => { if (renewal) timers.clear(renewal); renewal = null; };

  async function refresh() {
    clock.seen(state());
    const kept = parseJournal(journal());
    let frozenCloud = null, frozenPlace = null;
    if (!awayIn(kept)) {
      const marker = await frozen().catch(() => null);
      if (marker?.frozen && !marker.place) frozenCloud = typeof marker.cloud === "string" ? marker.cloud : true;
      else if (marker?.frozen && marker.place) frozenPlace = { origin: typeof marker.cloud === "string" ? marker.cloud : null, place: String(marker.place) };
    }
    if (frozenCloud || awayIn(kept)) await restored();
    const before = home;
    const worked = homeOf({ journal: kept, frozenCloud, state: state() });
    // Home is this computer then, in its clean workspace, so a Cloud of this account's own opens from it as for anyone.
    home = clean && noWayHome(worked, state()) ? { kind: "computer", clean: true, ...(worked.other ? { other: true } : {}) } : worked;
    if (frozenPlace) home = { ...home, frozenPlace };
    if (home.kind !== before.kind || home.origin !== before.origin || home.clean !== before.clean) {
      try { rehomed(home); } catch {}
    }
    return home;
  }

  /** What this computer's screen says: why the place isn't open, or that it is opening; `name` while it is a team's. */
  function view() {
    const name = teamName();
    const named = (value) => (name ? { ...value, name } : value);
    if (screen) return named(screen);
    if (status) return named({ screen: status });
    if (home.kind === "signin" && !name) return { screen: "signin" };
    const own = ownMove();
    if (own && MOVING.has(own.phase)) return { screen: "moving", name: own.name };
    return named({ screen: MOVING.has(parseJournal(journal())?.phase ?? "") ? "moving" : "opening" });
  }
  const tell = () => { try { publish(view()); } catch {} };
  let splashSaid = null;
  const onSplash = (text) => {
    if (text === splashSaid) return;
    splashSaid = text;
    try { splash()?.status(text); } catch {}
  };
  function say(next) {
    status = next;
    if (next) onSplash(splashText(next, teamName(), t));
    tell();
  }
  const shows = (origin) => {
    const win = window();
    return Boolean(win && !win.isDestroyed()) && originOf(win.webContents.getURL()) === origin;
  };
  const onOwnPage = () => {
    const win = window();
    const at = win && !win.isDestroyed() ? originOf(win.webContents.getURL()) : null;
    return !at || at === localOrigin();
  };
  const onScreen = () => {
    const win = window();
    if (!win || win.isDestroyed()) return false;
    try { const at = new URL(win.webContents.getURL()); return at.origin === localOrigin() && at.pathname === HOME_SCREEN_PATH; } catch { return false; }
  };
  const clearRetry = () => { if (retry) timers.clear(retry); retry = null; };
  /** Whatever was being opened for an earlier click stops; the window still shows what it showed. */
  function stopOpening() { opens++; recovering = null; clearRetry(); screen = null; status = null; }

  function goScreen() {
    tell();
    away = true;
    if (!onScreen()) void Promise.resolve(go(`${localOrigin()}${HOME_SCREEN_PATH}`, null, null)).catch(() => {});
  }
  /** The place can't be shown now: this computer's screen says why, and the startup screen, while it is up, that the place
   * is opening. */
  function toScreen(next) {
    screen = next;
    status = null;
    onSplash(splashText("opening", teamName(), t));
    goScreen();
  }

  /** The place didn't answer: this computer's screen says so, with the Admin's reason for My Cloud, and looks again by itself. */
  function offline(token) {
    if (!wanted(token)) return;
    const cause = otherPlace(opening) ? null : cloudUnreachableCause(state(), cloudOrigin());
    toScreen({ screen: "offline", ...(cause ? { reason: cloudUnreachableText(state(), cloudOrigin()), cause } : {}) });
    later();
  }
  function later(delays = RETRY_MS, count = () => tries++) {
    clearRetry();
    const ms = delays[Math.min(count(), delays.length - 1)];
    retry = timers.set(() => { retry = null; if (screen) void show(); }, ms);
  }
  /** Whether the window's own resolver finds `origin`'s name now, its host cache bypassed. */
  async function found(origin) {
    const resolver = hosts();
    if (!resolver) return true;
    const slow = new Promise((done) => { const timer = setTimeout(() => done(null), 2 * LOOK_MS); timer.unref?.(); });
    try { return (await Promise.race([resolver.resolveHost(new URL(origin).hostname, { cacheUsage: "disallowed" }), slow]))?.endpoints?.length > 0; }
    catch { return false; }
  }
  const forgetNames = () => Promise.resolve().then(() => hosts()?.clearHostResolverCache()).catch(() => {});
  /** My Cloud didn't answer: a brand-new one whose name isn't found yet is still settling in (the setup screen, tried
   * again sooner, for NAME_WAIT_MS); anything else is the offline screen. */
  async function unreachable(place, token, missing = null) {
    if (place.kind !== "cloud" || !(naming || clock.fresh(place.origin)) || (missing ?? !(await found(place.origin))) === false) return offline(token);
    if (!wanted(token)) return;
    naming ??= { since: now(), tries: 0 };
    if (now() - naming.since >= NAME_WAIT_MS) return offline(token);
    log(`home: My Cloud's name isn't found yet; trying again (${naming.tries + 1})`);
    toScreen({ screen: "setup", step: "checking" });
    later(NAME_RETRY_MS, () => naming.tries++);
  }
  /** The person went somewhere else while My Cloud was asked for: nothing pulls the window back. */
  function letAskGo() {
    asked = null; naming = null; screen = null; status = null; away = false;
    clearRetry();
  }
  const signInNeeded = () => ["reauth-required", "signed-out"].includes(state()?.status);
  /** This device has no MausBot Cloud sign-in to pair a team with (homeOf's rule for My Cloud). */
  const signedOut = () => { const current = state(); return !current?.account?.id || ["signed-out", "connecting"].includes(current.status); };
  /** The session's list's word for My Cloud (the account's primary Cloud), else null: an older Admin, or one it doesn't list. */
  const myListed = () => { const current = state(); return current?.status === "connected" && Array.isArray(current.clouds) ? current.clouds.find((cloud) => cloud.primary) ?? null : null; };
  /** My Cloud's own screen while the session says it isn't running: its list's word (`cloudScreen`) when the Admin sends a list
   * (none for a My Cloud off it: its owner removed it from their list), else its machine's (an older Admin). Null while it
   * runs or is set up: those open as before. */
  function myCloudScreen() {
    const current = state();
    if (current?.status !== "connected") return null;
    if (Array.isArray(current.clouds)) { const listed = cloudScreen(myListed()); return listed && listed.screen !== "setup" ? listed : null; }
    return current.machine && PLAN_PROBLEMS.has(current.machine.status) ? { screen: "plan", machine: current.machine.status } : null;
  }
  /** The session says My Cloud isn't running (its screen says so, or it was removed from the list): never looked up then. */
  const notRunning = () => Boolean(myCloudScreen()) || (state()?.status === "connected" && PLAN_PROBLEMS.has(state().machine?.status));

  async function answers(origin) {
    try {
      const response = await cookieFetch(`${origin}/api/health`, { credentials: "include", cache: "no-store", redirect: "manual",
        signal: AbortSignal.timeout(LOOK_MS), headers: { accept: "application/json", ...BACKGROUND_HEADERS } });
      await response.body?.cancel?.().catch(() => {});
      if (asleepAnswer(response)) return "asleep";
      return response.type === "opaqueredirect" || response.status < 500 ? "yes" : "no";
    } catch { return "no"; }
  }
  /** The window's own session there: `{ expiresAt }` (null when the server names no end), or null when it has none. */
  async function sessionAt(origin, timeoutMs) {
    try {
      const response = await cookieFetch(`${origin}/api/auth/session`, { credentials: "include", cache: "no-store", signal: AbortSignal.timeout(timeoutMs) });
      const body = response.ok ? await response.json() : null;
      return body?.kind === "session" ? { expiresAt: Number.isSafeInteger(body.expiresAt) ? body.expiresAt : null } : null;
    } catch { return null; }
  }
  const signedInAt = async (origin, timeoutMs) => Boolean(await sessionAt(origin, timeoutMs));
  /** A one-time code redeemed into the window's own cookie, as lending does for My Cloud: the page doesn't move. */
  async function redeem(origin, grant) {
    try {
      const response = await cookieFetch(`${origin}/api/auth/pair`, { method: "POST", credentials: "include", redirect: "error", cache: "no-store", signal: AbortSignal.timeout(15_000),
        headers: { "content-type": "application/json", origin }, body: JSON.stringify({ code: grant.code, cookie: true, label: "OpenMausBot app" }) });
      await response.body?.cancel?.().catch(() => {});
      return response.ok;
    } catch { return false; }
  }
  /** The next silent sign-in to the team on screen: `ends`, the end its session reports, else TEAM_RENEW_MS from now. */
  function renewLater(place, ms) {
    clearRenewal();
    renewal = timers.set(() => { renewal = null; void renew(place); }, Math.max(0, Math.min(ms, TEAM_RENEW_MS)));
  }
  /** The team on screen signs in again before its session ends, without moving the page; the Admin refusing means the
   * person was removed. Nothing happens for a team that is no longer on screen. */
  async function renew(place = shown) {
    clearRenewal();
    if (place?.kind !== "team" || !still(place) || !shows(place.origin)) return;
    let grant = null;
    try { grant = await pair(place); } catch (error) {
      if (error?.gone) { if (still(place)) await gone(place); return; }
      log(`home: could not sign in to ${place.name} again (${error?.message ?? error})`);
    }
    if (!still(place)) return;
    renewLater(place, grant && await redeem(place.origin, grant) ? TEAM_RENEW_MS : TEAM_RENEW_RETRY_MS);
  }

  /** The Admin says the team is no longer this person's: it leaves the switcher now, and My Cloud opens with one line. */
  function gone(place) {
    drop(place);
    clearRenewal();
    if (shown?.kind === place.kind && shown.id === place.id) shown = null;
    return show({ place: "cloud" });
  }

  async function connect(place, token, open) {
    const origin = place.origin;
    // Only a window this opened as home is left as it is: one that shows My Cloud as a saved server (opened from the
    // switcher before the move) is opened again as home, and goes home with it if My Cloud ends.
    if (shows(origin) && away && !screen && !open) {
      shown = place;
      if (place.kind === "team" && !renewal) renewLater(place, TEAM_RENEW_MS);
      return;
    }
    clearRetry();
    opening = place;
    const client = account();
    const machine = otherPlace(place) ? place.machine : state()?.status === "connected" ? state().machine : null;
    // My Cloud the session's list says isn't running: its own screen, never its address, and nothing to look again for.
    const listed = place.kind === "cloud" ? cloudScreen(myListed()) : null;
    if (listed && NOT_RUNNING.has(listed.screen)) { clearRetry(); return toScreen(listed); }
    if (machine && (PLAN_PROBLEMS.has(machine.status) || (place.kind === "team" && machine.hold))) {
      toScreen(planScreen(PLAN_PROBLEMS.has(machine.status) ? machine.status : "stopped", place)); later(); return;
    }
    // Its address is never looked up while it is set up: the session's next word (changed()) opens it.
    const setup = machineSetup(machine);
    if (setup) return toScreen({ screen: "setup", ...setup });
    const win = window();
    const blank = !win || win.isDestroyed() || !originOf(win.webContents.getURL());
    // A team's Cloud never sleeps, and the Admin wakes only My Cloud: a team that answers asleep is simply not reachable yet.
    let asleep = place.kind === "cloud" && power.asleep();
    say(asleep ? "waking" : "opening");
    const handoff = blank ? timers.set(() => { if (wanted(token) && status) goScreen(); }, SPLASH_HANDOFF_MS) : null;
    try {
      if (!asleep) {
        if (naming) await forgetNames();
        const look = await answers(origin);
        if (look === "no") return unreachable(place, token);
        if (look === "asleep" && otherPlace(place)) return offline(token);
        if (look === "asleep") { power.hold(); asleep = true; if (wanted(token)) say("waking"); }
      }
      let session = null;
      const signedIn = asleep
        ? await openSleepingCloud({ power: client?.power() ?? { state: "asleep" }, wake: () => client?.wakeHome(), waking: (on) => client?.setWaking(on), signedIn: (ms) => signedInAt(origin, ms) })
        : Boolean(session = await sessionAt(origin, SIGNED_IN_MS));
      if (!wanted(token)) return;
      let grant = null;
      if (!signedIn) {
        try { grant = await pair(place); } catch (error) {
          if (error?.gone && wanted(token)) return gone(place);
          log(`home: no sign-in code for ${otherPlace(place) ? place.name : "My Cloud"} (${error?.message ?? error})`);
        }
        if (!wanted(token)) return;
        if (!grant) {
          if (signInNeeded()) return toScreen({ screen: "signin" });
          return offline(token);
        }
      }
      // My Cloud opened before the move made it home is a saved server the window shows, as the Server menu opens one.
      const asHome = otherPlace(place) || home.kind !== "computer";
      screen = null; status = null; tries = 0; away = asHome; shown = asHome ? place : null;
      // A page that never finishes loading is the offline screen after OPEN_MS, which looks again by itself and has Try again.
      let slow = null;
      const loaded = await Promise.race([Promise.resolve(go(cloudHomeConnectUrl({ origin, grant }, now(), open === "phone" ? "phone" : null), origin, place)).then(() => true, () => true),
        new Promise((done) => { slow = timers.set(() => done(false), OPEN_MS); })]);
      timers.clear(slow);
      if (!loaded) { log(`home: ${otherPlace(place) ? place.name : "My Cloud"} didn't load in ${OPEN_MS / 1000} s`); return offline(token); }
      if (place.kind === "cloud") { clearRenewal(); power.woke(); }
      else if (place.kind === "team") renewLater(place, grant || !session?.expiresAt ? TEAM_RENEW_MS : session.expiresAt - now() - TEAM_RENEW_EARLY_MS);
      else clearRenewal();
      tell();
    } finally {
      if (handoff) timers.clear(handoff);
    }
  }

  /** Shows the window onto the person's home. `open: "phone"` opens My Cloud's Settings on its phone pairing. `place`:
   * "cloud" for My Cloud, `{ team }` for a team's Cloud, `{ cloud }` for another of the person's own, else the place
   * remembered from last time. */
  async function show({ open = null, place = null } = {}) {
    if (recovering) { if (!open && !place) return; recovering = null; }
    const token = ++opens;
    if (open || place) profileShown = null;
    const current = await refresh();
    if (!wanted(token)) return;
    if (current.kind !== "cloud") profileShown = null;
    // This computer's own copy is the server again before anything of the person's Cloud opens; a profile they opened keeps
    // running, so going back to it is one click.
    if (!current.clean && clean?.active() && !clean.profile?.()) { await clean.leave(); if (!wanted(token)) return; }
    if (profileShown) {
      opening = null; asked = null; naming = null;
      clearRenewal();
      if (await clean.open(profileShown)) {
        if (!wanted(token)) return;
        // `away`: a Cloud's page may still be on its way (a click just before this one); this page's load replaces it.
        const leaving = away || onScreen() || !shows(localOrigin());
        ensureWindow(); clearRetry(); screen = null; status = null; shown = null; away = false;
        return showLocal(leaving);
      }
      profileShown = null;
      if (!wanted(token)) return;
    }
    // Nobody signed in has nothing to open but the clean workspace; the place stays remembered for when they're back.
    const target = current.clean && signedOut() ? { kind: "cloud" } : placeOf({ ask: place, remembered: remembered(), state: state() });
    if (target.kind === "cloud" && target.left) { left(target.left); remember(null); }
    if (otherPlace(target)) {
      ensureWindow();
      opening = target;
      // Chosen is remembered at once, so the screen's own retry (and the next launch) asks for the same place.
      remember(target.kind === "own" ? { cloud: target.id } : { team: target.id });
      if (signedOut()) { clearRetry(); return toScreen({ screen: "signin" }); }
      // Another Cloud of their own that the session's list says isn't running: its own screen, never its address.
      const listed = target.kind === "own" ? cloudScreen(otherOwnClouds(state()?.clouds).find((cloud) => cloud.id === target.id)) : null;
      if (listed && NOT_RUNNING.has(listed.screen)) { clearRetry(); return toScreen(listed); }
      const setup = machineSetup(target.machine);
      if (setup) { clearRetry(); return toScreen({ screen: "setup", ...setup }); }
      if (!target.origin) { toScreen(planScreen(target.machine?.status ?? "stopped", target)); later(); return; }
      return connect(target, token, open);
    }
    if (place === "cloud" || target.left) remember(null);
    opening = null;
    clearRenewal();
    if (current.kind === "computer" && asked) {
      const setup = cloudSetupOf(state()), mine = readyCloudOrigin(state()), stopped = setup || mine ? null : myCloudScreen();
      if (setup || mine || stopped) ensureWindow();
      if (setup) { clearRetry(); return toScreen({ screen: "setup", ...setup }); }
      if (mine) return connect({ kind: "cloud", origin: mine }, token, open ?? asked.open);
      // Not running (it ended, its checkout wasn't finished, a payment problem): its own screen, never its address.
      if (stopped) { clearRetry(); return toScreen(stopped); }
      asked = null; naming = null;
    }
    if (current.kind === "computer") {
      const parkedAt = current.frozenPlace;
      if (!current.clean && parkedAt && !ownMove() && onOwnPage()) {
        clearRetry(); screen = null; status = null; shown = null; away = false;
        ensureWindow();
        let slow = null;
        const mine = {}; recovering = mine;
        try {
          const back = await Promise.race([
            reopen(parkedAt, () => wanted(token)).catch(() => false),
            new Promise((done) => { slow = timers.set(() => done(false), OPEN_MS); }),
          ]);
          timers.clear(slow);
          if (!wanted(token)) return;
          if (back) return;
          toScreen({ screen: "offline", reason: `Your bots are on ${parkedAt.place}.` });
          return later();
        } finally { if (recovering === mine) recovering = null; }
      }
      if (current.clean && !(await clean.enter())) { if (wanted(token)) { ensureWindow(); clearRetry(); toScreen({ screen: "signin", ...(current.other ? { other: true } : {}) }); } return; }
      if (!wanted(token)) return;
      clearRetry(); screen = null; status = null; shown = null;
      const win = window();
      const leaving = away || Boolean(win && !win.isDestroyed() && !originOf(win.webContents.getURL()));
      away = false;
      return showLocal(leaving);
    }
    ensureWindow();
    if (current.kind === "signin") { clearRetry(); return toScreen({ screen: "signin", ...(current.other ? { other: true } : {}) }); }
    return connect({ kind: "cloud", origin: current.origin }, token, open);
  }

  /** The place's session in this window ended: a new one by itself, back where the page was. */
  function repair() {
    const place = shown ?? (home.kind === "cloud" ? { kind: "cloud", origin: home.origin } : null);
    if (repairing || !place) return repairing ?? Promise.resolve();
    const origin = place.origin;
    repairing = (async () => {
      if (await signedInAt(origin, SIGNED_IN_MS)) return;
      const win = window();
      const route = win && !win.isDestroyed() ? routeOf(win.webContents.getURL(), origin) : null;
      let grant = null;
      try { grant = await pair(place); } catch (error) {
        if (error?.gone) return gone(place);
        log(`home: could not sign in to ${otherPlace(place) ? place.name : "My Cloud"} again (${error?.message ?? error})`);
      }
      if (placeOrigin() !== origin) return;
      if (!grant) return signInNeeded() ? toScreen({ screen: "signin" }) : offline(++opens);
      await go(cloudHomeConnectUrl({ origin, grant }, now(), null, route), origin, place);
    })().finally(() => { repairing = null; });
    return repairing;
  }

  async function version(origin) {
    try {
      const response = await cookieFetch(`${origin}/.well-known/openmausbot/environment`, { cache: "no-store", redirect: "error",
        signal: AbortSignal.timeout(LOOK_MS), headers: { accept: "application/json", ...BACKGROUND_HEADERS } });
      const body = response.ok ? await response.json() : null;
      return typeof body?.version === "string" ? body.version : null;
    } catch { return null; }
  }
  /** A newer page reloads at a quiet moment; a Cloud asleep is never asked (that would start it). */
  async function watchVersion() {
    const origin = placeOrigin();
    if (!origin || !shows(origin) || asleepAt(origin)) return;
    const current = await version(origin);
    if (!current) return;
    if (!loaded) { loaded = current; return; }
    if (current !== loaded) newer = true;
    if (!newer || now() - lastInput() < quietMs || !shows(origin)) return;
    newer = false; loaded = current; updatingAt = now();
    window()?.webContents.reload();
  }
  const watcher = timers.every(() => { void watchVersion(); }, versionEveryMs);
  function homePage(event) {
    const origin = placeOrigin(), win = window();
    if (!origin || !win || win.isDestroyed() || event?.sender !== win.webContents || !event.senderFrame || event.senderFrame !== win.webContents.mainFrame) return false;
    return originOf(event.senderFrame.url) === origin;
  }

  return {
    show,
    /** Profile `id` from the rail while My Cloud is home: the window shows it, and My Cloud is one click away. False when it
     * can't open, and the window shows My Cloud as before. */
    async openProfile(id) {
      if (!clean?.open || (await refresh()).kind !== "cloud") return false;
      profileShown = id;
      await show();
      return profileShown === id;
    },
    /** The profile on screen, opened from the rail, else null. */
    profile: () => profileShown,
    /** Whether `ask` ("cloud" for My Cloud, or a team or another Cloud of the person's own as remembered) would open now, by
     * the session's word and nothing looked up: false for one gone from its list, not running, being set up or without an
     * address, and for My Cloud while it isn't home or isn't running; null while nobody is signed in. As of the last
     * refresh(). */
    opensNow(ask) {
      if (signedOut()) return null;
      if (ask === "cloud") return home.kind === "cloud" && !notRunning();
      const target = placeOf({ ask, remembered: remembered(), state: state() });
      if (!otherPlace(target) || !target.origin || machineSetup(target.machine)) return false;
      return !target.machine || !(PLAN_PROBLEMS.has(target.machine.status) || (target.kind === "team" && target.machine.hold));
    },
    /** The person asked for My Cloud (the Server menu, the welcome, Settings, the ready notice). Before the move made it
     * home it opens as a saved server, and while the Admin sets it up the setup screen shows until it opens by itself. */
    async openCloud(open = null) {
      if ((await refresh()).kind !== "computer") return show({ place: "cloud", open });
      if (!cloudSetupOf(state()) && !readyCloudOrigin(state()) && !notRunning()) throw new Error(t("native.cloud.notReady"));
      asked = { open };
      return show({ place: "cloud", open });
    },
    /** Remove from list, from this computer's screen (its own button, once the person said yes there): the Cloud the screen
     * shows leaves their list on the Admin (cloud-account.mjs hideCloud), and the window goes where it would without it.
     * Answers the outcome; nothing is asked of a Cloud the session doesn't say is `removable`. */
    async remove() {
      const own = opening?.kind === "own" ? opening : null, cloudId = own ? own.id : myListed()?.id ?? null;
      const cloud = cloudId ? (state()?.clouds ?? []).find((entry) => entry.id === cloudId) ?? null : null;
      if (!cloud?.removable || !screen?.removable) return "unsupported";
      const client = account();
      const outcome = client?.hideCloud ? (await client.hideCloud(cloudId).catch(() => ({ outcome: "failed" }))).outcome : "unsupported";
      if (outcome !== "removed" && outcome !== "gone") return outcome;
      if (own) { if (opening === own) opening = null; drop(own); } else { asked = null; naming = null; }
      screen = null; status = null; clearRetry();
      await show({ place: "cloud" });
      return outcome;
    },
    /** My Cloud is asked for and not open yet: the window shows this computer's screen for it. */
    asking: () => Boolean(asked),
    refresh,
    view,
    /** My Cloud's origin while it is home, else null. */
    cloudOrigin,
    /** Whether the Cloud at `origin` sleeps now (see `asleepAt`): nothing in the background asks it then. */
    asleepAt,
    /** The origin of the place the window shows (a team's Cloud, or My Cloud), else null. */
    placeOrigin,
    /** Home as last worked out. */
    current: () => home,
    /** The place the window was last sent to: My Cloud, or a team's or another own Cloud (`{ kind: "team" | "own", id, name, origin }`). */
    shown: () => shown,
    /** The person asked again (the screen's timer, back online, or the window focused). */
    retry: () => show(),
    /** The screen's one button: this device's MausBot Cloud sign-in, again. A sign-in already open in the browser opens
     * again; one with an account My Cloud isn't (`other`) ends first, so the browser can sign in another; one that is still
     * good only looks again. */
    async signIn() {
      const client = account();
      if (!client) return;
      const status = client.state().status;
      if (status === "reauth-required") return void await client.signInAgain();
      if (status === "connecting") return void await client.reopen?.();
      if (status !== "signed-out") {
        const current = await refresh();
        if (!current.other) return void await show();
        await client.signOut?.();
      }
      await client.begin();
    },
    repair,
    /** The MausBot Cloud sign-in changed: a window that waits on it, a home that changed, or a team shown that the
     * session no longer lists (or lists at another address), shows again. */
    async changed() {
      const before = home;
      const after = await refresh();
      const next = shown && shows(shown.origin) ? placeOf({ remembered: remembered(), state: state() }) : null;
      const moved = Boolean(next && (next.kind !== shown.kind || (next.origin ?? null) !== (shown.origin ?? null) || (next.id ?? null) !== (shown.id ?? null)));
      if (moved || after.kind !== before.kind || after.origin !== before.origin || after.clean !== before.clean || screen || status) await show();
    },
    /** The signed-in account is about to change: nothing under way for the one leaving goes on, and the next show() takes
     * the window off whatever it shows of that account. */
    accountLeaving() {
      opens++;
      profileShown = null;
      clearRetry(); clearRenewal();
      asked = null; naming = null; shown = null; opening = null; screen = null; status = null;
      away = away || !shows(localOrigin());
    },
    /** A response the window got (Electron's webRequest): a 401 from the place's API means its session ended. */
    responded({ url, statusCode, webContentsId }) {
      const origin = placeOrigin(), win = window();
      if (statusCode !== 401 || !origin || !win || win.isDestroyed() || webContentsId !== win.webContents.id) return;
      try { const at = new URL(url); if (at.origin !== origin || !at.pathname.startsWith("/api/")) return; } catch { return; }
      // The place's own sign-in page is the new session on its way: its 401s until it redeems the code start nothing.
      try { const page = new URL(win.webContents.getURL()); if (page.origin === origin && page.pathname === "/pair") return; } catch {}
      void repair();
    },
    /** The main frame went somewhere: the place's own sign-in page means its session ended; any other page of it is
     * the version the window now shows. */
    navigated(url) {
      // Only a page that loaded counts: an error page (chrome-error:) is the load that failed().
      if (asked && /^https?:/.test(url)) {
        const at = originOf(url);
        if (at === readyCloudOrigin(state())) { asked = null; naming = null; }
        else if (!(at === localOrigin() && new URL(url).pathname === HOME_SCREEN_PATH)) letAskGo();
      }
      // The window went to another server (the Server menu): the team it showed is no longer the place on screen.
      if (otherPlace(shown) && ![shown.origin, localOrigin()].includes(originOf(url))) { shown = null; clearRenewal(); }
      const origin = placeOrigin();
      if (!origin || originOf(url) !== origin) return;
      const at = new URL(url);
      if (at.pathname === "/pair" && !/[#&]code=/.test(at.hash)) { void repair(); return; }
      newer = false; loaded = null;
      void version(origin).then((current) => { if (current && placeOrigin() === origin) loaded = current; });
    },
    /** The main frame could not load the place, or My Cloud before it is home (Chromium's net error `code`): this
     * computer's screen says so and the window tries again by itself. False for any other server. */
    failed(url, code = 0) {
      const at = originOf(url), place = placeOrigin(), mine = readyCloudOrigin(state());
      const parkedAt = home.frozenPlace;
      if (!place && parkedAt?.origin && at === parkedAt.origin) {
        ++opens;
        if (ownMove()) tell();
        else toScreen({ screen: "offline", reason: `Your bots are on ${parkedAt.place}.` });
        later();
        return true;
      }
      if (!at || (at !== place && (home.kind !== "computer" || at !== mine))) return false;
      if (at !== place) asked ??= { open: null };
      const token = ++opens;
      void unreachable(otherPlace(shown) && at === place ? shown : { kind: "cloud", origin: at }, token, nameNotFound(code));
      return true;
    },
    /** `event` came from the shown place's own page in this window (My Cloud's while it is home, or a team's Cloud's). */
    homePage,
    updating(event) {
      if (!homePage(event)) return false;
      const reloaded = updatingAt !== null && now() - updatingAt < UPDATING_MS;
      updatingAt = null;
      return reloaded;
    },
    /** `event` came from this computer's screen in this window. */
    screenPage(event) {
      const win = window();
      if (!win || win.isDestroyed() || event?.sender !== win.webContents || !event.senderFrame || event.senderFrame !== win.webContents.mainFrame) return false;
      return originOf(event.senderFrame.url) === localOrigin();
    },
    /** Signs the team on screen in again now, without moving the page (the timer's own step; also for tests). */
    renew: () => renew(),
    cancel() { stopOpening(); shown = null; away = false; },
    stopOpening,
    /** The person chose the place on screen again before the one they'd asked for opened: it stays as it is. */
    stay() { stopOpening(); profileShown = null; },
    close() { opens++; clearRetry(); clearRenewal(); timers.clear(watcher); },
  };
}

/** The window's IPC: this computer's screen reads why the place isn't open and asks again; the place's own page, while
 * it is shown, asks for a new session; either asks this device's sign-in again (from the person's click on My Cloud). */
export function installHomeWindowIpc({ ipcMain, homeWindow, cloudPage }) {
  const from = (channel, allowed, handler) => (event, ...args) => {
    if (!allowed(event)) throw new Error(`${channel} is only available in this app's window`);
    return handler(...args);
  };
  const screenPage = (event) => homeWindow.screenPage(event);
  ipcMain.handle("home:state", from("home:state", screenPage, () => homeWindow.view()));
  ipcMain.handle("home:retry", from("home:retry", screenPage, () => homeWindow.retry().then(() => homeWindow.view())));
  ipcMain.handle("home:remove", from("home:remove", screenPage, () => homeWindow.remove()));
  ipcMain.handle("home:sign-in", from("home:sign-in", (event) => screenPage(event) || cloudPage(event) === "cloud" || (homeWindow.homePage(event) && homeWindow.shown()?.kind === "own"), () => homeWindow.signIn()));
  ipcMain.handle("home:repair", from("home:repair", (event) => homeWindow.homePage(event), () => homeWindow.repair()));
  ipcMain.on("home:page", (event) => { try { event.returnValue = homeWindow.homePage(event); } catch { event.returnValue = false; } });
  ipcMain.on("home:updating", (event) => { try { event.returnValue = homeWindow.updating(event); } catch { event.returnValue = false; } });
}