Files
openwork/packages/web/components/cloud-control.tsx

1707 lines
56 KiB
TypeScript

"use client";
import { FormEvent, useEffect, useState } from "react";
type Step = 1 | 2;
type AuthMode = "sign-in" | "sign-up";
type ShellView = "workers" | "billing";
type WorkerStatusBucket = "ready" | "starting" | "attention" | "other";
type AuthUser = {
id: string;
email: string;
name: string | null;
};
type WorkerLaunch = {
workerId: string;
workerName: string;
status: string;
provider: string | null;
instanceUrl: string | null;
openworkUrl: string | null;
workspaceId: string | null;
clientToken: string | null;
hostToken: string | null;
};
type WorkerSummary = {
workerId: string;
workerName: string;
status: string;
instanceUrl: string | null;
provider: string | null;
isMine: boolean;
};
type WorkerTokens = {
clientToken: string | null;
hostToken: string | null;
openworkUrl: string | null;
workspaceId: string | null;
};
type WorkerListItem = {
workerId: string;
workerName: string;
status: string;
instanceUrl: string | null;
provider: string | null;
isMine: boolean;
createdAt: string | null;
};
type EventLevel = "info" | "success" | "warning" | "error";
type LaunchEvent = {
id: string;
level: EventLevel;
label: string;
detail: string;
at: string;
};
const LAST_WORKER_STORAGE_KEY = "openwork:web:last-worker";
const WORKER_STATUS_POLL_MS = 5000;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function shortValue(value: string): string {
if (value.length <= 18) {
return value;
}
return `${value.slice(0, 8)}...${value.slice(-6)}`;
}
function getErrorMessage(payload: unknown, fallback: string): string {
if (typeof payload === "string" && payload.trim().length > 0) {
const trimmed = payload.trim();
const lower = trimmed.toLowerCase();
if (lower.startsWith("<!doctype") || lower.startsWith("<html") || lower.includes("<body")) {
return `${fallback} Upstream returned an HTML error page.`;
}
if (trimmed.length > 240) {
return `${fallback} Upstream returned a non-JSON error payload.`;
}
return trimmed;
}
if (!isRecord(payload)) {
return fallback;
}
const message = payload.message;
if (typeof message === "string" && message.trim().length > 0) {
return message;
}
const error = payload.error;
if (typeof error === "string" && error.trim().length > 0) {
return error;
}
return fallback;
}
function getUser(payload: unknown): AuthUser | null {
if (!isRecord(payload) || !isRecord(payload.user)) {
return null;
}
const user = payload.user;
if (typeof user.id !== "string" || typeof user.email !== "string") {
return null;
}
return {
id: user.id,
email: user.email,
name: typeof user.name === "string" ? user.name : null
};
}
function getToken(payload: unknown): string | null {
if (!isRecord(payload)) {
return null;
}
return typeof payload.token === "string" ? payload.token : null;
}
function getCheckoutUrl(payload: unknown): string | null {
if (!isRecord(payload) || !isRecord(payload.polar)) {
return null;
}
return typeof payload.polar.checkoutUrl === "string" ? payload.polar.checkoutUrl : null;
}
function getWorker(payload: unknown): WorkerLaunch | null {
if (!isRecord(payload) || !isRecord(payload.worker)) {
return null;
}
const worker = payload.worker;
if (typeof worker.id !== "string" || typeof worker.name !== "string") {
return null;
}
const instance = isRecord(payload.instance) ? payload.instance : null;
const tokens = isRecord(payload.tokens) ? payload.tokens : null;
return {
workerId: worker.id,
workerName: worker.name,
status: typeof worker.status === "string" ? worker.status : "unknown",
provider: instance && typeof instance.provider === "string" ? instance.provider : null,
instanceUrl: instance && typeof instance.url === "string" ? instance.url : null,
openworkUrl: instance && typeof instance.url === "string" ? instance.url : null,
workspaceId: null,
clientToken: tokens && typeof tokens.client === "string" ? tokens.client : null,
hostToken: tokens && typeof tokens.host === "string" ? tokens.host : null
};
}
function getWorkerSummary(payload: unknown): WorkerSummary | null {
if (!isRecord(payload) || !isRecord(payload.worker)) {
return null;
}
const worker = payload.worker;
if (typeof worker.id !== "string" || typeof worker.name !== "string") {
return null;
}
const instance = isRecord(payload.instance) ? payload.instance : null;
return {
workerId: worker.id,
workerName: worker.name,
status: typeof worker.status === "string" ? worker.status : "unknown",
instanceUrl: instance && typeof instance.url === "string" ? instance.url : null,
provider: instance && typeof instance.provider === "string" ? instance.provider : null,
isMine: worker.isMine === true
};
}
function getWorkerTokens(payload: unknown): WorkerTokens | null {
if (!isRecord(payload) || !isRecord(payload.tokens)) {
return null;
}
const tokens = payload.tokens;
const connect = isRecord(payload.connect) ? payload.connect : null;
const clientToken = typeof tokens.client === "string" ? tokens.client : null;
const hostToken = typeof tokens.host === "string" ? tokens.host : null;
const openworkUrl = connect && typeof connect.openworkUrl === "string" ? connect.openworkUrl : null;
const workspaceId = connect && typeof connect.workspaceId === "string" ? connect.workspaceId : null;
if (!clientToken && !hostToken) {
return null;
}
return { clientToken, hostToken, openworkUrl, workspaceId };
}
function parseWorkerListItem(value: unknown): WorkerListItem | null {
if (!isRecord(value)) {
return null;
}
const workerId = value.id;
const workerName = value.name;
if (typeof workerId !== "string" || typeof workerName !== "string") {
return null;
}
const instance = isRecord(value.instance) ? value.instance : null;
const createdAt = typeof value.createdAt === "string" ? value.createdAt : null;
return {
workerId,
workerName,
status: typeof value.status === "string" ? value.status : "unknown",
instanceUrl: instance && typeof instance.url === "string" ? instance.url : null,
provider: instance && typeof instance.provider === "string" ? instance.provider : null,
isMine: value.isMine === true,
createdAt
};
}
function getWorkersList(payload: unknown): WorkerListItem[] {
if (!isRecord(payload) || !Array.isArray(payload.workers)) {
return [];
}
const rows: WorkerListItem[] = [];
for (const item of payload.workers) {
const parsed = parseWorkerListItem(item);
if (parsed) {
rows.push(parsed);
}
}
return rows;
}
function getWorkerStatusMeta(status: string): { label: string; bucket: WorkerStatusBucket } {
const normalized = status.trim().toLowerCase();
if (normalized === "healthy" || normalized === "ready") {
return { label: "Ready", bucket: "ready" };
}
if (normalized === "provisioning" || normalized === "starting") {
return { label: "Starting", bucket: "starting" };
}
if (normalized === "failed" || normalized === "suspended") {
return { label: "Needs attention", bucket: "attention" };
}
return { label: "Unknown", bucket: "other" };
}
function getWorkerStatusCopy(status: string): string {
const normalized = status.trim().toLowerCase();
switch (normalized) {
case "provisioning":
case "starting":
return "Starting... This may take a minute.";
case "healthy":
case "ready":
return "Ready to connect.";
case "failed":
return "Worker failed to start.";
case "suspended":
return "Worker is suspended.";
default:
return "Worker status unknown.";
}
}
function isWorkerLaunch(value: unknown): value is WorkerLaunch {
if (!isRecord(value)) {
return false;
}
return (
typeof value.workerId === "string" &&
typeof value.workerName === "string" &&
typeof value.status === "string" &&
(typeof value.provider === "string" || value.provider === null) &&
(typeof value.instanceUrl === "string" || value.instanceUrl === null) &&
(typeof value.openworkUrl === "string" || value.openworkUrl === null || typeof value.openworkUrl === "undefined") &&
(typeof value.workspaceId === "string" || value.workspaceId === null || typeof value.workspaceId === "undefined") &&
(typeof value.clientToken === "string" || value.clientToken === null) &&
(typeof value.hostToken === "string" || value.hostToken === null)
);
}
function listItemToWorker(item: WorkerListItem, current: WorkerLaunch | null = null): WorkerLaunch {
return {
workerId: item.workerId,
workerName: item.workerName,
status: item.status,
provider: item.provider,
instanceUrl: item.instanceUrl,
openworkUrl: item.instanceUrl,
workspaceId: null,
clientToken: current?.workerId === item.workerId ? current.clientToken : null,
hostToken: current?.workerId === item.workerId ? current.hostToken : null
};
}
function normalizeUrl(value: string): string {
return value.trim().replace(/\/+$/, "");
}
function parseWorkspaceIdFromUrl(value: string): string | null {
const normalized = normalizeUrl(value);
if (!normalized) {
return null;
}
try {
const url = new URL(normalized);
const segments = url.pathname.split("/").filter(Boolean);
const last = segments[segments.length - 1] ?? "";
const prev = segments[segments.length - 2] ?? "";
if (prev !== "w" || !last) {
return null;
}
return decodeURIComponent(last);
} catch {
const match = normalized.match(/\/w\/([^/?#]+)/);
if (!match?.[1]) {
return null;
}
try {
return decodeURIComponent(match[1]);
} catch {
return match[1];
}
}
}
function buildWorkspaceUrl(instanceUrl: string, workspaceId: string): string {
return `${normalizeUrl(instanceUrl)}/w/${encodeURIComponent(workspaceId)}`;
}
function buildOpenworkDeepLink(
openworkUrl: string | null,
accessToken: string | null,
workerId: string | null,
workerName: string | null,
): string | null {
if (!openworkUrl || !accessToken) {
return null;
}
const params = new URLSearchParams({
openworkHostUrl: openworkUrl,
openworkToken: accessToken,
source: "openwork-web"
});
if (workerId) {
params.set("workerId", workerId);
}
if (workerName) {
params.set("workerName", workerName);
}
return `openwork://connect-remote?${params.toString()}`;
}
function parseWorkspaceIdFromWorkspacesPayload(payload: unknown): string | null {
if (!isRecord(payload) || !Array.isArray(payload.items)) {
return null;
}
const activeId = typeof payload.activeId === "string" ? payload.activeId : null;
if (activeId && payload.items.some((item) => isRecord(item) && item.id === activeId)) {
return activeId;
}
for (const item of payload.items) {
if (isRecord(item) && typeof item.id === "string" && item.id.trim()) {
return item.id;
}
}
return null;
}
async function requestAbsoluteJson(url: string, init: RequestInit = {}, timeoutMs = 12000) {
const headers = new Headers(init.headers);
headers.set("Accept", "application/json");
const shouldAttachTimeout = !init.signal && timeoutMs > 0;
const timeoutController = shouldAttachTimeout ? new AbortController() : null;
const timeoutHandle = timeoutController
? setTimeout(() => {
timeoutController.abort();
}, timeoutMs)
: null;
let response: Response;
try {
response = await fetch(url, {
...init,
headers,
credentials: "omit",
signal: init.signal ?? timeoutController?.signal
});
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
const text = await response.text();
let payload: unknown = null;
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
}
return { response, payload };
}
async function resolveOpenworkWorkspaceUrl(instanceUrl: string, accessToken: string): Promise<{ workspaceId: string; openworkUrl: string } | null> {
const baseUrl = normalizeUrl(instanceUrl);
const token = accessToken.trim();
if (!baseUrl || !token) {
return null;
}
const mountedWorkspaceId = parseWorkspaceIdFromUrl(baseUrl);
if (mountedWorkspaceId) {
return {
workspaceId: mountedWorkspaceId,
openworkUrl: baseUrl
};
}
const { response, payload } = await requestAbsoluteJson(`${baseUrl}/workspaces`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
return null;
}
const workspaceId = parseWorkspaceIdFromWorkspacesPayload(payload);
if (!workspaceId) {
return null;
}
return {
workspaceId,
openworkUrl: buildWorkspaceUrl(baseUrl, workspaceId)
};
}
async function requestJson(path: string, init: RequestInit = {}, timeoutMs = 30000) {
const headers = new Headers(init.headers);
headers.set("Accept", "application/json");
if (init.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const shouldAttachTimeout = !init.signal && timeoutMs > 0;
const timeoutController = shouldAttachTimeout ? new AbortController() : null;
const timeoutHandle = timeoutController
? setTimeout(() => {
timeoutController.abort();
}, timeoutMs)
: null;
let response: Response;
try {
response = await fetch(`/api/den${path}`, {
...init,
headers,
credentials: "include",
signal: init.signal ?? timeoutController?.signal
});
} finally {
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
}
const text = await response.text();
let payload: unknown = null;
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = text;
}
}
return { response, payload, text };
}
function CredentialRow({
label,
value,
placeholder,
canCopy,
copied,
onCopy
}: {
label: string;
value: string | null;
placeholder: string;
canCopy: boolean;
copied: boolean;
onCopy: () => void;
}) {
return (
<label className="ow-field-block">
<span className="ow-field-label">{label}</span>
<div className="ow-copy-row">
<input readOnly value={value ?? placeholder} className="ow-input ow-mono" onClick={(event) => event.currentTarget.select()} />
<button type="button" className="ow-btn-icon" disabled={!canCopy} onClick={onCopy}>
{copied ? "Copied" : canCopy ? "Copy" : "N/A"}
</button>
</div>
</label>
);
}
export function CloudControlPanel() {
const [step, setStep] = useState<Step>(1);
const [shellView, setShellView] = useState<ShellView>("workers");
const [authMode, setAuthMode] = useState<AuthMode>("sign-in");
const [name, setName] = useState("OpenWork Builder");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [authBusy, setAuthBusy] = useState(false);
const [authInfo, setAuthInfo] = useState("Sign in to launch and manage cloud workers.");
const [authError, setAuthError] = useState<string | null>(null);
const [user, setUser] = useState<AuthUser | null>(null);
const [authToken, setAuthToken] = useState<string | null>(null);
const [workerName, setWorkerName] = useState("Founder Ops Pilot");
const [worker, setWorker] = useState<WorkerLaunch | null>(null);
const [workerLookupId, setWorkerLookupId] = useState("");
const [workers, setWorkers] = useState<WorkerListItem[]>([]);
const [workersBusy, setWorkersBusy] = useState(false);
const [workersError, setWorkersError] = useState<string | null>(null);
const [launchBusy, setLaunchBusy] = useState(false);
const [actionBusy, setActionBusy] = useState<"status" | "token" | null>(null);
const [launchStatus, setLaunchStatus] = useState("Name your worker and click launch.");
const [launchError, setLaunchError] = useState<string | null>(null);
const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null);
const [paymentReturned, setPaymentReturned] = useState(false);
const [events, setEvents] = useState<LaunchEvent[]>([]);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [tokenFetchedForWorkerId, setTokenFetchedForWorkerId] = useState<string | null>(null);
const [workerQuery, setWorkerQuery] = useState("");
const [workerStatusFilter, setWorkerStatusFilter] = useState<WorkerStatusBucket | "all">("all");
const [showLaunchForm, setShowLaunchForm] = useState(false);
const selectedWorker = workers.find((item) => item.workerId === workerLookupId) ?? null;
const activeWorker: WorkerLaunch | null =
worker && workerLookupId === worker.workerId
? worker
: selectedWorker
? listItemToWorker(selectedWorker, worker)
: worker;
const progressWidth = step === 1 ? "45%" : "100%";
const isShellStep = step === 2;
const openworkConnectUrl = activeWorker?.openworkUrl ?? activeWorker?.instanceUrl ?? null;
const hasWorkspaceScopedUrl = Boolean(openworkConnectUrl && /\/w\/[^/?#]+/.test(openworkConnectUrl));
const openworkDeepLink = buildOpenworkDeepLink(
openworkConnectUrl,
activeWorker?.clientToken ?? null,
activeWorker?.workerId ?? null,
activeWorker?.workerName ?? null,
);
const filteredWorkers = workers.filter((item) => {
const query = workerQuery.trim().toLowerCase();
const matchesQuery =
!query ||
item.workerName.toLowerCase().includes(query) ||
item.workerId.toLowerCase().includes(query);
if (!matchesQuery) {
return false;
}
if (workerStatusFilter === "all") {
return true;
}
return getWorkerStatusMeta(item.status).bucket === workerStatusFilter;
});
const selectedWorkerStatus = activeWorker?.status ?? selectedWorker?.status ?? "unknown";
const selectedStatusMeta = getWorkerStatusMeta(selectedWorkerStatus);
function appendEvent(level: EventLevel, label: string, detail: string) {
setEvents((current) => {
const next: LaunchEvent[] = [
{
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
level,
label,
detail,
at: new Date().toISOString()
},
...current
];
return next.slice(0, 10);
});
}
async function withResolvedOpenworkCredentials(candidate: WorkerLaunch, options: { quiet?: boolean } = {}) {
const existingConnectUrl = candidate.openworkUrl?.trim() ?? "";
const existingWorkspaceId = candidate.workspaceId?.trim() ?? "";
if (existingConnectUrl && existingWorkspaceId) {
return {
...candidate,
openworkUrl: existingConnectUrl,
workspaceId: existingWorkspaceId
};
}
const instanceUrl = candidate.instanceUrl?.trim() ?? "";
if (!instanceUrl) {
return {
...candidate,
openworkUrl: null,
workspaceId: null
};
}
const accessToken = candidate.clientToken?.trim() ?? "";
if (!accessToken) {
const mountedWorkspaceId = parseWorkspaceIdFromUrl(instanceUrl);
return {
...candidate,
openworkUrl: normalizeUrl(instanceUrl),
workspaceId: mountedWorkspaceId
};
}
try {
const resolved = await resolveOpenworkWorkspaceUrl(instanceUrl, accessToken);
if (resolved) {
return {
...candidate,
openworkUrl: resolved.openworkUrl,
workspaceId: resolved.workspaceId
};
}
} catch {
if (!options.quiet) {
appendEvent("warning", "Credential hint", "Could not resolve /w/ws_ URL yet. Using host URL fallback.");
}
}
return {
...candidate,
openworkUrl: normalizeUrl(instanceUrl),
workspaceId: parseWorkspaceIdFromUrl(instanceUrl)
};
}
async function refreshWorkers(options: { keepSelection?: boolean } = {}) {
if (!user) {
setWorkers([]);
setWorkersError(null);
return;
}
setWorkersBusy(true);
setWorkersError(null);
try {
const { response, payload } = await requestJson("/v1/workers?limit=20", {
method: "GET",
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined
});
if (!response.ok) {
const message = getErrorMessage(payload, `Failed to load workers (${response.status}).`);
setWorkersError(message);
return;
}
const nextWorkers = getWorkersList(payload);
setWorkers(nextWorkers);
const currentSelection = options.keepSelection ? workerLookupId : "";
const nextSelectedId =
currentSelection && nextWorkers.some((item) => item.workerId === currentSelection)
? currentSelection
: nextWorkers[0]?.workerId ?? "";
setWorkerLookupId(nextSelectedId);
if (nextSelectedId && worker && worker.workerId === nextSelectedId) {
const selected = nextWorkers.find((item) => item.workerId === nextSelectedId) ?? null;
if (selected) {
setWorker((current) => listItemToWorker(selected, current));
}
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown network error";
setWorkersError(message);
} finally {
setWorkersBusy(false);
}
}
async function copyToClipboard(field: string, value: string | null) {
if (!value) {
return;
}
await navigator.clipboard.writeText(value);
setCopiedField(field);
setTimeout(() => {
setCopiedField((current) => (current === field ? null : current));
}, 1800);
}
async function refreshSession(quiet = false) {
const headers = new Headers();
if (authToken) {
headers.set("Authorization", `Bearer ${authToken}`);
}
const { response, payload } = await requestJson("/v1/me", { method: "GET", headers }, 12000);
if (!response.ok) {
setUser(null);
if (!quiet) {
setAuthError("No active session found. Sign in first.");
}
return null;
}
const sessionUser = getUser(payload);
if (!sessionUser) {
if (!quiet) {
setAuthError("Session response did not include a user.");
}
return null;
}
setUser(sessionUser);
setAuthInfo(`Signed in as ${sessionUser.email}.`);
return sessionUser;
}
useEffect(() => {
void refreshSession(true);
}, []);
useEffect(() => {
if (!user) {
setWorkers([]);
setWorkersError(null);
return;
}
void refreshWorkers();
}, [user?.id, authToken]);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const params = new URLSearchParams(window.location.search);
const customerSessionToken = params.get("customer_session_token");
if (!customerSessionToken) {
return;
}
setPaymentReturned(true);
setCheckoutUrl(null);
setLaunchStatus("Checkout return detected. Click launch to continue worker provisioning.");
appendEvent("success", "Returned from checkout", `Session ${shortValue(customerSessionToken)}`);
params.delete("customer_session_token");
const nextQuery = params.toString();
const nextUrl = nextQuery ? `${window.location.pathname}?${nextQuery}` : window.location.pathname;
window.history.replaceState({}, "", nextUrl);
}, []);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const raw = window.localStorage.getItem(LAST_WORKER_STORAGE_KEY);
if (!raw) {
return;
}
try {
const parsed = JSON.parse(raw) as unknown;
if (!isWorkerLaunch(parsed)) {
return;
}
const restored: WorkerLaunch = {
...parsed,
openworkUrl: parsed.openworkUrl ?? parsed.instanceUrl,
workspaceId: parsed.workspaceId ?? parseWorkspaceIdFromUrl(parsed.instanceUrl ?? ""),
clientToken: null,
hostToken: null
};
setWorker(restored);
setWorkerLookupId(restored.workerId);
setLaunchStatus(`Recovered worker ${restored.workerName}. ${getWorkerStatusCopy(restored.status)}`);
appendEvent("info", "Recovered worker context", `Worker ID ${restored.workerId}`);
} catch {
return;
}
}, []);
useEffect(() => {
if (typeof window === "undefined" || !worker) {
return;
}
const serializable: WorkerLaunch = {
...worker,
clientToken: null,
hostToken: null
};
window.localStorage.setItem(LAST_WORKER_STORAGE_KEY, JSON.stringify(serializable));
}, [worker]);
useEffect(() => {
if (user || checkoutUrl || paymentReturned || worker) {
setStep(2);
return;
}
setStep(1);
}, [worker, user, checkoutUrl, paymentReturned]);
useEffect(() => {
if (step !== 2) {
return;
}
if (workers.length === 0) {
setShowLaunchForm(true);
}
}, [step, workers.length]);
useEffect(() => {
if (!user || !worker) {
return;
}
if (worker.clientToken) {
return;
}
if (actionBusy !== null || launchBusy) {
return;
}
if (tokenFetchedForWorkerId === worker.workerId) {
return;
}
setTokenFetchedForWorkerId(worker.workerId);
void handleGenerateKey();
}, [actionBusy, launchBusy, tokenFetchedForWorkerId, user, worker]);
useEffect(() => {
if (!user || !worker || worker.status !== "provisioning") {
return;
}
if (actionBusy !== null || launchBusy) {
return;
}
let cancelled = false;
const poll = async () => {
if (cancelled) {
return;
}
await handleCheckStatus({ workerId: worker.workerId, quiet: true, background: true });
};
void poll();
const interval = window.setInterval(() => {
void poll();
}, WORKER_STATUS_POLL_MS);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [actionBusy, authToken, launchBusy, user?.id, worker?.workerId, worker?.status]);
async function handleAuthSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setAuthBusy(true);
setAuthError(null);
try {
const endpoint = authMode === "sign-up" ? "/api/auth/sign-up/email" : "/api/auth/sign-in/email";
const body =
authMode === "sign-up"
? {
name: name.trim() || "OpenWork Builder",
email: email.trim(),
password
}
: {
email: email.trim(),
password
};
const { response, payload } = await requestJson(endpoint, {
method: "POST",
body: JSON.stringify(body)
});
if (!response.ok) {
setAuthError(getErrorMessage(payload, `Authentication failed with ${response.status}.`));
return;
}
const token = getToken(payload);
if (token) {
setAuthToken(token);
}
const payloadUser = getUser(payload);
if (payloadUser) {
setUser(payloadUser);
setAuthInfo(`Signed in as ${payloadUser.email}.`);
appendEvent("success", authMode === "sign-up" ? "Account created" : "Signed in", payloadUser.email);
} else {
const refreshed = await refreshSession(true);
if (!refreshed) {
setAuthInfo("Authentication succeeded, but session details are still syncing.");
} else {
appendEvent("success", authMode === "sign-up" ? "Account created" : "Signed in", refreshed.email);
}
}
setStep(2);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown network error";
setAuthError(message);
} finally {
setAuthBusy(false);
}
}
async function handleSignOut() {
if (authBusy) {
return;
}
setAuthBusy(true);
setAuthError(null);
try {
await requestJson("/api/auth/sign-out", {
method: "POST",
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
body: JSON.stringify({})
});
} catch {
// Ignore sign-out transport issues and clear local session state anyway.
} finally {
setAuthBusy(false);
}
setUser(null);
setAuthToken(null);
setWorker(null);
setWorkers([]);
setWorkerLookupId("");
setWorkersError(null);
setLaunchError(null);
setCheckoutUrl(null);
setPaymentReturned(false);
setTokenFetchedForWorkerId(null);
setActionBusy(null);
setLaunchBusy(false);
setStep(1);
setShellView("workers");
setWorkerQuery("");
setWorkerStatusFilter("all");
setShowLaunchForm(false);
setAuthMode("sign-in");
setPassword("");
setAuthInfo("Sign in to launch and manage cloud workers.");
setLaunchStatus("Name your worker and click launch.");
setEvents([]);
if (typeof window !== "undefined") {
window.localStorage.removeItem(LAST_WORKER_STORAGE_KEY);
}
}
async function handleLaunchWorker() {
if (!user) {
setAuthError("Sign in before launching a worker.");
return;
}
setLaunchBusy(true);
setLaunchError(null);
setCheckoutUrl(null);
setLaunchStatus("Checking subscription and launch eligibility...");
appendEvent("info", "Launch requested", workerName.trim() || "Cloud worker");
try {
const { response, payload } = await requestJson(
"/v1/workers",
{
method: "POST",
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
body: JSON.stringify({
name: workerName.trim() || "Cloud Worker",
destination: "cloud"
})
},
12000
);
if (response.status === 402) {
const url = getCheckoutUrl(payload);
setCheckoutUrl(url);
setLaunchStatus("Payment is required. Complete checkout and return to continue launch.");
setLaunchError(url ? null : "Checkout URL missing from paywall response.");
appendEvent("warning", "Paywall required", url ? "Checkout URL generated" : "Checkout URL missing");
return;
}
if (!response.ok) {
const message = getErrorMessage(payload, `Launch failed with ${response.status}.`);
setLaunchError(message);
setLaunchStatus("Launch failed. Fix the error and retry.");
appendEvent("error", "Launch failed", message);
return;
}
const parsedWorker = getWorker(payload);
if (!parsedWorker) {
setLaunchError("Launch response was missing worker details.");
setLaunchStatus("Launch response format was unexpected.");
appendEvent("error", "Launch failed", "Worker payload missing");
return;
}
const resolvedWorker = await withResolvedOpenworkCredentials(parsedWorker);
setWorker(resolvedWorker);
setWorkerLookupId(parsedWorker.workerId);
setPaymentReturned(false);
setCheckoutUrl(null);
setShowLaunchForm(false);
if (resolvedWorker.status === "provisioning") {
setLaunchStatus("Provisioning started. This can take a few minutes, and we will keep checking automatically.");
appendEvent("info", "Provisioning started", `Worker ID ${parsedWorker.workerId}`);
} else {
setLaunchStatus(getWorkerStatusCopy(resolvedWorker.status));
appendEvent("success", "Worker launched", `Worker ID ${parsedWorker.workerId}`);
}
} catch (error) {
const message =
error instanceof DOMException && error.name === "AbortError"
? "Launch request took longer than expected. Provisioning can continue in the background. Refresh worker status below."
: error instanceof Error
? error.message
: "Unknown network error";
setLaunchError(message);
setLaunchStatus("Launch request failed.");
appendEvent("error", "Launch failed", message);
} finally {
setLaunchBusy(false);
void refreshWorkers({ keepSelection: true });
}
}
async function handleCheckStatus(options: { workerId?: string; quiet?: boolean; background?: boolean } = {}) {
const quiet = options.quiet === true;
const background = options.background === true;
if (!user) {
if (!quiet) {
setLaunchError("Sign in before checking worker status.");
}
return;
}
const fallbackId = workerLookupId.trim() || worker?.workerId || workers[0]?.workerId || "";
const id = options.workerId ?? fallbackId;
if (!id) {
if (!quiet) {
setLaunchError("No worker selected yet. Launch one first, then use this panel.");
}
return;
}
setWorkerLookupId(id);
if (!background) {
setActionBusy("status");
}
if (!quiet) {
setLaunchError(null);
}
try {
const { response, payload } = await requestJson(`/v1/workers/${encodeURIComponent(id)}`, {
method: "GET",
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined
});
if (!response.ok) {
const message = getErrorMessage(payload, `Status check failed with ${response.status}.`);
if (!quiet) {
setLaunchError(message);
appendEvent("error", "Status check failed", message);
}
return;
}
const summary = getWorkerSummary(payload);
if (!summary) {
if (!quiet) {
setLaunchError("Status response was missing worker details.");
appendEvent("error", "Status check failed", "Worker summary missing");
}
return;
}
const previousStatus = worker?.workerId === summary.workerId ? worker.status : null;
const nextWorker: WorkerLaunch =
worker && worker.workerId === summary.workerId
? {
...worker,
workerName: summary.workerName,
status: summary.status,
provider: summary.provider,
instanceUrl: summary.instanceUrl
}
: {
workerId: summary.workerId,
workerName: summary.workerName,
status: summary.status,
provider: summary.provider,
instanceUrl: summary.instanceUrl,
openworkUrl: summary.instanceUrl,
workspaceId: null,
clientToken: null,
hostToken: null
};
const resolvedWorker = await withResolvedOpenworkCredentials(nextWorker, { quiet: true });
setWorker(resolvedWorker);
setWorkerLookupId(summary.workerId);
if (!quiet) {
setLaunchStatus(`Worker ${summary.workerName} is currently ${summary.status}.`);
appendEvent("info", "Status refreshed", `${summary.workerName}: ${summary.status}`);
} else if (previousStatus && previousStatus !== summary.status) {
setLaunchStatus(getWorkerStatusCopy(summary.status));
if (summary.status === "healthy") {
appendEvent("success", "Provisioning complete", `${summary.workerName} is ready`);
} else if (summary.status === "failed") {
appendEvent("error", "Provisioning failed", `${summary.workerName} failed to provision`);
} else {
appendEvent("info", "Provisioning update", `${summary.workerName}: ${summary.status}`);
}
}
if (!background) {
void refreshWorkers({ keepSelection: true });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown network error";
if (!quiet) {
setLaunchError(message);
appendEvent("error", "Status check failed", message);
}
} finally {
if (!background) {
setActionBusy(null);
}
}
}
async function handleGenerateKey() {
if (!user) {
setLaunchError("Sign in before fetching a worker access token.");
return;
}
const id = workerLookupId.trim() || worker?.workerId || workers[0]?.workerId || "";
if (!id) {
setLaunchError("No worker selected yet. Launch one first, then fetch a token.");
return;
}
setWorkerLookupId(id);
setActionBusy("token");
setLaunchError(null);
try {
const { response, payload } = await requestJson(`/v1/workers/${encodeURIComponent(id)}/tokens`, {
method: "POST",
headers: authToken ? { Authorization: `Bearer ${authToken}` } : undefined,
body: JSON.stringify({})
});
if (!response.ok) {
const message = getErrorMessage(payload, `Token fetch failed with ${response.status}.`);
setLaunchError(message);
appendEvent("error", "Token fetch failed", message);
return;
}
const tokens = getWorkerTokens(payload);
if (!tokens) {
setLaunchError("Token response returned no token values.");
appendEvent("error", "Token fetch failed", "Missing token payload");
return;
}
const nextWorker: WorkerLaunch =
worker && worker.workerId === id
? {
...worker,
openworkUrl: tokens.openworkUrl ?? worker.openworkUrl,
workspaceId: tokens.workspaceId ?? worker.workspaceId,
clientToken: tokens.clientToken,
hostToken: tokens.hostToken
}
: {
workerId: id,
workerName: "Existing worker",
status: "unknown",
provider: null,
instanceUrl: null,
openworkUrl: tokens.openworkUrl,
workspaceId: tokens.workspaceId,
clientToken: tokens.clientToken,
hostToken: tokens.hostToken
};
const resolvedWorker = await withResolvedOpenworkCredentials(nextWorker, { quiet: true });
setWorker(resolvedWorker);
setLaunchStatus("Worker is ready to connect.");
appendEvent("success", "Access token ready", `Worker ID ${id}`);
void refreshWorkers({ keepSelection: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown network error";
setLaunchError(message);
appendEvent("error", "Token fetch failed", message);
} finally {
setActionBusy(null);
}
}
return (
<section className={`ow-card${isShellStep ? " ow-card-shell" : ""}`}>
{!isShellStep ? (
<div className="ow-progress-track">
<span className="ow-progress-fill" style={{ width: progressWidth }} />
</div>
) : null}
<div className="ow-card-body">
{step === 1 ? (
<div className="ow-stack">
<div className="ow-heading-block">
<span className="ow-icon-chip">01</span>
<h1 className="ow-title">Welcome back</h1>
<p className="ow-subtitle">Sign in to launch and manage cloud workers.</p>
</div>
<form className="ow-stack" onSubmit={handleAuthSubmit}>
{authMode === "sign-up" ? (
<label className="ow-field-block">
<span className="ow-field-label">Name</span>
<input
className="ow-input"
value={name}
onChange={(event) => setName(event.target.value)}
autoComplete="name"
required
/>
</label>
) : null}
<label className="ow-field-block">
<span className="ow-field-label">Email</span>
<input
className="ow-input"
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
autoComplete="email"
required
/>
</label>
<label className="ow-field-block">
<span className="ow-field-label">Password</span>
<input
className="ow-input"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
autoComplete={authMode === "sign-up" ? "new-password" : "current-password"}
required
/>
</label>
<button type="submit" className="ow-btn-primary" disabled={authBusy}>
{authBusy ? "Working..." : authMode === "sign-in" ? "Continue" : "Create account"}
</button>
</form>
<div className="ow-inline-row">
<p className="ow-caption">{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}</p>
<button
type="button"
className="ow-link"
onClick={() => setAuthMode((current) => (current === "sign-in" ? "sign-up" : "sign-in"))}
>
{authMode === "sign-in" ? "Create account" : "Switch to sign in"}
</button>
</div>
<div className="ow-note-box">
<p>{authInfo}</p>
{authError ? <p className="ow-error-text">{authError}</p> : null}
</div>
</div>
) : null}
{step === 2 ? (
<div className="ow-app-shell">
<aside className="ow-app-nav">
<div className="ow-nav-group">
<p className="ow-nav-label">Workspace</p>
<button
type="button"
className={`ow-nav-item ${shellView === "workers" ? "is-active" : ""}`}
onClick={() => setShellView("workers")}
>
Workers
</button>
<button
type="button"
className={`ow-nav-item ${shellView === "billing" ? "is-active" : ""}`}
onClick={() => setShellView("billing")}
>
Billing
</button>
</div>
<div className="ow-app-nav-footer">
<p className="ow-nav-label">Account</p>
<p className="ow-caption ow-nav-email">{(user?.email ?? email) || "account"}</p>
<button type="button" className="ow-link" onClick={() => void handleSignOut()} disabled={authBusy}>
{authBusy ? "Signing out..." : "Log out"}
</button>
</div>
</aside>
{shellView === "workers" ? (
<>
<section className="ow-pane ow-workers-pane">
<div className="ow-pane-head ow-pane-head-row">
<div>
<p className="ow-section-title">Workers</p>
<p className="ow-caption">Pick a worker to see details.</p>
</div>
<button type="button" className="ow-btn-secondary ow-btn-compact" onClick={() => setShowLaunchForm((current) => !current)}>
{showLaunchForm ? "Hide launch" : "Launch worker"}
</button>
</div>
{showLaunchForm ? (
<div className="ow-pane-block">
<label className="ow-field-block">
<span className="ow-field-label">Worker Name</span>
<input
className="ow-input"
value={workerName}
onChange={(event) => setWorkerName(event.target.value)}
maxLength={80}
/>
</label>
<button
type="button"
className="ow-btn-primary"
onClick={handleLaunchWorker}
disabled={!user || launchBusy || worker?.status === "provisioning"}
>
{launchBusy
? "Starting worker..."
: worker?.status === "provisioning"
? "Worker is starting..."
: `Launch "${workerName || "Cloud Worker"}"`}
</button>
{(launchStatus || launchError) && showLaunchForm ? (
<div className="ow-note-box">
<p>{launchStatus}</p>
{launchError ? <p className="ow-error-text">{launchError}</p> : null}
</div>
) : null}
{checkoutUrl ? (
<div className="ow-paywall-box">
<p className="ow-paywall-title">Payment needed before launch</p>
<a href={checkoutUrl} rel="noreferrer" className="ow-btn-secondary ow-full">
Continue to checkout
</a>
<p className="ow-caption">After payment, come back and click launch again.</p>
</div>
) : null}
</div>
) : null}
<div className="ow-filter-row">
<input
className="ow-input"
value={workerQuery}
onChange={(event) => setWorkerQuery(event.target.value)}
placeholder="Search workers"
aria-label="Search workers"
/>
<select
className="ow-select"
value={workerStatusFilter}
onChange={(event) => setWorkerStatusFilter(event.target.value as WorkerStatusBucket | "all")}
>
<option value="all">All statuses</option>
<option value="ready">Ready</option>
<option value="starting">Starting</option>
<option value="attention">Needs attention</option>
</select>
</div>
{workersBusy ? <p className="ow-caption">Loading workers...</p> : null}
{workersError ? <p className="ow-error-text">{workersError}</p> : null}
{filteredWorkers.length > 0 ? (
<ul className="ow-worker-list ow-worker-list-panel">
{filteredWorkers.map((item) => {
const meta = getWorkerStatusMeta(item.status);
return (
<li key={item.workerId}>
<button
type="button"
className={`ow-worker-select ${workerLookupId === item.workerId ? "is-active" : ""}`}
onClick={() => {
setWorkerLookupId(item.workerId);
setWorker((current) => listItemToWorker(item, current));
}}
>
<div className="ow-worker-head">
<div>
<p className="ow-step-title">{item.workerName}</p>
<p className="ow-step-detail">{meta.label}</p>
</div>
{item.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
</button>
</li>
);
})}
</ul>
) : null}
{workers.length > 0 && filteredWorkers.length === 0 ? (
<p className="ow-caption">No workers match this filter.</p>
) : null}
{workers.length === 0 && !workersBusy ? (
<p className="ow-caption">No workers yet. Launch one to get started.</p>
) : null}
</section>
<section className="ow-pane ow-detail-pane">
{selectedWorker ? (
<>
<div className="ow-pane-head">
<p className="ow-section-title">Overview</p>
<div className="ow-worker-head">
<div>
<p className="ow-step-title">{activeWorker?.workerName ?? selectedWorker.workerName}</p>
<p className="ow-step-detail">{selectedStatusMeta.label}</p>
</div>
{selectedWorker.isMine ? <span className="ow-badge">Yours</span> : null}
</div>
<p className="ow-caption">{getWorkerStatusCopy(selectedWorkerStatus)}</p>
</div>
<div className="ow-overview-grid">
<div className="ow-overview-card">
<p className="ow-overview-label">Status</p>
<p className={`ow-overview-value is-${selectedStatusMeta.bucket}`}>{selectedStatusMeta.label}</p>
</div>
<div className="ow-overview-card">
<p className="ow-overview-label">Connect</p>
<p className="ow-overview-value">{openworkDeepLink ? "One-click ready" : "Preparing"}</p>
</div>
</div>
<div className="ow-inline-actions">
<button
type="button"
className="ow-btn-primary ow-btn-primary-inline"
onClick={() => {
if (!openworkDeepLink) {
return;
}
window.location.href = openworkDeepLink;
}}
disabled={!openworkDeepLink || selectedStatusMeta.bucket !== "ready"}
>
{openworkDeepLink ? "Open in OpenWork" : "Preparing connection..."}
</button>
</div>
{!openworkDeepLink || !openworkConnectUrl || (!hasWorkspaceScopedUrl && openworkConnectUrl) ? (
<div className="ow-note-box">
{!openworkDeepLink ? <p className="ow-caption">Getting connection details ready...</p> : null}
{!openworkConnectUrl ? <p className="ow-caption">Keep this page open for a moment.</p> : null}
{!hasWorkspaceScopedUrl && openworkConnectUrl ? <p className="ow-caption">Finishing your workspace URL...</p> : null}
</div>
) : null}
<details className="ow-howto">
<summary>Copy details manually</summary>
<p className="ow-howto-copy">
If one-click open doesn't work, copy these into OpenWork: Add a worker &gt; Connect remote.
</p>
<CredentialRow
label="OpenWork worker URL"
value={openworkConnectUrl}
placeholder="URL appears once ready"
canCopy={Boolean(openworkConnectUrl)}
copied={copiedField === "openwork-url"}
onCopy={() => void copyToClipboard("openwork-url", openworkConnectUrl)}
/>
<CredentialRow
label="Access token"
value={activeWorker?.clientToken ?? null}
placeholder="Use more options to refresh"
canCopy={Boolean(activeWorker?.clientToken)}
copied={copiedField === "access-token"}
onCopy={() => void copyToClipboard("access-token", activeWorker?.clientToken ?? null)}
/>
<figure className="ow-connect-shot">
<img src="/connect-remote-menu.png" alt="OpenWork Add a worker menu with Connect remote option" />
</figure>
</details>
<details className="ow-howto">
<summary>More options</summary>
<div className="ow-inline-actions">
<button
type="button"
className="ow-btn-secondary"
onClick={() => void refreshWorkers({ keepSelection: true })}
disabled={workersBusy || actionBusy !== null}
>
{workersBusy ? "Refreshing..." : "Refresh list"}
</button>
<button
type="button"
className="ow-btn-secondary"
onClick={() => void handleCheckStatus({ workerId: selectedWorker.workerId })}
disabled={actionBusy !== null}
>
{actionBusy === "status" ? "Checking..." : "Check status"}
</button>
<button
type="button"
className="ow-btn-secondary"
onClick={handleGenerateKey}
disabled={actionBusy !== null}
>
{actionBusy === "token" ? "Fetching..." : "Refresh token"}
</button>
</div>
</details>
<details className="ow-howto">
<summary>Technical details</summary>
<CredentialRow
label="Worker host URL"
value={activeWorker?.instanceUrl ?? null}
placeholder="Host URL"
canCopy={Boolean(activeWorker?.instanceUrl)}
copied={copiedField === "worker-host-url"}
onCopy={() => void copyToClipboard("worker-host-url", activeWorker?.instanceUrl ?? null)}
/>
<CredentialRow
label="Worker ID"
value={(activeWorker?.workerId ?? workerLookupId) || null}
placeholder="Worker ID"
canCopy={Boolean(activeWorker?.workerId || workerLookupId)}
copied={copiedField === "worker-id"}
onCopy={() => void copyToClipboard("worker-id", (activeWorker?.workerId ?? workerLookupId) || null)}
/>
{events.length > 0 ? (
<div className="ow-log-box">
<p className="ow-section-title">Recent activity</p>
<ul className="ow-log-list">
{events.map((entry) => (
<li key={entry.id} className={`ow-log-item level-${entry.level}`}>
<div className="ow-log-head">
<span>{entry.label}</span>
<span className="ow-mono">{new Date(entry.at).toLocaleTimeString()}</span>
</div>
<p>{entry.detail}</p>
</li>
))}
</ul>
</div>
) : null}
</details>
</>
) : (
<div className="ow-empty-detail">
<p className="ow-section-title">Select a worker</p>
<p className="ow-caption">Pick a worker from the list to see details and connect.</p>
</div>
)}
</section>
</>
) : (
<section className="ow-pane ow-billing-pane ow-pane-span-2">
<div className="ow-pane-head">
<p className="ow-section-title">Billing</p>
<p className="ow-caption">Handle checkout when launching a new worker.</p>
</div>
{checkoutUrl ? (
<div className="ow-paywall-box">
<p className="ow-paywall-title">Checkout in progress</p>
<a href={checkoutUrl} rel="noreferrer" className="ow-btn-secondary ow-full">
Continue to checkout
</a>
</div>
) : (
<div className="ow-note-box">
<p>No payment action right now.</p>
<p className="ow-caption">Go to Workers and launch a worker when you're ready.</p>
</div>
)}
</section>
)}
</div>
) : null}
</div>
</section>
);
}