// 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));
}
}
})();