import { ControlPlaneLayout } from "./ControlPlaneLayout.tsx"; import { useEffect, useRef, useState } from "octane"; import { Button, LinkButton } from "octane-kumo/components/button"; import { LayerCard } from "octane-kumo/components/layer-card"; import { InstallationProgress } from "./InstallationProgress.tsx"; import type { ReleaseIdentity } from "../installation-metadata.ts"; import { sameRelease } from "../installation-metadata.ts"; type View = | "checking" | "select" | "updating" | "success" | "current" | "unsupported" | "failed" | "reconnect" | "error"; import { usePublisherSession } from "./PublisherQueryProvider"; import { useInstallationQuery, installationOptions, } from "./queries/use-installation-query"; import { useConnectionQuery, connectionOptions, } from "./queries/use-connection-query"; import { PublisherReadError } from "./publisher-request"; const key = (id: string) => `flarebot-update-${id}`; const requestId = () => crypto.randomUUID().replaceAll("-", ""); export function UpdateInstallation({ installationId, identityError, }: { installationId: string; identityError: Error | null; }) { const [view, setView] = useState("checking"); const cache = usePublisherSession(); const epoch = cache.getSnapshot().epoch; const query = useInstallationQuery({ id: installationId, interval: view === "updating" ? 1500 : false, }); const detail = query.data ?? null; const needsGrant = !!detail?.latestRelease && detail.installation.status === "ready" && !sameRelease(detail.installation.installedRelease, detail.latestRelease) && !!detail.installation.installedRelease && detail.upgradeFrom.includes( detail.installation.installedRelease.artifactDigest, ); const accountQuery = useConnectionQuery({ enabled: needsGrant && view === "checking", }); const connection = accountQuery.data ?? null; const started = useRef(false); const observedAt = useRef(0); const posted = useRef(false); const alive = useRef(true); const target = useRef(null); const controller = useRef(null); const request = (url: string, options: RequestInit, timeout = 15_000) => fetch(url, { credentials: "same-origin", cache: "no-store", ...options, signal: AbortSignal.any([ controller.current!.signal, AbortSignal.timeout(timeout), ]), }); const beginUpdate = async () => { const value = detail!; if (["updating", "installing"].includes(value.installation.status)) { target.current = value.installation.desiredRelease; return setView("updating"); } if (value.installation.status === "failed") return setView("failed"); if (!value.latestRelease) return setView("error"); if (sameRelease(value.installation.installedRelease, value.latestRelease)) return setView("current"); if ( !value.installation.installedRelease || !value.upgradeFrom.includes( value.installation.installedRelease.artifactDigest, ) ) return setView("unsupported"); const connected = connection!; if ( !connected.accounts.some( (item) => item.id === value.installation.accountId, ) ) return setView("select"); if (connected.selectedAccountId !== value.installation.accountId) { const selected = await request("/api/account", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ accountId: value.installation.accountId, }).toString(), }); const selectedData = await selected.json(); if (!alive.current || cache.getSnapshot().epoch !== epoch) return; if (selected.status === 401) return setView("reconnect"); if (!selected.ok) return setView("select"); if (selectedData.ownerSubject !== cache.getSnapshot().ownerSubject) { cache.identify(selectedData.ownerSubject); return; } await cache.client.cancelQueries({ queryKey: connectionOptions(cache).queryKey, }); if (!alive.current || cache.getSnapshot().epoch !== epoch) return; cache.client.setQueryData(connectionOptions(cache).queryKey, (current) => current ? { ...current, selectedAccountId: value.installation.accountId } : current, ); } let intent: { requestId: string; target: ReleaseIdentity; ownerSubject: string; submitted?: boolean; } | null; try { intent = JSON.parse( sessionStorage.getItem(key(installationId)) ?? "null", ); } catch { intent = null; } if ( intent?.submitted && intent.ownerSubject === value.installation.ownerSubject && sameRelease(intent.target, value.installation.installedRelease) ) { // A completed saved target is no longer ambiguous, even if the owner // closed this page before observing success and another release shipped. sessionStorage.removeItem(key(installationId)); intent = null; } // Never turn a saved ambiguous operation into a different release request. if (intent?.ownerSubject !== value.installation.ownerSubject) intent = null; if (intent?.submitted) return setView("error"); if ( !intent || intent.ownerSubject !== value.installation.ownerSubject || !/^[a-f0-9]{32}$/.test(intent.requestId) || !sameRelease(intent.target, value.latestRelease) ) { intent = { requestId: requestId(), ownerSubject: value.installation.ownerSubject, target: Object.freeze({ ...value.latestRelease }), }; sessionStorage.setItem(key(installationId), JSON.stringify(intent)); } // An ambiguous previous submission is never automatically repeated. The // exact intent remains available for status reconciliation/manual recovery. if (posted.current) return; posted.current = true; target.current = intent.target; intent.submitted = true; sessionStorage.setItem(key(installationId), JSON.stringify(intent)); const upgraded = await request( `/api/installations/${installationId}/upgrade`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ requestId: intent.requestId, target: JSON.stringify(intent.target), }).toString(), }, 120_000, ); const result = await upgraded.json(); if (!alive.current || cache.getSnapshot().epoch !== epoch) return; if (upgraded.status === 401) return setView("reconnect"); if (!upgraded.ok) { return setView( [ "artifact_unavailable", "resource_conflict", "recovery_required", ].includes(result.error) ? "unsupported" : "error", ); } observedAt.current = query.dataUpdatedAt; setView("updating"); if (result.installation) { await cache.client.cancelQueries({ queryKey: installationOptions(cache, installationId).queryKey, }); if (!alive.current || cache.getSnapshot().epoch !== epoch) return; cache.client.setQueryData( installationOptions(cache, installationId).queryKey, (current) => current ? { ...current, installation: result.installation } : current, ); } }; useEffect(() => { alive.current = true; controller.current = new AbortController(); return () => { alive.current = false; controller.current?.abort(); }; }, [installationId]); useEffect(() => { if (started.current) return; const error = identityError ?? query.error ?? (needsGrant ? accountQuery.error : null); if (error || cache.getSnapshot().signedOut) { setView( cache.getSnapshot().signedOut || (error instanceof PublisherReadError && error.status === 401) ? "reconnect" : "error", ); return; } if (!detail || (needsGrant && !connection)) return; started.current = true; observedAt.current = query.dataUpdatedAt; void beginUpdate().catch(() => { if (alive.current && cache.getSnapshot().epoch === epoch) setView("error"); }); }, [detail, connection, query.error, accountQuery.error, identityError]); useEffect(() => { if ( view !== "updating" || !detail || query.dataUpdatedAt <= observedAt.current ) return; if (detail.installation.status === "ready") { if ( !target.current || !sameRelease(detail.installation.installedRelease, target.current) ) return setView("error"); sessionStorage.removeItem(key(installationId)); setView("success"); } else if (detail.installation.status === "failed") setView("failed"); }, [view, detail, query.dataUpdatedAt]); const returnUrl = detail?.installation.resources.runtimeOrigin ? new URL("/auth/login", detail.installation.resources.runtimeOrigin).href : null; useEffect(() => { if (view !== "success" || !returnUrl) return; const timer = setTimeout(() => location.assign(returnUrl), 2000); return () => clearTimeout(timer); }, [view, returnUrl]); const text: Record = { checking: "Checking this installation and the latest compatible release…", updating: "Updating Flarebot… You will return to your instance when it is ready.", success: "Flarebot was updated successfully.", current: "This installation is already on the latest release.", unsupported: "This pre-release installation cannot be updated safely. Contact the publisher for manual recovery. Your existing resources and data have not been reset.", failed: "The previous installation operation failed. Automatic retry is disabled; reconnect and use the manual recovery controls.", reconnect: "Your deployment authorization expired. Reconnect Cloudflare and select this installation’s account, then return here.", select: "Reconnect Cloudflare or select the account that owns this installation before updating.", error: "The update status is unavailable. Reload to check the saved request; no automatic retry will be made.", }; return (

Update status

{returnUrl && ["success", "current"].includes(view) && ( {view === "success" ? "Return to Flarebot" : "Open Flarebot"} )}

{text[view]}

{view === "updating" && query.error && detail && (

Could not refresh update status. The last available progress is shown; reconnect to check again.

)} {["checking", "updating", "success", "current", "failed"].includes( view, ) && ( )}
{["reconnect", "select"].includes(view) && (
)}
{view === "select" && connection && (

Required account: {detail?.installation.accountId}

)}
); }