// Shim for CDN React const React = window.React; const ReactDOM = window.ReactDOM; const jsx = React.createElement; const jsxs = React.createElement; const Fragment = React.Fragment; "use strict"; (() => { // react-cdn:react-dom/client var hydrateRoot = ReactDOM.hydrateRoot; var client_default = ReactDOM; // react-cdn:react var react_default = React; var { useState, useEffect, useRef, createElement, Fragment } = React; // react-cdn:react/jsx-runtime var jsx = React.createElement; var jsxs = React.createElement; var Fragment2 = React.Fragment; // src/data-plane/components/IndexPage.tsx function GoogleIcon() { return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", children: [ /* @__PURE__ */ jsx("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), /* @__PURE__ */ jsx("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), /* @__PURE__ */ jsx("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), /* @__PURE__ */ jsx("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" }) ] }); } function DiscordIcon() { return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "#5865F2", children: /* @__PURE__ */ jsx("path", { d: "M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" }) }); } function VersionBadge({ sandbox }) { if (sandbox.imageRef === void 0) { return /* @__PURE__ */ jsx("span", { className: "version-badge unknown", title: "Version unknown", children: "unknown" }); } if (sandbox.isOutdated) { return /* @__PURE__ */ jsx("span", { className: "version-badge outdated", title: "Outdated version", children: "outdated" }); } return /* @__PURE__ */ jsx("span", { className: "version-badge current", title: "Up to date", children: "current" }); } function HealthDot({ status }) { const title = status === "checking" ? "Checking..." : status === "healthy" ? "Online" : "Offline"; return /* @__PURE__ */ jsx("span", { className: `health-dot ${status}`, title }); } function SandboxCard({ sandbox, scheme, baseDomain, portSuffix, session, onDelete, onReset, onRevive, deleting, reviving, healthStatus, localMode }) { const baseUrl = sandbox.tunnelUrl ? sandbox.tunnelUrl : `${scheme}://${sandbox.externalId}.${baseDomain}${portSuffix}`; const buildAuthUrl = (url) => { if (session?.access_token && session?.refresh_token) { const u = new URL(url); u.hash = `access_token=${encodeURIComponent(session.access_token)}&refresh_token=${encodeURIComponent(session.refresh_token)}&type=recovery`; return u.toString(); } return url; }; const needsRevive = healthStatus === "unhealthy" || sandbox.isOutdated; const isDisabled = healthStatus === "unhealthy"; return /* @__PURE__ */ jsx("div", { className: `sandbox${deleting ? " deleting" : ""}${reviving ? " reviving" : ""}`, children: /* @__PURE__ */ jsxs("div", { className: "sandbox-header", children: [ /* @__PURE__ */ jsxs("div", { className: "sandbox-name-group", children: [ /* @__PURE__ */ jsx( "a", { href: isDisabled ? void 0 : buildAuthUrl(baseUrl), className: `sandbox-name${isDisabled ? " disabled" : ""}`, children: sandbox.projectId } ), /* @__PURE__ */ jsx(HealthDot, { status: healthStatus }), sandbox.isOutdated && /* @__PURE__ */ jsx(VersionBadge, { sandbox }) ] }), /* @__PURE__ */ jsxs("div", { className: "sandbox-header-right", children: [ needsRevive && (session || localMode) && /* @__PURE__ */ jsx( "button", { className: "revive-btn", onClick: () => onRevive(sandbox), disabled: reviving, children: reviving ? "..." : "Revive" } ), (session || localMode) && /* @__PURE__ */ jsx( "button", { className: "delete-btn", onClick: () => localMode ? onReset(sandbox) : onDelete(sandbox.id), title: localMode ? "Reset" : "Delete", children: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: [ /* @__PURE__ */ jsx("path", { d: "M3 6h18" }), /* @__PURE__ */ jsx("path", { d: "M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6" }), /* @__PURE__ */ jsx("path", { d: "M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2" }) ] }) } ) ] }) ] }) }); } function AuthModal({ open, onClose, supabaseClient }) { const handleOAuth = async (provider) => { if (supabaseClient) { await supabaseClient.auth.signInWithOAuth({ provider, options: { redirectTo: window.location.href } }); } }; return /* @__PURE__ */ jsx("div", { className: `auth-modal${open ? " open" : ""}`, onClick: (e) => e.target === e.currentTarget && onClose(), children: /* @__PURE__ */ jsxs("div", { className: "auth-modal-content", children: [ /* @__PURE__ */ jsx("h2", { children: "Sign In" }), /* @__PURE__ */ jsxs("div", { className: "oauth-buttons", children: [ /* @__PURE__ */ jsxs("button", { className: "oauth-btn", onClick: () => handleOAuth("google"), children: [ /* @__PURE__ */ jsx(GoogleIcon, {}), "Continue with Google" ] }), /* @__PURE__ */ jsxs("button", { className: "oauth-btn", onClick: () => handleOAuth("discord"), children: [ /* @__PURE__ */ jsx(DiscordIcon, {}), "Continue with Discord" ] }) ] }) ] }) }); } function generateSuggestedName(existingIds) { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; for (let i = 0; i < 50; i++) { let name = ""; for (let j = 0; j < 6; j++) { name += chars[Math.floor(Math.random() * chars.length)]; } if (!existingIds.has(name)) return name; } return Math.random().toString(36).slice(2, 8); } function CreateProjectModal({ open, onClose, username, onSubmit, creating, existingProjectIds }) { const [projectId, setProjectId] = useState(""); const inputRef = useRef(null); useEffect(() => { if (open) { setProjectId(generateSuggestedName(existingProjectIds)); setTimeout(() => { inputRef.current?.focus(); inputRef.current?.select(); }, 0); } }, [open]); const handleSubmit = () => { const pid = projectId.trim().toLowerCase(); if (pid) { onSubmit(pid); } }; useEffect(() => { const handleKeyDown = (e) => { if (e.key === "Escape" && open) onClose(); }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [open, onClose]); return /* @__PURE__ */ jsx("div", { className: `modal-overlay${open ? " open" : ""}`, onClick: (e) => e.target === e.currentTarget && onClose(), children: /* @__PURE__ */ jsxs("div", { className: "modal", children: [ /* @__PURE__ */ jsx("h2", { children: "Create Project" }), /* @__PURE__ */ jsxs("div", { className: "modal-field", children: [ /* @__PURE__ */ jsx("label", { htmlFor: "projectIdInput", children: "Project Name" }), /* @__PURE__ */ jsx( "input", { ref: inputRef, type: "text", id: "projectIdInput", placeholder: "e.g., my-app, portfolio", autoComplete: "off", value: projectId, onChange: (e) => setProjectId(e.target.value), onKeyDown: (e) => e.key === "Enter" && handleSubmit() } ), /* @__PURE__ */ jsxs("div", { className: "hint", children: [ "Your project will be available at ", projectId || "project", "--", username, ".sandbox-test.websim.com" ] }) ] }), /* @__PURE__ */ jsxs("div", { className: "modal-actions", children: [ /* @__PURE__ */ jsx("button", { type: "button", className: "btn-secondary", onClick: onClose, disabled: creating, children: "Cancel" }), /* @__PURE__ */ jsx("button", { type: "button", className: "btn-primary", onClick: handleSubmit, disabled: creating || !projectId.trim(), children: creating ? "Creating..." : "Create" }) ] }) ] }) }); } function UserInfo({ user, onSignOut }) { const avatarUrl = user.user_metadata?.avatar_url; const name = user.user_metadata?.full_name || user.user_metadata?.name || user.email || "User"; const initial = name[0].toUpperCase(); return /* @__PURE__ */ jsxs("div", { className: "user-info", children: [ /* @__PURE__ */ jsx("div", { className: "user-avatar", children: avatarUrl ? /* @__PURE__ */ jsx("img", { src: avatarUrl, alt: "Avatar" }) : initial }), /* @__PURE__ */ jsx("span", { className: "user-email", children: user.email || "" }), /* @__PURE__ */ jsx("button", { className: "auth-btn secondary", onClick: onSignOut, children: "Sign Out" }) ] }); } function IndexPageApp(props2) { const { sandboxes: initialSandboxes, baseDomain, protocol, port, supabaseUrl, supabaseAnonKey, localMode } = props2; const hasSupabase = Boolean(supabaseUrl && supabaseAnonKey); const scheme = protocol.replace(":", ""); const portSuffix = port ? `:${port}` : ""; const [sandboxes, setSandboxes] = useState(initialSandboxes); const [session, setSession] = useState(null); const [authLoading, setAuthLoading] = useState(hasSupabase); const [authModalOpen, setAuthModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false); const [creating, setCreating] = useState(false); const [status, setStatus] = useState({ visible: false, content: "" }); const [deletingIds, setDeletingIds] = useState(/* @__PURE__ */ new Set()); const [revivingIds, setRevivingIds] = useState(/* @__PURE__ */ new Set()); const [healthStatus, setHealthStatus] = useState({}); const [supabaseClient, setSupabaseClient] = useState(null); useEffect(() => { if (hasSupabase && window.supabase) { const client = window.supabase.createClient(supabaseUrl, supabaseAnonKey); setSupabaseClient(client); client.auth.getSession().then(({ data: { session: session2 } }) => { setSession(session2); setAuthLoading(false); }); client.auth.onAuthStateChange((event, session2) => { setSession(session2); if (event === "SIGNED_IN") { setAuthModalOpen(false); } }); } else { setAuthLoading(false); } }, [hasSupabase, supabaseUrl, supabaseAnonKey]); const getAuthToken = () => session?.access_token || null; const websimUsername = session?.user?.user_metadata?.custom_claims?.global_name?.replace(/[^a-z0-9-]/g, "") || session?.user?.email?.split("@")[0]?.replace(/[^a-z0-9-]/g, "") || ""; console.log("websimUsername", websimUsername); console.log("session", session); const canCreate = Boolean(session && websimUsername); console.log("canCreate", canCreate); const userSandboxes = websimUsername ? sandboxes.filter((s) => s.username === websimUsername) : localMode ? sandboxes : []; useEffect(() => { if (userSandboxes.length === 0) return; const checkHealth = async (sandbox) => { try { const response = await fetch(`/api/sandboxes/${sandbox.id}/health`, { method: "GET", signal: AbortSignal.timeout(1e4) }); const data = await response.json(); setHealthStatus((prev) => ({ ...prev, [sandbox.id]: data.healthy ? "healthy" : "unhealthy" })); } catch { setHealthStatus((prev) => ({ ...prev, [sandbox.id]: "unhealthy" })); } }; const initialStatus = {}; userSandboxes.forEach((s) => { initialStatus[s.id] = "checking"; }); setHealthStatus(initialStatus); userSandboxes.forEach(checkHealth); const interval = setInterval(() => { userSandboxes.forEach(checkHealth); }, 3e4); return () => clearInterval(interval); }, [userSandboxes.map((s) => s.id).join(",")]); const handleRevive = async (sandbox) => { setRevivingIds((prev) => new Set(prev).add(sandbox.id)); try { const token = getAuthToken(); if (!token && !localMode) throw new Error("Sign in required"); try { await fetch(`/api/sandboxes/${sandbox.id}`, { method: "DELETE", headers: token ? { Authorization: `Bearer ${token}` } : {} }); } catch { } const res = await fetch("/api/sandboxes/acquire", { method: "POST", headers: { ...token ? { Authorization: `Bearer ${token}` } : {}, "Content-Type": "application/json" }, body: JSON.stringify({ projectId: sandbox.projectId, username: sandbox.username }) }); const data = await res.json(); const errorMsg = typeof data.error === "string" ? data.error : data.error?.message; if (!res.ok) throw new Error(errorMsg || "Failed"); if (data.state === "error") throw new Error(errorMsg || "Failed"); const projectUrl = window.location.port ? data.urls.host.replace(/(https?:\/\/[^/]+)/, `$1:${window.location.port}`) : data.urls.host; for (let i = 0; i < 40; i++) { try { const healthRes = await fetch(`${projectUrl}/health`, { signal: AbortSignal.timeout(3e3) }); if (healthRes.ok) { const authUrl = session?.access_token && session?.refresh_token ? `${projectUrl}#access_token=${encodeURIComponent(session.access_token)}&refresh_token=${encodeURIComponent(session.refresh_token)}&type=recovery` : projectUrl; window.location.href = authUrl; return; } } catch { } await new Promise((r) => setTimeout(r, 1500)); } window.location.href = projectUrl; } catch (err) { setStatus({ visible: true, content: `${err.message}` }); setRevivingIds((prev) => { const s = new Set(prev); s.delete(sandbox.id); return s; }); } }; const handleCreate = async (projectId) => { setCreating(true); setCreateModalOpen(false); setStatus({ visible: true, content: `Creating project "${projectId}"...` }); try { const token = getAuthToken(); if (!token) throw new Error("Please sign in to create a project"); if (!websimUsername) throw new Error("WebSim username not found in your account"); const res = await fetch("/api/sandboxes/acquire", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, body: JSON.stringify({ projectId, username: websimUsername }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error?.message || "Failed to create project"); if (data.state === "error") throw new Error(data.error || "Project creation failed"); const projectUrl = window.location.port ? data.urls.host.replace(/(https?:\/\/[^/]+)/, `$1:${window.location.port}`) : data.urls.host; const actionText = data.created ? "created" : "found existing"; setStatus({ visible: true, content: `Project ${data.projectId} ${actionText}. Waiting for project to be ready...` }); let ready = false; for (let i = 0; i < 15; i++) { try { await fetch(projectUrl, { method: "HEAD", mode: "no-cors" }); ready = true; break; } catch { await new Promise((r) => setTimeout(r, 1e3)); setStatus({ visible: true, content: `Project ${data.projectId} ${actionText}. Waiting for project to be ready... (${i + 2}s)` }); } } if (ready) { setStatus({ visible: true, content: 'Project ready! Redirecting...' }); const authUrl = session?.access_token && session?.refresh_token ? `${projectUrl}#access_token=${encodeURIComponent(session.access_token)}&refresh_token=${encodeURIComponent(session.refresh_token)}&type=recovery` : projectUrl; setTimeout(() => { window.location.href = authUrl; }, 300); } else { setStatus({ visible: true, content: `Project ${actionText} but may not be ready yet. Open anyway` }); setCreating(false); } } catch (err) { setStatus({ visible: true, content: `Error: ${err.message}` }); setCreating(false); } }; const handleDelete = async (id) => { setDeletingIds((prev) => new Set(prev).add(id)); try { const res = await fetch(`/api/sandboxes/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${getAuthToken()}` } }); if (!res.ok) { const data = await res.json(); throw new Error(data.error?.message || "Failed to delete project"); } setSandboxes((prev) => prev.filter((s) => s.id !== id)); } catch (err) { alert(`Error deleting project: ${err.message}`); } finally { setDeletingIds((prev) => { const s = new Set(prev); s.delete(id); return s; }); } }; const handleReset = async (sandbox) => { setDeletingIds((prev) => new Set(prev).add(sandbox.id)); try { const appUrl = sandbox.target || `${scheme}://${sandbox.externalId}.${baseDomain}${portSuffix}`; const res = await fetch(`${appUrl}/api/reset`, { method: "POST" }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || "Failed to reset project"); } window.location.reload(); } catch (err) { alert(`Error resetting project: ${err.message}`); } finally { setDeletingIds((prev) => { const s = new Set(prev); s.delete(sandbox.id); return s; }); } }; return /* @__PURE__ */ jsxs("div", { className: "container", children: [ /* @__PURE__ */ jsxs("div", { className: "header", children: [ /* @__PURE__ */ jsx("h1", { children: "Projects" }), /* @__PURE__ */ jsxs("div", { className: "auth-section", children: [ hasSupabase && (authLoading ? /* @__PURE__ */ jsx("div", { className: "auth-loading" }) : session ? /* @__PURE__ */ jsx(UserInfo, { user: session.user, onSignOut: () => { setSession(null); supabaseClient?.auth.signOut().catch(() => { }); } }) : /* @__PURE__ */ jsx("button", { className: "auth-btn", onClick: () => setAuthModalOpen(true), children: "Sign In" })), canCreate && /* @__PURE__ */ jsx("button", { className: "create-btn", onClick: () => setCreateModalOpen(true), disabled: creating, children: creating ? "Creating..." : "+ New Project" }) ] }) ] }), status.visible && /* @__PURE__ */ jsx("div", { className: "create-status visible", dangerouslySetInnerHTML: { __html: status.content } }), userSandboxes.length === 0 ? /* @__PURE__ */ jsx("p", { className: "empty", children: !session && hasSupabase ? "Sign in to see your projects" : "No projects yet. Create one to get started!" }) : userSandboxes.map((sandbox) => /* @__PURE__ */ jsx( SandboxCard, { sandbox, scheme, baseDomain, portSuffix, session, onDelete: handleDelete, onReset: handleReset, onRevive: handleRevive, deleting: deletingIds.has(sandbox.id), reviving: revivingIds.has(sandbox.id), healthStatus: healthStatus[sandbox.id] || "checking", localMode }, sandbox.id )), /* @__PURE__ */ jsxs("div", { className: "footer", children: [ userSandboxes.length, " project", userSandboxes.length !== 1 ? "s" : "" ] }), hasSupabase && /* @__PURE__ */ jsx(AuthModal, { open: authModalOpen, onClose: () => setAuthModalOpen(false), supabaseClient }), canCreate && /* @__PURE__ */ jsx( CreateProjectModal, { open: createModalOpen, onClose: () => setCreateModalOpen(false), username: websimUsername, onSubmit: handleCreate, creating, existingProjectIds: new Set(userSandboxes.map((s) => s.projectId)) } ) ] }); } // src/data-plane/client.tsx var props = window.__INITIAL_PROPS__; if (props) { const root = document.getElementById("root"); if (root) { hydrateRoot(root, createElement(IndexPageApp, props)); } } })();