Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
10.0 kB · 272 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273import { ControlPlaneLayout } from "./ControlPlaneLayout.tsx";import { useEffect, useRef, useState } from "octane";import { Button } from "octane-kumo/components/button";import { LayerCard } from "octane-kumo/components/layer-card";import type { Installation, ReleaseIdentity,} from "../installation-metadata.ts";import { sameRelease } from "../installation-metadata.ts";
type View = | "checking" | "select" | "updating" | "success" | "current" | "unsupported" | "failed" | "reconnect" | "error";interface Detail { installation: Installation; latestRelease: ReleaseIdentity | null; upgradeFrom: string[];}interface Connection { accounts: { id: string; name: string }[]; selectedAccountId: string | null;}const key = (id: string) => `flarebot-update-${id}`;const requestId = () => crypto.randomUUID().replaceAll("-", "");
export function UpdateInstallation({ installationId,}: { installationId: string;}) { const [view, setView] = useState<View>("checking"); const [detail, setDetail] = useState<Detail | null>(null); const [connection, setConnection] = useState<Connection | null>(null); const posted = useRef(false); const alive = useRef(true); const target = useRef<ReleaseIdentity | null>(null); const controller = useRef<AbortController | null>(null); const request = (url: string, options: RequestInit = {}) => fetch(url, { credentials: "same-origin", cache: "no-store", ...options, signal: AbortSignal.any([ controller.current!.signal, AbortSignal.timeout(15_000), ]), }); const load = async () => { const response = await request(`/api/installations/${installationId}`); if (response.status === 401) return setView("reconnect"); if (!response.ok) return setView("error"); const value = (await response.json()) as Detail; if (!alive.current) return; setDetail(value); 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 account = await request("/api/connection"); if (account.status === 401) return setView("reconnect"); if (!account.ok) return setView("error"); const connected = (await account.json()) as Connection; setConnection(connected); 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(), }); if (selected.status === 401) return setView("reconnect"); if (!selected.ok) return setView("select"); } 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?.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(), }, ); if (upgraded.status === 401) return setView("reconnect"); if (!upgraded.ok) { const result = await upgraded.json(); return setView( [ "artifact_unavailable", "resource_conflict", "recovery_required", ].includes(result.error) ? "unsupported" : "error", ); } setView("updating"); }; useEffect(() => { alive.current = true; controller.current = new AbortController(); void load().catch(() => setView("error")); return () => { alive.current = false; controller.current?.abort(); }; }, [installationId]); useEffect(() => { if (view !== "updating") return; let polling = false; const timer = setInterval(() => { if (polling) return; polling = true; void request(`/api/installations/${installationId}`) .then(async (r) => { if (r.status === 401) return setView("reconnect"); if (!r.ok) return setView("error"); const value = (await r.json()) as Detail; if (!alive.current) return; setDetail(value); if (value.installation.status === "ready") { if ( !target.current || !sameRelease(value.installation.installedRelease, target.current) ) return setView("error"); sessionStorage.removeItem(key(installationId)); setView("success"); } else if (value.installation.status === "failed") setView("failed"); }) .catch(() => { if (alive.current) setView("error"); }) .finally(() => { polling = false; }); }, 1500); return () => clearInterval(timer); }, [view, installationId]); 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<View, string> = { checking: "Checking this installation and the latest compatible release…", updating: `Updating Flarebot… ${detail && ["installing", "updating"].includes(detail.installation.status) ? detail.installation.progress : "preparing"}. 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 ( <ControlPlaneLayout title="Update your Flarebot" description="Install a compatible release in your Cloudflare account and follow its progress." page="Update installation" > <LayerCard className="connect-card"> <div className="intro"> <h2>Update status</h2> </div> <p role={view === "error" || view === "failed" ? "alert" : "status"}> {text[view]} </p> <div className="actions"> {returnUrl && ["success", "current"].includes(view) && ( <Button onClick={() => location.assign(returnUrl)}> {view === "success" ? "Return to Flarebot" : "Open Flarebot"} </Button> )} {["reconnect", "select"].includes(view) && ( <form action="/auth/start" method="post"> <Button type="submit">Reconnect Cloudflare</Button> </form> )} <Button variant="secondary" onClick={() => location.assign("/connect")} > Installation settings </Button> </div> {view === "select" && connection && ( <p>Required account: {detail?.installation.accountId}</p> )} </LayerCard> </ControlPlaneLayout> );}