first commit

This commit is contained in:
2026-04-24 22:58:32 +00:00
commit e60d971120
94 changed files with 19127 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BareCloud — Free BareOS hosting</title>
<meta
name="description"
content="Launch VPN-protected BareOS booters on demand. In-browser terminal, no login required."
/>
</head>
<body class="min-h-full flex flex-col bg-[#05070c] text-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
import { useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { Toasts } from "@/components/Toasts";
import { MainLayout } from "./layout/MainLayout";
import { HomePage } from "./pages/HomePage";
import { LaunchPage } from "./pages/LaunchPage";
import { BooterPage } from "./pages/BooterPage";
import { MyBootersPage } from "./pages/MyBootersPage";
import { CommunityPage } from "./pages/CommunityPage";
import { StatsPage } from "./pages/StatsPage";
import { RunLocallyPage } from "./pages/RunLocallyPage";
import { UserManualPage } from "./pages/UserManualPage";
import { TermsPage } from "./pages/TermsPage";
import { PrivacyPage } from "./pages/PrivacyPage";
export default function App() {
useEffect(() => {
const log = console.info ?? console.log;
log.call(console, "[BareCloud] React App mounted", {
pathname: typeof window !== "undefined" ? window.location.pathname : "",
});
}, []);
return (
<>
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/launch" element={<LaunchPage />} />
<Route path="/community" element={<CommunityPage />} />
<Route path="/my-booters" element={<MyBootersPage />} />
<Route path="/run-locally" element={<RunLocallyPage />} />
<Route path="/user-manual" element={<UserManualPage />} />
<Route path="/terms" element={<TermsPage />} />
<Route path="/privacy" element={<PrivacyPage />} />
<Route path="/stats" element={<StatsPage />} />
</Route>
<Route path="/booter/:id" element={<BooterPage />} />
</Routes>
<Toasts />
</>
);
}
+61
View File
@@ -0,0 +1,61 @@
import { Link } from "react-router-dom";
import { format, formatDistanceToNow } from "date-fns";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
export type BooterSummary = {
id: string;
skipVpn: boolean;
region: string;
createdAt: number;
lastAccessed: number;
expiresAt: number;
};
type Props = {
booter: BooterSummary;
};
export function BooterCard({ booter }: Props) {
return (
<Card className="overflow-hidden">
<CardHeader className="pb-3">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<CardTitle className="text-base">Bare OS instance</CardTitle>
<CardDescription className="break-all font-mono text-xs text-white/55">{booter.id}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="text-sm text-white/65">
<div>
Region: <span className="text-white/85">{booter.region}</span>
</div>
<div>
Last opened:{" "}
<span className="text-white/85">{formatDistanceToNow(booter.lastAccessed, { addSuffix: true })}</span>
</div>
<div>
Keep until:{" "}
<span className="text-white/85">
{format(booter.expiresAt, "MMM d, HH:mm")}{" "}
<span className="text-white/55">({formatDistanceToNow(booter.expiresAt, { addSuffix: true })})</span>
</span>
</div>
<div>
Created:{" "}
<span className="text-white/85">{formatDistanceToNow(booter.createdAt, { addSuffix: true })}</span>
</div>
</div>
<Button asChild variant="default" className="shrink-0">
<Link to={`/booter/${booter.id}`}>
Open console
<ArrowRight className="h-4 w-4" />
</Link>
</Button>
</CardContent>
</Card>
);
}
+40
View File
@@ -0,0 +1,40 @@
import { useState } from "react";
import { toast } from "sonner";
import { HeartPulse } from "lucide-react";
import { Button } from "@/components/ui/button";
type Props = {
booterId: string;
/** Called after a successful extend (e.g. refresh status). */
onExtended?: () => void;
};
export function KeepAliveButton({ booterId, onExtended }: Props) {
const [loading, setLoading] = useState(false);
async function confirm() {
setLoading(true);
try {
const res = await fetch(`/api/booters/${encodeURIComponent(booterId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "keepalive" }),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(json.error || "Request failed");
toast.success(json.message || "Thanks — your instance will stay active.");
onExtended?.();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Could not save that");
} finally {
setLoading(false);
}
}
return (
<Button type="button" variant="secondary" onClick={confirm} disabled={loading} className="w-full sm:w-auto">
<HeartPulse className="h-4 w-4" />
{loading ? "Saving…" : "Extend seven more days"}
</Button>
);
}
+696
View File
@@ -0,0 +1,696 @@
import { useLayoutEffect, useRef, useState } from "react";
import { Terminal as XTerm } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";
import { Loader2, RotateCcw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { barecloudDebug } from "@/lib/barecloud-debug";
import { appWsClient, wsMode } from "@/lib/app-ws";
type Props = {
booterId: string;
};
function appBasePath(): string {
return (import.meta.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "");
}
/** Same-origin by default; override when the page is served from a different host than the API (or behind a tricky proxy). */
/** Must match server `terminal-proxy.ts` export `WS_CLOSE_PEAR_RESTART` / `WS_CLOSE_PM2_RESTART` (4410). */
const WS_CLOSE_PEAR_RESTART = 4410;
/** Instance was deleted on the server — do not reconnect. */
const WS_CLOSE_INSTANCE_DELETED = 4411;
let warnedMisconfiguredTerminalWsUrl = false;
/** Duplicate instance id in the query so a broken proxy can still be corrected server-side (see `resolveTerminalBooterIdFromUpgrade`). */
function withTerminalBooterQueryParam(url: string, booterId: string): string {
if (/[?&]booter=/.test(url)) {
try {
const u = new URL(url);
if (u.searchParams.get("booter") === booterId) return url;
} catch {
/* ignore */
}
return url;
}
const joiner = url.includes("?") ? "&" : "?";
return `${url}${joiner}booter=${encodeURIComponent(booterId)}`;
}
function terminalWebSocketUrl(booterId: string): string {
const enc = encodeURIComponent(booterId);
const basePath = appBasePath();
const explicit = import.meta.env.NEXT_PUBLIC_TERMINAL_WS_URL?.trim();
let url: string;
if (explicit) {
if (/\{id\}/i.test(explicit)) {
url = explicit.replace(/\{id\}/gi, enc);
} else {
if (!warnedMisconfiguredTerminalWsUrl) {
warnedMisconfiguredTerminalWsUrl = true;
console.warn(
"[BareCloud] NEXT_PUBLIC_TERMINAL_WS_URL must include the literal substring {id} (example: wss://host/base/api/terminal/{id}). " +
"A full URL without {id} would send every instance to the same console. Using this pages origin instead.",
);
}
const appOrigin = import.meta.env.NEXT_PUBLIC_APP_ORIGIN?.trim();
if (appOrigin) {
try {
const u = new URL(appOrigin);
const wsProto = u.protocol === "https:" ? "wss:" : "ws:";
url = `${wsProto}//${u.host}${basePath}/api/terminal/${enc}`;
} catch {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
url = `${proto}//${window.location.host}${basePath}/api/terminal/${enc}`;
}
} else {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
url = `${proto}//${window.location.host}${basePath}/api/terminal/${enc}`;
}
}
} else {
const appOrigin = import.meta.env.NEXT_PUBLIC_APP_ORIGIN?.trim();
if (appOrigin) {
try {
const u = new URL(appOrigin);
const wsProto = u.protocol === "https:" ? "wss:" : "ws:";
url = `${wsProto}//${u.host}${basePath}/api/terminal/${enc}`;
} catch {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
url = `${proto}//${window.location.host}${basePath}/api/terminal/${enc}`;
}
} else {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
url = `${proto}//${window.location.host}${basePath}/api/terminal/${enc}`;
}
}
return withTerminalBooterQueryParam(url, booterId);
}
function retriableWsClose(code: number): boolean {
return code === 1006 || code === 1001 || code === 1012 || code === 1013;
}
/** Strip CSI / OSC sequences so streamed PTY output can be matched reliably. */
const ANSI_ESC = String.fromCharCode(0x1b);
const ANSI_BEL = String.fromCharCode(0x07);
const ANSI_OSC_RE = new RegExp(`${ANSI_ESC}\\][^${ANSI_BEL}]*${ANSI_BEL}`, "g");
const ANSI_CSI_RE = new RegExp(`${ANSI_ESC}\\[[0-?]*[ -/]*[@-~]`, "g");
const ANSI_ESC_RE = new RegExp(`${ANSI_ESC}[@-_]`, "g");
function stripAnsiForMatch(s: string): string {
return s
.replace(ANSI_OSC_RE, "")
.replace(ANSI_CSI_RE, "")
.replace(ANSI_ESC_RE, "");
}
const OUTPUT_MATCH_BUF_MAX = 24_000;
const DEFAULT_TERMINAL_SCROLLBACK = 1_000_000;
const MAX_TERMINAL_SCROLLBACK = 4_294_967_295;
const WHEEL_PX_PER_LINE = 24;
function terminalScrollback(): number {
const raw = import.meta.env.NEXT_PUBLIC_TERMINAL_SCROLLBACK?.trim();
if (!raw) return DEFAULT_TERMINAL_SCROLLBACK;
const n = Number.parseInt(raw, 10);
if (!Number.isFinite(n) || n < 0) return DEFAULT_TERMINAL_SCROLLBACK;
return Math.min(n, MAX_TERMINAL_SCROLLBACK);
}
function pearExitedWithCodeOne(t: string): boolean {
return (
t.includes("pear exited (exitcode=1") ||
(t.includes("[barecloud]") && t.includes("pear exited") && /exitcode\s*=\s*1\b/.test(t))
);
}
/**
* Pear PTY failures where a fresh WebSocket / PTY often recovers:
* kernel replication, or IPC/RPC teardown (e.g. Error: RPC destroyed).
* Requires BareCloud's Pear exited line with exitCode=1.
*/
function shouldOfferPearFailureReboot(buf: string): boolean {
const t = stripAnsiForMatch(buf).toLowerCase();
if (!pearExitedWithCodeOne(t)) return false;
return (
t.includes("kernel not found after replication") ||
t.includes("rpc destroyed")
);
}
/** Pear exited 0 / signal 0 with the server “reconnect later” line — tmux resumable session left nothing to attach to. */
function shouldAutoRestartAfterPearCleanExit(buf: string): boolean {
const t = stripAnsiForMatch(buf).toLowerCase();
if (!t.includes("pear exited")) return false;
if (!/exitcode\s*=\s*0\b/.test(t)) return false;
if (!/signal\s*=\s*0\b/.test(t)) return false;
return t.includes("reconnect the console") || t.includes("supervised instance");
}
/** Shown when the console keeps dropping — usually network, browser sleep, or a proxy in front of the site. */
function terminalConnectionHelp(): string {
return [
"If you are on WiFi or VPN, try a more stable connection.",
"Corporate or school networks sometimes block long-lived sessions — try another network if you can.",
"Refreshing the page often restores the console after an idle timeout.",
].join("\n\n");
}
export function Terminal({ booterId }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<XTerm | null>(null);
const fitRef = useRef<FitAddon | null>(null);
const [status, setStatus] = useState<"connecting" | "open" | "error">("connecting");
const [errorDetail, setErrorDetail] = useState<string | null>(null);
const [kernelRebootBanner, setKernelRebootBanner] = useState<{
seconds: number;
detail: string;
} | null>(null);
const kernelReconnectRef = useRef<(() => void) | null>(null);
useLayoutEffect(() => {
barecloudDebug("Terminal layout effect start", {
booterId,
hasContainerRef: Boolean(containerRef.current),
pagePath: typeof window !== "undefined" ? window.location.pathname : "",
basePath: appBasePath(),
terminalWsEnv: import.meta.env.NEXT_PUBLIC_TERMINAL_WS_URL?.trim() || null,
appOriginEnv: import.meta.env.NEXT_PUBLIC_APP_ORIGIN?.trim() || null,
});
const el = containerRef.current;
if (!el) {
barecloudDebug("Terminal: container ref is null — xterm will not start until layout is fixed", { booterId });
return;
}
let cancelled = false;
barecloudDebug("Terminal mount / xterm init", {
booterId,
pagePath: typeof window !== "undefined" ? window.location.pathname : "",
});
const term = new XTerm({
cursorBlink: true,
convertEol: true,
scrollback: terminalScrollback(),
scrollOnUserInput: true,
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
fontSize: 14,
theme: {
background: "#070a0f",
foreground: "#e8f0ff",
cursor: "#67d4ff",
selectionBackground: "#67d4ff44",
},
allowTransparency: true,
});
const fit = new FitAddon();
term.loadAddon(fit);
term.open(el);
term.attachCustomWheelEventHandler((ev) => {
if (ev.deltaY === 0) return true;
const active = term.buffer.active;
const hasXtermScrollback = active.baseY > 0;
if (!hasXtermScrollback) {
// xterm otherwise converts wheel into up/down keys when there is no scrollback.
ev.preventDefault();
ev.stopPropagation();
return false;
}
const mode = ev.deltaMode;
const rawLines =
mode === WheelEvent.DOM_DELTA_LINE
? ev.deltaY
: mode === WheelEvent.DOM_DELTA_PAGE
? ev.deltaY * Math.max(1, term.rows)
: ev.deltaY / WHEEL_PX_PER_LINE;
const whole = rawLines > 0 ? Math.floor(rawLines) : Math.ceil(rawLines);
const lines = whole === 0 ? (rawLines > 0 ? 1 : -1) : whole;
term.scrollLines(lines);
// Prevent xterm from forwarding wheel as terminal input (which can trigger shell command history).
ev.preventDefault();
ev.stopPropagation();
return false;
});
requestAnimationFrame(() => {
term.focus();
});
const basePath = appBasePath();
let resizePostTimer: ReturnType<typeof setTimeout> | null = null;
let lastResizePosted = { cols: 0, rows: 0 };
const scheduleResizeNotify = () => {
if (resizePostTimer) clearTimeout(resizePostTimer);
resizePostTimer = setTimeout(() => {
resizePostTimer = null;
const cols = term.cols;
const rows = term.rows;
if (cols < 2 || rows < 1) return;
if (cols === lastResizePosted.cols && rows === lastResizePosted.rows) return;
lastResizePosted = { cols, rows };
void fetch(`${basePath}/api/terminal/${encodeURIComponent(booterId)}/resize`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cols, rows }),
}).catch(() => {
/* ignore */
});
}, 120);
};
let lastFitSize = { w: 0, h: 0 };
const runFit = () => {
try {
const w = el.clientWidth;
const h = el.clientHeight;
if (w < 2 || h < 2) {
lastFitSize = { w: 0, h: 0 };
return;
}
if (w === lastFitSize.w && h === lastFitSize.h) return;
lastFitSize = { w, h };
fit.fit();
scheduleResizeNotify();
} catch {
/* ignore */
}
};
requestAnimationFrame(() => {
runFit();
requestAnimationFrame(runFit);
});
termRef.current = term;
fitRef.current = fit;
setErrorDetail(null);
const activeWsRef: { current: WebSocket | null } = { current: null };
const wasOpenRef = { current: false };
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let attempt = 0;
const maxAttempts = 18;
let welcomeShown = false;
const outputMatchBufRef = { current: "" };
const kernelRebootOfferedRef = { current: false };
const pearCleanExitRestartScheduledRef = { current: false };
let kernelRebootInterval: ReturnType<typeof setInterval> | null = null;
let postPearRestartEstablishTimer: ReturnType<typeof setTimeout> | null = null;
const clearKernelRebootTimers = () => {
if (kernelRebootInterval) {
clearInterval(kernelRebootInterval);
kernelRebootInterval = null;
}
};
const clearReconnect = () => {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
const scheduleEstablishAfterPearRestart = () => {
if (cancelled) return;
if (postPearRestartEstablishTimer) clearTimeout(postPearRestartEstablishTimer);
postPearRestartEstablishTimer = setTimeout(() => {
postPearRestartEstablishTimer = null;
if (cancelled) return;
attempt = 0;
clearReconnect();
establishWs();
}, 500);
};
const scheduleReconnect = (afterMs: number, note: string) => {
if (cancelled) return;
attempt += 1;
if (attempt > maxAttempts) {
setErrorDetail(`${note} Tried ${maxAttempts} times.\n\n${terminalConnectionHelp()}`);
setStatus("error");
return;
}
/* After the first successful session, stay on "open" so the xterm is not covered by a blur overlay. */
if (!welcomeShown) setStatus("connecting");
const sec = Math.max(1, Math.ceil(afterMs / 1000));
term.writeln(`\x1b[33m[BareCloud]\x1b[0m ${note} Retrying in ${sec}s… (attempt ${attempt}/${maxAttempts})`);
reconnectTimer = setTimeout(() => establishWs(), afterMs);
};
const performKernelReconnect = () => {
if (cancelled) return;
clearKernelRebootTimers();
kernelRebootOfferedRef.current = false;
outputMatchBufRef.current = "";
setKernelRebootBanner(null);
attempt = 0;
clearReconnect();
establishWs();
};
kernelReconnectRef.current = performKernelReconnect;
const scheduleKernelRebootOffer = () => {
if (cancelled || kernelRebootOfferedRef.current) return;
kernelRebootOfferedRef.current = true;
const t = stripAnsiForMatch(outputMatchBufRef.current).toLowerCase();
let detail = "Bare OS stopped unexpectedly";
if (t.includes("kernel not found after replication")) {
detail = "Still loading from the network";
} else if (t.includes("rpc destroyed")) {
detail = "Session interrupted";
}
setKernelRebootBanner({ seconds: 5, detail });
kernelRebootInterval = setInterval(() => {
setKernelRebootBanner((b) => {
if (!b) return null;
if (b.seconds <= 1) {
clearKernelRebootTimers();
queueMicrotask(() => performKernelReconnect());
return null;
}
return { ...b, seconds: b.seconds - 1 };
});
}, 1000);
};
const schedulePearCleanExitRestart = () => {
if (cancelled || pearCleanExitRestartScheduledRef.current) return;
pearCleanExitRestartScheduledRef.current = true;
outputMatchBufRef.current = "";
clearReconnect();
term.clear();
term.writeln(
"\x1b[36m[BareCloud]\x1b[0m Pear exited cleanly with nothing to attach to. The console will request a fresh Pear session in 3 seconds.",
);
term.writeln("");
setTimeout(async () => {
if (cancelled) return;
try {
const res = await fetch(`${basePath}/api/booters/${encodeURIComponent(booterId)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "restart" }),
});
const j = (await res.json().catch(() => ({}))) as { error?: string };
if (!res.ok) {
throw new Error(j.error ?? "Restart failed");
}
term.writeln("\x1b[36m[BareCloud]\x1b[0m Restart requested — reconnecting…");
term.writeln("");
scheduleEstablishAfterPearRestart();
pearCleanExitRestartScheduledRef.current = false;
} catch (e) {
pearCleanExitRestartScheduledRef.current = false;
term.writeln(
`\x1b[31m[BareCloud]\x1b[0m ${e instanceof Error ? e.message : "Restart failed"}. Refresh the page or use Restart instance in the sidebar.`,
);
term.writeln("");
}
}, 3000);
};
const establishWs = () => {
if (cancelled) return;
clearReconnect();
try {
activeWsRef.current?.close();
} catch {
/* ignore */
}
activeWsRef.current = null;
wasOpenRef.current = false;
/* Subprotocol carries booter id so proxies that rewrite WS paths or strip ?booter= still attach the right instance. */
const wsUrl = terminalWebSocketUrl(booterId);
const subprotocols = [`barecloud.booter.${booterId}`];
barecloudDebug("Terminal WebSocket opening", {
booterId,
wsUrl,
subprotocols,
href: typeof window !== "undefined" ? window.location.href : "",
});
const ws = new WebSocket(wsUrl, subprotocols);
ws.binaryType = "arraybuffer";
activeWsRef.current = ws;
ws.onopen = () => {
if (cancelled) return;
if (activeWsRef.current !== ws) return;
const hello = { barecloudTerminal: 1, booterId };
try {
ws.send(JSON.stringify(hello));
barecloudDebug("Terminal WebSocket first frame (hello) sent", hello);
} catch (e) {
barecloudDebug("Terminal WebSocket hello send failed", e);
}
wasOpenRef.current = true;
setStatus("open");
setErrorDetail(null);
if (!welcomeShown) {
welcomeShown = true;
term.writeln("\x1b[36m[BareCloud]\x1b[0m Connected — your Bare OS console is ready.");
term.writeln("");
} else {
term.writeln("\x1b[36m[BareCloud]\x1b[0m Reconnected.");
}
requestAnimationFrame(() => {
runFit();
requestAnimationFrame(runFit);
term.focus();
});
};
ws.onerror = () => {
if (cancelled) return;
/* onclose usually follows; avoid duplicate UI */
};
ws.onclose = (ev) => {
/* Ignore closes from a socket we already replaced (prevents reconnect storms). */
if (activeWsRef.current !== ws) return;
activeWsRef.current = null;
if (cancelled) return;
const wasOpen = wasOpenRef.current;
wasOpenRef.current = false;
barecloudDebug("Terminal WebSocket closed", {
booterId,
code: ev.code,
reason: ev.reason ? String(ev.reason) : "",
wasClean: ev.wasClean,
hadBeenOpen: wasOpen,
});
if (ev.code === 4404) {
setErrorDetail(
"This instance could not be started on the server. Try again in a moment; if it keeps happening, the host may need attention.",
);
setStatus("error");
return;
}
if (ev.code === 4403) {
setErrorDetail(
"There is no data for this instance on the server anymore, or the server could not verify the console session.\n\nStart a new instance from the home page if needed. After upgrading BareCloud, hard-refresh this page (Ctrl+Shift+R) so the console sends the required first-message handshake.",
);
setStatus("error");
return;
}
if (ev.code === 4409) {
setErrorDetail(
"Another tab already has the console open for this instance. Close it there first, then refresh this page.",
);
setStatus("error");
return;
}
if (ev.code === WS_CLOSE_PEAR_RESTART) {
scheduleEstablishAfterPearRestart();
return;
}
if (ev.code === WS_CLOSE_INSTANCE_DELETED) {
setErrorDetail(
"This instance was removed on the server (deleted or purged). Open the home page if you need a new instance.",
);
setStatus("error");
return;
}
if (ev.code === 1011 && ev.reason) {
setErrorDetail(`Server closed the session (${String(ev.reason)}).`);
setStatus("error");
return;
}
if (!wasOpen && ev.code !== 1000) {
if (retriableWsClose(ev.code)) {
const wait = Math.min(8000, 400 + attempt * 350);
scheduleReconnect(wait, "Could not open the console connection.");
return;
}
setErrorDetail(
"Could not reach the console. Check that you are online and this site is reachable, then refresh the page.",
);
setStatus("error");
return;
}
if (wasOpen && ev.code === 1000) {
return;
}
if (wasOpen && ev.code !== 1000) {
if (retriableWsClose(ev.code)) {
const wait = Math.min(10000, 500 + attempt * 400);
scheduleReconnect(wait, "The connection dropped.");
return;
}
const reason = ev.reason ? String(ev.reason).trim() : "";
const second = reason ? `Details: ${reason}` : "The network closed the session unexpectedly.";
const lines = [
"The console disconnected. Wait a moment and refresh, or try another network.",
`${second}\n\n${terminalConnectionHelp()}`,
];
setErrorDetail(lines.join("\n\n"));
setStatus("error");
}
};
ws.onmessage = (ev) => {
const d = ev.data;
const chunk = typeof d === "string" ? d : new TextDecoder().decode(d as ArrayBuffer);
if (typeof d === "string") term.write(d);
else term.write(new Uint8Array(d as ArrayBuffer));
const prev = outputMatchBufRef.current;
outputMatchBufRef.current = (prev + chunk).slice(-OUTPUT_MATCH_BUF_MAX);
if (
!kernelRebootOfferedRef.current &&
shouldOfferPearFailureReboot(outputMatchBufRef.current)
) {
scheduleKernelRebootOffer();
}
if (
!kernelRebootOfferedRef.current &&
!pearCleanExitRestartScheduledRef.current &&
shouldAutoRestartAfterPearCleanExit(outputMatchBufRef.current)
) {
schedulePearCleanExitRestart();
}
};
};
let offTerminalMux: (() => void) | null = null;
if (wsMode() === "multiplex") {
/* Rollout guardrail: terminal data path still uses legacy dedicated WS until server-side channel parity lands. */
offTerminalMux = appWsClient().subscribe(
"error",
() => {
/* ignore app-level terminal-channel-not-ready errors for now */
},
{ booterId },
);
barecloudDebug("Terminal: multiplex mode enabled, using legacy terminal stream fallback", { booterId });
}
establishWs();
const d = term.onData((payload) => {
const s = activeWsRef.current;
if (s && s.readyState === WebSocket.OPEN) s.send(payload);
});
let roRaf = 0;
const ro = new ResizeObserver(() => {
if (roRaf) cancelAnimationFrame(roRaf);
roRaf = requestAnimationFrame(() => {
roRaf = 0;
runFit();
});
});
ro.observe(el);
const onWin = () => {
if (roRaf) cancelAnimationFrame(roRaf);
roRaf = requestAnimationFrame(() => {
roRaf = 0;
runFit();
});
};
window.addEventListener("resize", onWin);
return () => {
barecloudDebug("Terminal unmount / effect cleanup", { booterId });
cancelled = true;
wasOpenRef.current = false;
clearKernelRebootTimers();
kernelRebootOfferedRef.current = false;
pearCleanExitRestartScheduledRef.current = false;
if (postPearRestartEstablishTimer) {
clearTimeout(postPearRestartEstablishTimer);
postPearRestartEstablishTimer = null;
}
outputMatchBufRef.current = "";
setKernelRebootBanner(null);
clearReconnect();
if (resizePostTimer) clearTimeout(resizePostTimer);
if (roRaf) cancelAnimationFrame(roRaf);
ro.disconnect();
window.removeEventListener("resize", onWin);
d.dispose();
offTerminalMux?.();
try {
activeWsRef.current?.close();
} catch {
/* ignore */
}
activeWsRef.current = null;
term.dispose();
termRef.current = null;
fitRef.current = null;
};
}, [booterId]);
return (
<div
className="barecloud-xterm-host relative flex h-full min-h-0 w-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-black/40 shadow-inner shadow-black/60"
aria-label="Bare OS console"
onMouseDown={(e) => {
if (e.button === 0) termRef.current?.focus();
}}
>
{kernelRebootBanner != null && (
<div className="absolute left-0 right-0 top-0 z-20 flex flex-wrap items-center justify-between gap-3 border-b border-amber-400/30 bg-amber-950/90 px-3 py-2.5 text-sm text-amber-50 shadow-md backdrop-blur">
<span className="min-w-0 flex-1 leading-snug">
<span className="font-medium">{kernelRebootBanner.detail}</span>
{" — "}
reconnecting in <strong className="tabular-nums">{kernelRebootBanner.seconds}</strong>s, or tap{" "}
<strong>Reconnect now</strong>.
</span>
<Button
type="button"
variant="secondary"
size="sm"
className="shrink-0 border-amber-400/40 bg-amber-900/50 text-amber-50 hover:bg-amber-800/60"
onClick={() => kernelReconnectRef.current?.()}
>
<RotateCcw className="h-3.5 w-3.5" />
Reconnect now
</Button>
</div>
)}
{status === "connecting" && (
<div className="absolute left-0 right-0 top-0 z-10 flex items-center justify-center gap-2 border-b border-white/10 bg-zinc-950/95 py-2.5 text-sm text-white/90 shadow-md">
<Loader2 className="h-4 w-4 shrink-0 animate-spin" />
Connecting to console
</div>
)}
{status === "error" && (
<div className="absolute bottom-3 left-3 right-3 z-10 whitespace-pre-line rounded-xl border border-red-400/25 bg-red-950/70 px-3 py-2 text-xs leading-relaxed text-red-100 backdrop-blur">
{errorDetail ?? "Console connection lost. Refresh the page and try again."}
</div>
)}
<div
ref={containerRef}
className="relative z-0 flex min-h-0 flex-1 flex-col overflow-x-hidden px-2"
/>
</div>
);
}
+16
View File
@@ -0,0 +1,16 @@
import { Toaster } from "sonner";
export function Toasts() {
return (
<Toaster
richColors
theme="dark"
toastOptions={{
classNames: {
toast:
"border border-white/10 bg-black/70 text-white backdrop-blur-xl shadow-2xl shadow-black/40",
},
}}
/>
);
}
+31
View File
@@ -0,0 +1,31 @@
/** Pear logo (from Pears / Holepunch brand assets). */
export function PearMark({ className }: { className?: string }) {
return (
<svg
width="22"
height="22"
viewBox="0 0 127 182"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden
>
<path d="M58.9658 0H68.0374V17.9736H58.9658V0Z" fill="#B0D944" />
<path
d="M54.4299 23.6604V27.3005H45.3583V31.8506H81.6449V27.3005H72.5732V20.2471H63.5016V23.6604H54.4299Z"
fill="#B0D944"
/>
<path d="M90.7166 34.124H63.5016V37.5374H36.2866V45.7275H90.7166V34.124Z" fill="#B0D944" />
<path d="M99.7882 48.0041H63.5016V51.4175H27.215V59.6076H99.7882V48.0041Z" fill="#B0D944" />
<path d="M99.7882 61.8811H63.5016V65.2944H27.215V73.4846H99.7882V61.8811Z" fill="#B0D944" />
<path d="M108.86 75.758H63.5016V79.1714H18.1433V87.3615H108.86V75.758Z" fill="#B0D944" />
<path d="M108.86 89.635H63.5016V93.0483H18.1433V101.238H108.86V89.635Z" fill="#B0D944" />
<path d="M63.4984 103.512V106.925H9.07166V115.115H117.932V103.512H63.4984Z" fill="#B0D944" />
<path d="M127 117.392H63.4984V120.805H0V128.996H127V117.392Z" fill="#B0D944" />
<path d="M127 131.269H63.4984V134.682H0V142.873H127V131.269Z" fill="#B0D944" />
<path d="M127 145.146H63.4984V148.559H0V156.749H127V145.146Z" fill="#B0D944" />
<path d="M108.86 159.023H63.5016V162.436H18.1433V170.626H108.86V159.023Z" fill="#B0D944" />
<path d="M90.7166 172.9H63.5016V176.313H36.2866V182H90.7166V172.9Z" fill="#B0D944" />
</svg>
);
}
+28
View File
@@ -0,0 +1,28 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-3 py-1 text-xs font-medium tracking-wide transition-colors",
{
variants: {
variant: {
default: "border-white/15 bg-white/5 text-white/85",
success: "border-emerald-400/25 bg-emerald-500/15 text-emerald-200",
warning: "border-amber-400/25 bg-amber-500/15 text-amber-100",
destructive: "border-red-400/25 bg-red-500/15 text-red-100",
},
},
defaultVariants: {
variant: "default",
},
},
);
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
+52
View File
@@ -0,0 +1,52 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/60 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-gradient-to-r from-sky-500 to-indigo-500 text-white shadow-lg shadow-sky-500/20 hover:from-sky-400 hover:to-indigo-400",
secondary:
"border border-white/10 bg-white/5 text-white hover:bg-white/10 backdrop-blur",
ghost: "text-white/80 hover:bg-white/10 hover:text-white",
destructive: "bg-red-600 text-white hover:bg-red-500",
outline:
"border border-white/15 bg-transparent text-white hover:bg-white/10",
warning:
"border border-amber-500/45 bg-amber-950/70 text-amber-50 shadow-lg shadow-amber-900/25 hover:bg-amber-900/80 hover:border-amber-400/50",
},
size: {
default: "h-11 px-5 py-2",
sm: "h-9 rounded-lg px-3",
lg: "h-12 rounded-xl px-8 text-base",
icon: "h-10 w-10 rounded-xl",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref as never} {...props} />
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
+44
View File
@@ -0,0 +1,44 @@
import * as React from "react";
import { cn } from "@/lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-2xl border border-white/10 bg-white/[0.04] shadow-xl shadow-black/40 backdrop-blur-xl",
className,
)}
{...props}
/>
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col gap-2 p-6 pb-3", className)} {...props} />
),
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-lg font-semibold tracking-tight text-white", className)} {...props} />
),
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-white/60", className)} {...props} />
),
);
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
),
);
CardContent.displayName = "CardContent";
export { Card, CardHeader, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,94 @@
import * as React from "react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export type ConfirmDialogTone = "default" | "destructive" | "warning";
export type ConfirmDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: React.ReactNode;
icon?: React.ReactNode;
confirmLabel?: string;
cancelLabel?: string;
tone?: ConfirmDialogTone;
pending?: boolean;
onConfirm: () => void | Promise<void>;
};
export function ConfirmDialog({
open,
onOpenChange,
title,
description,
icon,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
tone = "default",
pending = false,
onConfirm,
}: ConfirmDialogProps) {
const [internalPending, setInternalPending] = React.useState(false);
const busy = pending || internalPending;
const handleConfirm = async () => {
setInternalPending(true);
try {
await onConfirm();
onOpenChange(false);
} finally {
setInternalPending(false);
}
};
const confirmVariant =
tone === "destructive" ? "destructive" : tone === "warning" ? "warning" : "default";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="max-w-[420px] sm:max-w-md"
disableCloseButton={busy}
onPointerDownOutside={(e) => busy && e.preventDefault()}
onEscapeKeyDown={(e) => busy && e.preventDefault()}
>
<div className="flex gap-4 px-6 pb-1 pr-14 pt-6">
{icon ? (
<div
className={cn(
"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-white/10 bg-white/[0.06] text-white/90",
tone === "destructive" && "border-red-500/30 bg-red-950/50 text-red-200",
tone === "warning" && "border-amber-500/35 bg-amber-950/45 text-amber-200",
)}
>
{icon}
</div>
) : null}
<DialogHeader className={cn("flex-1 gap-2 p-0", !icon && "pr-0")}>
<DialogTitle>{title}</DialogTitle>
{description ? (
<DialogDescription className="text-white/65">{description}</DialogDescription>
) : null}
</DialogHeader>
</div>
<DialogFooter className="mt-2">
<Button type="button" variant="secondary" disabled={busy} onClick={() => onOpenChange(false)}>
{cancelLabel}
</Button>
<Button type="button" variant={confirmVariant} disabled={busy} onClick={() => void handleConfirm()}>
{busy ? "Please wait…" : confirmLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+115
View File
@@ -0,0 +1,115 @@
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"bc-dialog-overlay fixed inset-0 z-[100] bg-[#020306]/85 backdrop-blur-md",
"data-[state=open]:animate-[bc-dialog-overlay-in_200ms_ease-out_both] data-[state=closed]:animate-[bc-dialog-overlay-out_150ms_ease-in_both]",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
type DialogContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
/** When true, the corner close control is non-interactive (e.g. while an action is in flight). */
disableCloseButton?: boolean;
};
const DialogContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, DialogContentProps>(
({ className, children, disableCloseButton, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
{/*
Flex host keeps the panel centered in the *viewport* (not a transformed ancestor).
pointer-events: wrapper none, panel auto — backdrop clicks still reach the overlay.
*/}
<div className="fixed inset-0 z-[101] flex items-center justify-center overflow-y-auto overscroll-y-contain p-4 sm:p-6 pointer-events-none">
<DialogPrimitive.Content
ref={ref}
className={cn(
"bc-dialog-content pointer-events-auto relative z-[102] flex w-[min(100vw-2rem,28rem)] max-w-md flex-col overflow-hidden rounded-2xl border border-white/[0.14] bg-[#070a11]/[0.97] p-0 shadow-[0_24px_80px_-12px_rgba(0,0,0,0.75)] shadow-black/60 backdrop-blur-2xl outline-none",
"data-[state=open]:animate-[bc-dialog-content-in_220ms_cubic-bezier(0.16,1,0.3,1)_both] data-[state=closed]:animate-[bc-dialog-content-out_160ms_ease-in_both]",
"max-h-[min(90dvh,640px)] my-auto",
className,
)}
{...props}
>
<div
className="pointer-events-none h-px w-full shrink-0 bg-gradient-to-r from-transparent via-sky-500/45 to-transparent"
aria-hidden
/>
{children}
<DialogPrimitive.Close
type="button"
disabled={disableCloseButton}
aria-disabled={disableCloseButton || undefined}
className={cn(
"absolute right-3.5 top-3.5 inline-flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-white/[0.06] text-white/70 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-400/50",
disableCloseButton && "pointer-events-none opacity-40",
)}
aria-label="Close"
>
<X className="h-4 w-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</div>
</DialogPortal>
),
);
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col gap-1.5 px-6 pb-2 pr-14 pt-6 text-left", className)} {...props} />
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse gap-2 border-t border-white/[0.08] bg-black/25 px-6 py-4 sm:flex-row sm:justify-end sm:gap-3",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold tracking-tight text-white", className)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm leading-relaxed text-white/60", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription };
+21
View File
@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "@/lib/utils";
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-11 w-full rounded-xl border border-white/10 bg-black/30 px-4 py-2 text-sm text-white shadow-inner shadow-black/30 outline-none backdrop-blur transition placeholder:text-white/35 focus-visible:border-sky-400/60 focus-visible:ring-2 focus-visible:ring-sky-400/25 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };
+13
View File
@@ -0,0 +1,13 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn("text-sm font-medium text-white/80", className)} {...props} />
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };
+23
View File
@@ -0,0 +1,23 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-white/10",
orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
className,
)}
{...props}
/>
));
Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator };
+143
View File
@@ -0,0 +1,143 @@
@import "tailwindcss";
:root {
--background: #05070c;
--foreground: #e9f0ff;
--muted: rgba(255, 255, 255, 0.65);
--card: rgba(255, 255, 255, 0.04);
--border: rgba(255, 255, 255, 0.1);
--ring: rgba(103, 212, 255, 0.35);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: ui-sans-serif, system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
html,
body {
height: 100%;
}
body {
background:
radial-gradient(1200px 600px at 20% 0%, rgba(56, 189, 248, 0.18), transparent 55%),
radial-gradient(900px 500px at 85% 10%, rgba(99, 102, 241, 0.22), transparent 55%),
radial-gradient(700px 450px at 60% 90%, rgba(16, 185, 129, 0.12), transparent 55%),
linear-gradient(to bottom, #05070c, #05070c);
color: var(--foreground);
}
/* Thin, theme-matched scrollbars (Firefox + Chromium/WebKit). */
* {
scrollbar-width: thin;
scrollbar-color: rgba(103, 212, 255, 0.45) rgba(255, 255, 255, 0.06);
}
*::-webkit-scrollbar {
width: 9px;
height: 9px;
}
*::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 999px;
}
*::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(103, 212, 255, 0.6), rgba(129, 140, 248, 0.52));
border: 2px solid rgba(5, 7, 12, 0.85);
border-radius: 999px;
}
*::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(125, 223, 255, 0.78), rgba(147, 159, 255, 0.72));
}
::selection {
background: rgba(103, 212, 255, 0.35);
}
/* Flex + min-height:0 avoids the last row being clipped when a parent uses overflow:hidden (common xterm.js + FitAddon issue). */
.barecloud-xterm-host .xterm {
flex: 1 1 0;
min-height: 0;
width: 100%;
padding: 0;
}
.barecloud-xterm-host .xterm-viewport {
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: rgba(103, 212, 255, 0.68) rgba(255, 255, 255, 0.04);
}
.barecloud-xterm-host .xterm-viewport::-webkit-scrollbar {
width: 8px;
}
.barecloud-xterm-host .xterm-viewport::-webkit-scrollbar-track {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.025));
border-radius: 999px;
}
.barecloud-xterm-host .xterm-viewport::-webkit-scrollbar-thumb {
background:
linear-gradient(180deg, rgba(103, 212, 255, 0.82), rgba(129, 140, 248, 0.7)),
rgba(103, 212, 255, 0.5);
border: 2px solid rgba(7, 10, 15, 0.92);
border-radius: 999px;
box-shadow: 0 0 10px rgba(103, 212, 255, 0.18);
}
.barecloud-xterm-host .xterm-viewport::-webkit-scrollbar-thumb:hover {
background:
linear-gradient(180deg, rgba(125, 223, 255, 0.95), rgba(147, 159, 255, 0.86)),
rgba(103, 212, 255, 0.65);
}
@keyframes bc-dialog-overlay-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes bc-dialog-overlay-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes bc-dialog-content-in {
from {
opacity: 0;
transform: scale(0.97);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes bc-dialog-content-out {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.97);
}
}
@media (prefers-reduced-motion: reduce) {
.bc-dialog-overlay,
.bc-dialog-content {
animation-duration: 1ms !important;
}
}
+77
View File
@@ -0,0 +1,77 @@
import { Outlet, Link, useLocation } from "react-router-dom";
import { CloudCog } from "lucide-react";
import { PearMark } from "@/components/icons/PearMark";
import { cn } from "@/lib/utils";
export function MainLayout() {
const { pathname } = useLocation();
const isHome = pathname === "/";
return (
<div className="flex h-dvh min-h-0 flex-col overflow-hidden">
<header className="sticky top-0 z-40 shrink-0 border-b border-white/10 bg-black/30 backdrop-blur-xl">
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3 sm:gap-4 sm:px-5 sm:py-4">
<Link to="/" className="flex items-center gap-2 font-semibold tracking-tight">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-white/10 bg-white/5 shadow-inner shadow-black/40">
<CloudCog className="h-5 w-5 text-sky-300" />
</span>
<span>BareCloud</span>
</Link>
<nav className="flex flex-wrap items-center justify-end gap-x-3 gap-y-2 text-xs text-white/75 sm:gap-x-6 sm:text-sm">
<Link className="transition-colors hover:text-white" to="/launch">
Start instance
</Link>
<Link className="transition-colors hover:text-white" to="/community">
Community
</Link>
<Link className="transition-colors hover:text-white" to="/my-booters">
My instances
</Link>
<Link className="transition-colors hover:text-white" to="/stats">
Host stats
</Link>
<a
href="https://pears.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-2.5 py-1.5 text-xs font-medium text-white/80 shadow-sm shadow-black/20 transition-colors hover:border-[#B0D944]/35 hover:bg-[#B0D944]/[0.08] hover:text-white sm:text-sm"
>
<PearMark className="h-[18px] w-[18px] shrink-0" />
<span className="whitespace-nowrap">Built with Pears</span>
</a>
</nav>
</div>
</header>
<main
className={cn(
"mx-auto flex min-h-0 w-full max-w-6xl flex-1 flex-col px-5",
isHome ? "overflow-y-auto py-4 sm:py-8" : "overflow-y-auto py-5 sm:py-10",
)}
>
<Outlet />
</main>
<footer
className={cn(
"shrink-0 border-t border-white/10 text-center text-[11px] text-white/45",
isHome ? "py-2.5 sm:py-3" : "py-7",
)}
>
<div className="mx-auto flex max-w-6xl flex-col items-center gap-1 px-4">
<p className="leading-snug">
BareCloud hosts Bare OS for the community. Instances are removed if not extended within seven days open yours
and tap extend before the deadline to keep them.
</p>
<div className="flex flex-wrap items-center justify-center gap-2 text-white/60 leading-none">
<Link className="transition-colors hover:text-white" to="/terms">
Terms of Service
</Link>
<span aria-hidden></span>
<Link className="transition-colors hover:text-white" to="/privacy">
Privacy Policy
</Link>
</div>
</div>
</footer>
</div>
);
}
+116
View File
@@ -0,0 +1,116 @@
type WsListener<T = unknown> = (payload: T, frame: Record<string, unknown>) => void;
type SubKey = string;
type Subscription = {
key: SubKey;
topic: string;
booterId?: string;
ids?: string[];
listeners: Set<WsListener>;
};
function wsBasePath(): string {
return (import.meta.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "");
}
export function wsMode(): "legacy" | "multiplex" {
const raw = (import.meta.env.NEXT_PUBLIC_WS_MODE ?? "").trim().toLowerCase();
return raw === "multiplex" ? "multiplex" : "legacy";
}
function wsUrl(): string {
const proto = window.location.protocol === "https:" ? "wss" : "ws";
return `${proto}//${window.location.host}${wsBasePath()}/api/ws`;
}
function subKey(topic: string, booterId?: string, ids?: string[]): string {
const idPart = booterId ? `:${booterId}` : "";
const idsPart = ids && ids.length > 0 ? `:${[...ids].sort().join(",")}` : "";
return `${topic}${idPart}${idsPart}`;
}
class AppWsClient {
private ws: WebSocket | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private attempt = 0;
private readonly subs = new Map<SubKey, Subscription>();
private ensureConnected(): void {
if (wsMode() !== "multiplex") return;
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) return;
this.ws = new WebSocket(wsUrl());
this.ws.onopen = () => {
this.attempt = 0;
this.ws?.send(JSON.stringify({ type: "hello", client: "barecloud-web" }));
for (const s of this.subs.values()) {
this.ws?.send(JSON.stringify({ type: "subscribe", topic: s.topic, booterId: s.booterId, ids: s.ids }));
}
};
this.ws.onmessage = (ev) => {
try {
const frame = JSON.parse(String(ev.data)) as Record<string, unknown>;
const topic = typeof frame.type === "string" ? frame.type : "";
if (!topic) return;
for (const s of this.subs.values()) {
const byTopic = s.topic === topic;
const byBooter =
!s.booterId || typeof frame.booterId !== "string" || frame.booterId === s.booterId;
if (!byTopic || !byBooter) continue;
for (const listener of s.listeners) listener(frame.payload, frame);
}
} catch {
/* ignore malformed frames */
}
};
this.ws.onclose = () => {
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
const wait = Math.min(8000, 400 + this.attempt * 350);
this.attempt += 1;
this.reconnectTimer = setTimeout(() => this.ensureConnected(), wait);
};
this.ws.onerror = () => {
/* onclose handles reconnect */
};
}
subscribe<T = unknown>(
topic: string,
listener: WsListener<T>,
opts?: { booterId?: string; ids?: string[] },
): () => void {
if (wsMode() !== "multiplex") {
return () => {
/* no-op in legacy mode */
};
}
const key = subKey(topic, opts?.booterId, opts?.ids);
let sub = this.subs.get(key);
if (!sub) {
sub = { key, topic, booterId: opts?.booterId, ids: opts?.ids, listeners: new Set() };
this.subs.set(key, sub);
this.ensureConnected();
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: "subscribe", topic, booterId: opts?.booterId, ids: opts?.ids }));
}
}
sub.listeners.add(listener as WsListener);
return () => {
const current = this.subs.get(key);
if (!current) return;
current.listeners.delete(listener as WsListener);
if (current.listeners.size > 0) return;
this.subs.delete(key);
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: "unsubscribe", topic, booterId: opts?.booterId }));
}
};
}
}
const singleton = new AppWsClient();
export function appWsClient(): AppWsClient {
return singleton;
}
+15
View File
@@ -0,0 +1,15 @@
/**
* Browser console diagnostics (prefix `[BareCloud:debug]`).
* Uses `console.info` so lines appear under the **Info** level in Chrome (not hidden when “Verbose” is off).
*/
export function barecloudDebug(...args: unknown[]): void {
try {
if (typeof console !== "undefined" && typeof console.info === "function") {
console.info("[BareCloud:debug]", ...args);
} else if (typeof console !== "undefined" && typeof console.log === "function") {
console.log("[BareCloud:debug]", ...args);
}
} catch {
/* ignore */
}
}
+29
View File
@@ -0,0 +1,29 @@
/** Browser-only helpers for remembering instance IDs without accounts. */
const KEY = "barecloud-booters";
export function getStoredBooterIds(): string[] {
if (typeof window === "undefined") return [];
try {
const raw = window.localStorage.getItem(KEY);
if (!raw) return [];
const parsed = JSON.parse(raw) as unknown;
if (!Array.isArray(parsed)) return [];
return parsed.filter((x): x is string => typeof x === "string");
} catch {
return [];
}
}
export function rememberBooterId(id: string): void {
if (typeof window === "undefined") return;
const prev = getStoredBooterIds().filter((x) => x !== id);
prev.unshift(id);
window.localStorage.setItem(KEY, JSON.stringify(prev.slice(0, 64)));
}
export function forgetBooterId(id: string): void {
if (typeof window === "undefined") return;
const next = getStoredBooterIds().filter((x) => x !== id);
window.localStorage.setItem(KEY, JSON.stringify(next));
}
+274
View File
@@ -0,0 +1,274 @@
import { responseJsonObject } from "@/lib/json-response";
export type HostStatsPayload = {
generatedAt: number;
security: { redacted: boolean };
host: {
platform: string;
release: string;
hostnameAlias: string;
distro: string | null;
uptimeSec: number;
cpu: {
usagePct: number;
cores: number;
loadAvg: { one: number; five: number; fifteen: number };
perCore: Array<{ core: string; usagePct: number }>;
};
memory: {
totalBytes: number;
availableBytes: number;
usedBytes: number;
usedPct: number;
swapTotalBytes: number;
swapUsedBytes: number;
swapUsedPct: number;
};
pressure: {
cpu: { avg10: number; avg60: number; avg300: number; total: number } | null;
memory: { avg10: number; avg60: number; avg300: number; total: number } | null;
io: { avg10: number; avg60: number; avg300: number; total: number } | null;
};
filesystems: Array<{
mount: string;
fsType: string;
sizeBytes: number;
usedBytes: number;
availBytes: number;
usedPct: number;
inodesTotal: number;
inodesUsed: number;
inodeUsedPct: number;
}>;
network: Array<{
alias: string;
rxBytes: number;
txBytes: number;
rxRateBps: number;
txRateBps: number;
rxDrops: number;
txDrops: number;
rxErrors: number;
txErrors: number;
}>;
disks: Array<{
alias: string;
readBytes: number;
writeBytes: number;
readBps: number;
writeBps: number;
ioInProgress: number;
ioMs: number;
readsCompleted: number;
writesCompleted: number;
}>;
thermal: Array<{ alias: string; celsius: number }>;
topProcesses: Array<{
alias: string;
cpuPct: number;
memPct: number;
rssBytes: number;
state: string;
command: string;
}>;
};
instances: {
fleet: {
totalBooters: number;
runningBooters: number;
terminalActive: number;
detachedSessions: number;
tmuxSessions: number;
};
booters: Array<{
alias: string;
createdAt: number;
lastAccessed: number;
expiresAt: number;
running: boolean;
terminalSessionActive: boolean;
detachedTmuxSession: boolean;
health: string;
tmux: {
alias: string;
exists: boolean;
attachedClients: number;
windows: number;
};
}>;
};
tmux: {
enabled: boolean;
sessionsDetected: number;
};
};
function num(v: unknown): number {
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) ? n : 0;
}
function bool(v: unknown): boolean {
return Boolean(v);
}
function str(v: unknown): string {
return typeof v === "string" ? v : "";
}
function obj(v: unknown): Record<string, unknown> {
if (v && typeof v === "object" && !Array.isArray(v)) return v as Record<string, unknown>;
return {};
}
function arr(v: unknown): unknown[] {
return Array.isArray(v) ? v : [];
}
export function parseHostStatsPayload(raw: Record<string, unknown>): HostStatsPayload {
const host = obj(raw.host);
const cpu = obj(host.cpu);
const memory = obj(host.memory);
const pressure = obj(host.pressure);
const instances = obj(raw.instances);
const fleet = obj(instances.fleet);
const tmuxRuntime = obj(raw.tmux);
return {
generatedAt: num(raw.generatedAt),
security: { redacted: bool(obj(raw.security).redacted) },
host: {
platform: str(host.platform),
release: str(host.release),
hostnameAlias: str(host.hostnameAlias),
distro: typeof host.distro === "string" ? host.distro : null,
uptimeSec: num(host.uptimeSec),
cpu: {
usagePct: num(cpu.usagePct),
cores: num(cpu.cores),
loadAvg: {
one: num(obj(cpu.loadAvg).one),
five: num(obj(cpu.loadAvg).five),
fifteen: num(obj(cpu.loadAvg).fifteen),
},
perCore: arr(cpu.perCore).map((x) => {
const o = obj(x);
return { core: str(o.core), usagePct: num(o.usagePct) };
}),
},
memory: {
totalBytes: num(memory.totalBytes),
availableBytes: num(memory.availableBytes),
usedBytes: num(memory.usedBytes),
usedPct: num(memory.usedPct),
swapTotalBytes: num(memory.swapTotalBytes),
swapUsedBytes: num(memory.swapUsedBytes),
swapUsedPct: num(memory.swapUsedPct),
},
pressure: {
cpu: pressure.cpu ? (obj(pressure.cpu) as { avg10: number; avg60: number; avg300: number; total: number }) : null,
memory: pressure.memory
? (obj(pressure.memory) as { avg10: number; avg60: number; avg300: number; total: number })
: null,
io: pressure.io ? (obj(pressure.io) as { avg10: number; avg60: number; avg300: number; total: number }) : null,
},
filesystems: arr(host.filesystems).map((x) => {
const o = obj(x);
return {
mount: str(o.mount),
fsType: str(o.fsType),
sizeBytes: num(o.sizeBytes),
usedBytes: num(o.usedBytes),
availBytes: num(o.availBytes),
usedPct: num(o.usedPct),
inodesTotal: num(o.inodesTotal),
inodesUsed: num(o.inodesUsed),
inodeUsedPct: num(o.inodeUsedPct),
};
}),
network: arr(host.network).map((x) => {
const o = obj(x);
return {
alias: str(o.alias),
rxBytes: num(o.rxBytes),
txBytes: num(o.txBytes),
rxRateBps: num(o.rxRateBps),
txRateBps: num(o.txRateBps),
rxDrops: num(o.rxDrops),
txDrops: num(o.txDrops),
rxErrors: num(o.rxErrors),
txErrors: num(o.txErrors),
};
}),
disks: arr(host.disks).map((x) => {
const o = obj(x);
return {
alias: str(o.alias),
readBytes: num(o.readBytes),
writeBytes: num(o.writeBytes),
readBps: num(o.readBps),
writeBps: num(o.writeBps),
ioInProgress: num(o.ioInProgress),
ioMs: num(o.ioMs),
readsCompleted: num(o.readsCompleted),
writesCompleted: num(o.writesCompleted),
};
}),
thermal: arr(host.thermal).map((x) => {
const o = obj(x);
return { alias: str(o.alias), celsius: num(o.celsius) };
}),
topProcesses: arr(host.topProcesses).map((x) => {
const o = obj(x);
return {
alias: str(o.alias),
cpuPct: num(o.cpuPct),
memPct: num(o.memPct),
rssBytes: num(o.rssBytes),
state: str(o.state),
command: str(o.command),
};
}),
},
instances: {
fleet: {
totalBooters: num(fleet.totalBooters),
runningBooters: num(fleet.runningBooters),
terminalActive: num(fleet.terminalActive),
detachedSessions: num(fleet.detachedSessions),
tmuxSessions: num(fleet.tmuxSessions),
},
booters: arr(instances.booters).map((x) => {
const o = obj(x);
const tmux = obj(o.tmux);
return {
alias: str(o.alias),
createdAt: num(o.createdAt),
lastAccessed: num(o.lastAccessed),
expiresAt: num(o.expiresAt),
running: bool(o.running),
terminalSessionActive: bool(o.terminalSessionActive),
detachedTmuxSession: bool(o.detachedTmuxSession),
health: str(o.health),
tmux: {
alias: str(tmux.alias),
exists: bool(tmux.exists),
attachedClients: num(tmux.attachedClients),
windows: num(tmux.windows),
},
};
}),
},
tmux: {
enabled: bool(tmuxRuntime.enabled),
sessionsDetected: num(tmuxRuntime.sessionsDetected),
},
};
}
export async function fetchHostStats(signal?: AbortSignal): Promise<HostStatsPayload> {
const res = await fetch("/api/host-stats", { cache: "no-store", signal });
const json = await responseJsonObject(res);
if (!res.ok) throw new Error(String(json.error ?? "Failed to load host stats"));
return parseHostStatsPayload(json);
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Parse a fetch Response body as JSON. Empty bodies become `{}` so callers avoid
* `res.json()` throwing "Unexpected end of JSON input" on 5xx with no body.
*/
export async function responseJsonObject(res: Response): Promise<Record<string, unknown>> {
const text = await res.text();
const trimmed = text.trim();
if (!trimmed) return {};
try {
const v = JSON.parse(trimmed) as unknown;
if (v === null || typeof v !== "object" || Array.isArray(v)) return {};
return v as Record<string, unknown>;
} catch {
const hint = trimmed.length > 200 ? `${trimmed.slice(0, 200)}` : trimmed;
throw new Error(`Invalid JSON from server (HTTP ${res.status}): ${hint}`);
}
}
+190
View File
@@ -0,0 +1,190 @@
export type ManOption = {
flag: string;
meaning: string;
};
export type ManExample = {
caption?: string;
code: string;
};
export type ManSeeAlso = {
name: string;
section?: number;
};
export type ManPage = {
key: string;
name: string;
section: number;
title: string;
synopsis: string[];
description?: string;
options: ManOption[];
files: string[];
keywords: string[];
examples: ManExample[];
seeAlso: ManSeeAlso[];
bareOsNotes?: string;
listCategory: string;
searchText: string;
};
export type ManIndex = {
schemaVersion?: number;
generatedAt?: string;
pages: ManPage[];
categories: string[];
pageByKey: Map<string, ManPage>;
pagesByName: Map<string, ManPage[]>;
pagesByCategory: Map<string, ManPage[]>;
};
const nameCollator = new Intl.Collator(undefined, {
numeric: true,
sensitivity: "base",
});
function asString(value: unknown): string {
return typeof value === "string" ? value : "";
}
function asNumber(value: unknown, fallback = 1): number {
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.filter((entry): entry is string => typeof entry === "string");
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function normalizePage(value: unknown): ManPage | null {
const page = asRecord(value);
if (!page) return null;
const name = asString(page.name).trim();
const section = asNumber(page.section, 1);
if (!name) return null;
const options = Array.isArray(page.options)
? page.options
.map((option): ManOption | null => {
const record = asRecord(option);
if (!record) return null;
const flag = asString(record.flag).trim();
const meaning = asString(record.meaning).trim();
if (!flag && !meaning) return null;
return { flag, meaning };
})
.filter((option): option is ManOption => option !== null)
: [];
const examples = Array.isArray(page.examples)
? page.examples
.map((example): ManExample | null => {
const record = asRecord(example);
if (!record) return null;
const code = asString(record.code).trim();
const caption = asString(record.caption).trim();
if (!code) return null;
return { code, ...(caption ? { caption } : {}) };
})
.filter((example): example is ManExample => example !== null)
: [];
const seeAlso = Array.isArray(page.seeAlso)
? page.seeAlso
.map((entry): ManSeeAlso | null => {
const record = asRecord(entry);
if (!record) return null;
const relatedName = asString(record.name).trim();
if (!relatedName) return null;
const relatedSection =
typeof record.section === "number" && Number.isFinite(record.section) ? record.section : undefined;
return {
name: relatedName,
...(relatedSection ? { section: relatedSection } : {}),
};
})
.filter((entry): entry is ManSeeAlso => entry !== null)
: [];
const listCategory = asString(page.listCategory).trim() || "uncategorized";
const title = asString(page.title).trim() || `${name} (${section})`;
return {
key: `${name}.${section}`,
name,
section,
title,
synopsis: asStringArray(page.synopsis),
description: asString(page.description).trim() || undefined,
options,
files: asStringArray(page.files),
keywords: asStringArray(page.keywords),
examples,
seeAlso,
bareOsNotes: asString(page.bareOsNotes).trim() || undefined,
listCategory,
searchText: [name, title, ...asStringArray(page.keywords), asString(page.description)]
.join(" ")
.toLowerCase(),
};
}
export function parseManIndex(value: unknown): ManIndex {
const root = asRecord(value);
if (!root) throw new Error("Invalid man index: expected JSON object.");
const rawPages = Array.isArray(root.pages) ? root.pages : [];
const pages = rawPages.map(normalizePage).filter((page): page is ManPage => page !== null);
pages.sort((a, b) => {
const byName = nameCollator.compare(a.name, b.name);
if (byName !== 0) return byName;
if (a.section !== b.section) return a.section - b.section;
return nameCollator.compare(a.key, b.key);
});
const categorySet = new Set<string>();
const pageByKey = new Map<string, ManPage>();
const pagesByName = new Map<string, ManPage[]>();
const pagesByCategory = new Map<string, ManPage[]>();
for (const page of pages) categorySet.add(page.listCategory);
for (const page of pages) {
pageByKey.set(page.key, page);
const byName = pagesByName.get(page.name) ?? [];
byName.push(page);
pagesByName.set(page.name, byName);
const byCategory = pagesByCategory.get(page.listCategory) ?? [];
byCategory.push(page);
pagesByCategory.set(page.listCategory, byCategory);
}
const categories = Array.from(categorySet).sort((a, b) => a.localeCompare(b));
return {
schemaVersion: typeof root.schemaVersion === "number" ? root.schemaVersion : undefined,
generatedAt: asString(root.generatedAt).trim() || undefined,
pages,
categories,
pageByKey,
pagesByName,
pagesByCategory,
};
}
export function searchManPages(index: ManIndex, query: string, category = "all"): ManPage[] {
const needle = query.trim().toLowerCase();
const sourcePages = category === "all" ? index.pages : (index.pagesByCategory.get(category) ?? []);
return sourcePages.filter((page) => {
if (!needle) return true;
return page.searchText.includes(needle);
});
}
+7
View File
@@ -0,0 +1,7 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/** Merge Tailwind classes safely (shadcn-style). */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+25
View File
@@ -0,0 +1,25 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";
/** Runs as soon as the bundle loads — if you never see this, DevTools is on the wrong tab or the wrong origin (use the Vite dev URL in dev, not only the API port). */
if (typeof window !== "undefined" && typeof console !== "undefined") {
const log = console.info ?? console.log;
log.call(console, "[BareCloud] client bundle loaded", {
mode: import.meta.env.MODE,
href: window.location.href,
time: new Date().toISOString(),
});
}
const basename = (import.meta.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "") || undefined;
createRoot(document.getElementById("root")!).render(
<StrictMode>
<BrowserRouter basename={basename}>
<App />
</BrowserRouter>
</StrictMode>,
);
+404
View File
@@ -0,0 +1,404 @@
import { useCallback, useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { format, formatDistanceToNow } from "date-fns";
import {
Activity,
ArrowLeft,
CalendarClock,
Clock3,
Cpu,
Eraser,
RotateCcw,
ShieldOff,
Sparkles,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { KeepAliveButton } from "@/components/KeepAliveButton";
import { Terminal } from "@/components/Terminal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Separator } from "@/components/ui/separator";
import { forgetBooterId, rememberBooterId } from "@/lib/booter-storage";
import { barecloudDebug } from "@/lib/barecloud-debug";
import { responseJsonObject } from "@/lib/json-response";
import { appWsClient, wsMode } from "@/lib/app-ws";
type StatusPayload = {
id: string;
running: boolean;
uptimeSec: number | null;
startedAt: string | null;
lastAccessed: number;
createdAt: number;
expiresAt: number;
terminalSessionActive: boolean;
detachedTmuxSession: boolean;
};
function numField(v: unknown): number {
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) ? n : 0;
}
function numOrNullField(v: unknown): number | null {
if (v === null || v === undefined) return null;
if (typeof v === "number" && Number.isFinite(v)) return v;
if (typeof v === "string" && v.trim() !== "") {
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
return null;
}
function strOrNullField(v: unknown): string | null {
return typeof v === "string" ? v : null;
}
function statusPayloadFromJson(json: Record<string, unknown>): StatusPayload {
return {
id: typeof json.id === "string" ? json.id : "",
running: Boolean(json.running),
uptimeSec: numOrNullField(json.uptimeSec),
startedAt: strOrNullField(json.startedAt),
lastAccessed: numField(json.lastAccessed),
createdAt: numField(json.createdAt),
expiresAt: numField(json.expiresAt),
terminalSessionActive: Boolean(json.terminalSessionActive),
detachedTmuxSession: Boolean(json.detachedTmuxSession),
};
}
export function BooterPage() {
const { id = "" } = useParams<{ id: string }>();
const navigate = useNavigate();
const [status, setStatus] = useState<StatusPayload | null>(null);
const [loading, setLoading] = useState(true);
const [restarting, setRestarting] = useState(false);
const [clearingCorestore, setClearingCorestore] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [clearDialogOpen, setClearDialogOpen] = useState(false);
/** Increment after restart / storage clear so `<Terminal>` remounts with a fresh session. */
const [terminalResetKey, setTerminalResetKey] = useState(0);
useEffect(() => {
if (typeof window === "undefined") return;
const sp = new URLSearchParams(window.location.search);
if (sp.get("new") !== "1") return;
rememberBooterId(id);
toast.success("Save this link — you will need it to open your instance again.");
const base = (import.meta.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "");
window.history.replaceState({}, "", `${base}/booter/${encodeURIComponent(id)}`);
}, [id]);
const refresh = useCallback(async () => {
try {
const statusUrl = `/api/status/${encodeURIComponent(id)}?touch=1`;
const res = await fetch(statusUrl, { cache: "no-store" });
const json = await responseJsonObject(res);
if (!res.ok) throw new Error(String(json.error ?? "Failed to load status"));
const payload = statusPayloadFromJson(json);
barecloudDebug("BooterPage /api/status", {
routeBooterId: id,
responseStatus: res.status,
statusUrl,
serverId: payload.id,
idMatchesRoute: payload.id === id,
running: payload.running,
terminalSessionActive: payload.terminalSessionActive,
detachedTmuxSession: payload.detachedTmuxSession,
});
setStatus(payload);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Status refresh failed");
} finally {
setLoading(false);
}
}, [id]);
useEffect(() => {
void refresh();
if (wsMode() === "multiplex") {
const off = appWsClient().subscribe(
"booter.status",
(payload) => {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
const next = statusPayloadFromJson(payload as Record<string, unknown>);
if (!next.id || next.id !== id) return;
setStatus(next);
setLoading(false);
},
{ booterId: id },
);
return () => off();
}
const t = window.setInterval(() => void refresh(), 5000);
return () => window.clearInterval(t);
}, [refresh, id]);
const uptimeLabel =
status?.terminalSessionActive
? "Console connected"
: status?.detachedTmuxSession
? "Session saved — open the console to continue"
: status?.running && typeof status.uptimeSec === "number"
? (() => {
const total = status.uptimeSec;
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (h > 0) return `${h}h ${m}m`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
})()
: status?.running
? "Starting…"
: "Not running";
return (
<div className="flex min-h-dvh flex-col overflow-y-auto overflow-x-hidden bg-[#05070c] lg:h-dvh lg:max-h-dvh lg:overflow-hidden">
<header className="shrink-0 border-b border-white/10 bg-black/35 backdrop-blur-xl">
<div className="mx-auto flex max-w-[1400px] flex-wrap items-center justify-between gap-3 px-4 py-3 sm:gap-4 sm:px-5 sm:py-4">
<div className="flex items-center gap-3">
<Button asChild variant="secondary" size="sm" className="rounded-xl">
<Link to="/">
<ArrowLeft className="h-4 w-4" />
Home
</Link>
</Button>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h1 className="truncate text-lg font-semibold tracking-tight">Bare OS instance</h1>
{status?.running ? (
<Badge variant="success">
<Activity className="mr-1 inline h-3 w-3" />
{status.terminalSessionActive ? "Live" : "Running"}
</Badge>
) : (
<Badge variant="warning">
<Cpu className="mr-1 inline h-3 w-3" />
Not running
</Badge>
)}
</div>
<p className="truncate font-mono text-xs text-white/50">{id}</p>
</div>
</div>
<div className="w-full sm:w-auto">
<KeepAliveButton booterId={id} onExtended={() => void refresh()} />
</div>
</div>
</header>
<div className="mx-auto flex min-h-0 w-full max-w-[1600px] flex-1 flex-col gap-4 overflow-visible px-3 py-3 sm:px-4 sm:py-4 lg:flex-row lg:items-stretch lg:gap-5 lg:overflow-hidden lg:px-5 lg:py-5">
<section className="flex min-h-[min(58dvh,680px)] min-w-0 flex-1 flex-col gap-3 overflow-hidden lg:min-h-0">
<div className="flex shrink-0 flex-col gap-1 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 className="text-base font-semibold tracking-tight text-white">Console</h2>
<p className="text-xs text-white/55">
This is your live Bare OS session. Type here as you would in any terminal; resize the window if you need
more space.
</p>
</div>
<Badge variant="default" className="w-fit shrink-0">
Interactive
</Badge>
</div>
<div className="relative min-h-0 min-w-0 flex-1 overflow-hidden">
<Terminal key={`${id}-${terminalResetKey}`} booterId={id} />
</div>
</section>
<aside className="flex min-h-0 w-full min-w-0 shrink-0 flex-col gap-3 overflow-x-hidden overflow-y-visible lg:max-h-[calc(100dvh-5.75rem)] lg:h-full lg:w-[300px] lg:overflow-y-auto lg:overflow-x-hidden lg:pr-1">
<Card className="min-w-0 shrink-0">
<CardHeader className="space-y-2 p-4 pb-2 sm:p-5 sm:pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Sparkles className="h-4 w-4 shrink-0 text-sky-300" />
Lifecycle
</CardTitle>
<CardDescription className="text-xs leading-snug text-white/58 sm:text-sm">
Kept for seven days after each extend. Tap <span className="text-white/75">Extend seven more days</span>{" "}
before the deadline or this instance and its data are removed.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2.5 p-4 pt-0 text-sm text-white/70 sm:p-5 sm:pt-0">
<div className="flex items-start gap-2">
<Clock3 className="mt-0.5 h-4 w-4 text-white/45" />
<div>
<div className="text-xs text-white/45">Last activity</div>
<div className="text-white/85">
{loading || !status ? "…" : formatDistanceToNow(status.lastAccessed, { addSuffix: true })}
</div>
</div>
</div>
<Separator />
<div className="flex items-start gap-2">
<Activity className="mt-0.5 h-4 w-4 text-white/45" />
<div>
<div className="text-xs text-white/45">Status</div>
<div className="text-white/85">{loading ? "…" : uptimeLabel}</div>
</div>
</div>
<Separator />
<div className="flex items-start gap-2">
<CalendarClock className="mt-0.5 h-4 w-4 text-amber-300/80" />
<div>
<div className="text-xs text-white/45">Keep until</div>
<div className="text-white/85">
{loading || !status ? (
"…"
) : (
<>
{format(status.expiresAt, "yyyy-MM-dd HH:mm")}
<span className="block text-[11px] text-white/50">
({formatDistanceToNow(status.expiresAt, { addSuffix: true })})
</span>
</>
)}
</div>
</div>
</div>
<Separator />
<div className="text-xs text-white/45">Created</div>
<div className="font-mono text-xs text-white/75">
{loading || !status ? "…" : format(status.createdAt, "yyyy-MM-dd HH:mm")}
</div>
{!loading && status?.id && status.id !== id ? (
<p className="mt-2 text-xs leading-snug text-amber-200/95">
The server returned a different instance id than this page URL. Refresh the page; if the console
still does not match the id above, check that{" "}
<span className="font-mono">NEXT_PUBLIC_TERMINAL_WS_URL</span> includes{" "}
<span className="font-mono">{"{id}"}</span>.
</p>
) : null}
</CardContent>
</Card>
<Card className="min-w-0 shrink-0">
<CardHeader className="space-y-2 p-4 pb-2 sm:p-5 sm:pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<ShieldOff className="h-4 w-4 shrink-0 text-indigo-300" />
Controls
</CardTitle>
<CardDescription className="text-xs leading-snug text-white/58 sm:text-sm">
Clear storage wipes this instance's Corestore data and reboots Bare OS on this link. Corestore blocks
are encrypted on disk. Delete removes the instance entirely.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-2 p-4 pt-0 sm:p-5 sm:pt-0">
<Button
type="button"
variant="secondary"
className="w-full"
disabled={restarting || clearingCorestore}
title={
status?.terminalSessionActive
? "While the console is open, a brief disconnect is normal when you restart."
: "Stops and starts the Bare OS process for this instance."
}
onClick={async () => {
setRestarting(true);
try {
const res = await fetch(`/api/booters/${encodeURIComponent(id)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "restart" }),
});
const json = await responseJsonObject(res);
if (!res.ok) throw new Error(String((json as { error?: string }).error ?? "Restart failed"));
toast.success("Instance restarted");
setTerminalResetKey((k) => k + 1);
await refresh();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Restart failed");
} finally {
setRestarting(false);
}
}}
>
<RotateCcw className={`mr-2 h-4 w-4 ${restarting ? "animate-spin" : ""}`} />
{restarting ? "Restarting…" : "Restart instance"}
</Button>
<Button
type="button"
variant="destructive"
className="w-full"
disabled={clearingCorestore || restarting}
onClick={() => setDeleteDialogOpen(true)}
>
Delete instance
</Button>
<Button
type="button"
variant="outline"
className="w-full border-amber-500/35 text-amber-100 hover:bg-amber-950/40 hover:text-amber-50"
disabled={restarting || clearingCorestore}
title="Removes all stored data for this instance and starts Bare OS again. Your link stays the same."
onClick={() => setClearDialogOpen(true)}
>
<Eraser className={`mr-2 h-4 w-4 ${clearingCorestore ? "animate-pulse" : ""}`} />
{clearingCorestore ? "Clearing…" : "Clear storage"}
</Button>
<Button asChild variant="secondary" className="w-full">
<Link to="/my-booters">Back to my instances</Link>
</Button>
</CardContent>
</Card>
</aside>
</div>
<ConfirmDialog
open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen}
title="Delete this instance?"
description="This permanently removes the server-side instance and all of its data. Your browser may still remember the old link until you clear it from “My instances”."
icon={<Trash2 className="h-5 w-5" />}
confirmLabel="Delete permanently"
cancelLabel="Keep instance"
tone="destructive"
onConfirm={async () => {
const res = await fetch(`/api/booters/${encodeURIComponent(id)}`, { method: "DELETE" });
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error((json as { error?: string }).error || "Delete failed");
forgetBooterId(id);
toast.success("Instance deleted");
navigate("/my-booters");
}}
/>
<ConfirmDialog
open={clearDialogOpen}
onOpenChange={setClearDialogOpen}
title="Clear all storage?"
description="This erases this instance's Corestore data and boots Bare OS fresh on the same link. Corestore blocks are encrypted on disk. It cannot be undone."
icon={<Eraser className="h-5 w-5" />}
confirmLabel="Clear storage"
cancelLabel="Cancel"
tone="warning"
pending={clearingCorestore}
onConfirm={async () => {
setClearingCorestore(true);
try {
const res = await fetch(`/api/booters/${encodeURIComponent(id)}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "clear_corestore" }),
});
const json = await responseJsonObject(res);
if (!res.ok) throw new Error(String((json as { error?: string }).error ?? "Clear failed"));
toast.success("Storage cleared");
setTerminalResetKey((k) => k + 1);
await refresh();
} catch (e) {
toast.error(e instanceof Error ? e.message : "Clear failed");
throw e;
} finally {
setClearingCorestore(false);
}
}}
/>
</div>
);
}
+79
View File
@@ -0,0 +1,79 @@
import { ExternalLink, Gift, GitBranch, Terminal } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const DISCORD_INVITE = "https://join.discord-linux.com";
export function CommunityPage() {
return (
<div className="mx-auto flex w-full max-w-4xl flex-col gap-8">
<section className="rounded-3xl border border-white/10 bg-gradient-to-b from-white/[0.08] to-white/[0.02] p-6 shadow-2xl shadow-black/40 sm:p-8">
<div className="space-y-4">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-sky-300/90">Community</p>
<h1 className="max-w-3xl text-3xl font-semibold tracking-tight text-white sm:text-4xl">
Build with people who love Linux, open source, and shipping real projects.
</h1>
<p className="max-w-3xl text-sm leading-relaxed text-white/70 sm:text-base">
Join our Discord coding community to collaborate, ask questions, and help shape the Bare OS ecosystem. It is open
to everyone, whether you are just getting started or already deep in systems work.
</p>
<div className="pt-2">
<Button asChild size="lg" className="rounded-2xl px-7">
<a href={DISCORD_INVITE} target="_blank" rel="noopener noreferrer">
Join the Discord server
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
</div>
</section>
<section className="grid grid-cols-1 gap-4 md:grid-cols-3">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Gift className="h-4 w-4 text-emerald-300" />
Free Linux hosting
</CardTitle>
<CardDescription>
Community members can access real Linux hosting at no cost for learning, building, and testing.
</CardDescription>
</CardHeader>
<CardContent className="text-sm leading-relaxed text-white/70">
Build in public, share your experiments, and get feedback from peers who actively run Linux workloads.
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<GitBranch className="h-4 w-4 text-violet-300" />
Git server access
</CardTitle>
<CardDescription>
The server is where you can register for our git platform and start contributing to Bare OS.
</CardDescription>
</CardHeader>
<CardContent className="text-sm leading-relaxed text-white/70">
Get set up, publish your work, and open contributions with guidance from maintainers and other contributors.
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<Terminal className="h-4 w-4 text-sky-300" />
Coding-first community
</CardTitle>
<CardDescription>
Discussions stay practical: troubleshooting, architecture, tooling, and project collaboration.
</CardDescription>
</CardHeader>
<CardContent className="text-sm leading-relaxed text-white/70">
If you want to learn faster and contribute to meaningful open-source infrastructure, this is the place.
</CardContent>
</Card>
</section>
</div>
);
}
+138
View File
@@ -0,0 +1,138 @@
import type { ReactNode } from "react";
import { Link } from "react-router-dom";
import { ArrowRight, BookOpen, ExternalLink, Shield, TerminalSquare } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
const BARE_OS_REPO = "https://git.ssh.surf/snxraven/bare-operating-system";
type Accent = "sky" | "emerald" | "violet";
const accentBar: Record<Accent, string> = {
sky: "from-sky-400 via-cyan-300/70 to-transparent",
emerald: "from-emerald-400 via-teal-300/70 to-transparent",
violet: "from-violet-400 via-indigo-300/70 to-transparent",
};
const accentIcon: Record<Accent, string> = {
sky: "bg-sky-500/[0.12] text-sky-200 ring-1 ring-sky-400/20 shadow-[0_0_28px_-6px_rgba(56,189,248,0.45)]",
emerald: "bg-emerald-500/[0.12] text-emerald-200 ring-1 ring-emerald-400/20 shadow-[0_0_28px_-6px_rgba(52,211,153,0.4)]",
violet: "bg-violet-500/[0.12] text-violet-200 ring-1 ring-violet-400/25 shadow-[0_0_28px_-6px_rgba(167,139,250,0.35)]",
};
function HomeFeatureCard({
accent,
icon,
title,
body,
foot,
}: {
accent: Accent;
icon: ReactNode;
title: string;
body: string;
foot: string;
}) {
return (
<article className="group relative flex min-h-0 min-w-0 flex-col overflow-hidden rounded-2xl border border-white/[0.12] bg-gradient-to-b from-white/[0.08] to-white/[0.02] shadow-lg shadow-black/30 backdrop-blur-xl transition-[border-color,box-shadow] duration-300 hover:border-white/[0.18] hover:shadow-xl hover:shadow-black/40">
<div
className={`pointer-events-none h-1 w-full shrink-0 bg-gradient-to-r ${accentBar[accent]} opacity-90`}
aria-hidden
/>
<div className="flex min-h-0 flex-1 flex-col gap-4 p-4 sm:p-5">
<div
className={`inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl transition-transform duration-300 group-hover:scale-[1.03] ${accentIcon[accent]}`}
>
{icon}
</div>
<div className="min-h-0 space-y-2">
<h3 className="text-base font-semibold tracking-tight text-white sm:text-lg">{title}</h3>
<p className="text-xs leading-relaxed text-white/65 sm:text-sm sm:leading-relaxed">{body}</p>
</div>
</div>
<div className="shrink-0 border-t border-white/[0.06] bg-black/25 px-4 py-3 sm:px-5">
<p className="text-[11px] leading-snug text-white/55 sm:text-xs sm:leading-relaxed">{foot}</p>
</div>
</article>
);
}
export function HomePage() {
return (
<div className="flex min-h-0 flex-col gap-3 sm:h-full sm:gap-5 md:gap-6">
<section className="relative min-h-0 shrink-0 overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03] p-4 shadow-2xl shadow-black/40 backdrop-blur-xl sm:rounded-3xl sm:p-7 md:p-9">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(600px_300px_at_20%_0%,rgba(56,189,248,0.25),transparent_60%)]" />
<div className="relative flex h-full min-h-0 flex-col gap-3 sm:gap-5">
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<Badge variant="success">Free to use</Badge>
<Badge variant="default">No sign-up</Badge>
<Badge variant="warning">Extend every 7 days</Badge>
</div>
<div className="min-h-0 flex-1 space-y-3 overflow-visible sm:flex-none sm:space-y-0">
<h1 className="line-clamp-3 max-w-3xl text-balance text-3xl font-semibold tracking-tight text-white sm:line-clamp-none sm:text-4xl md:text-5xl">
Bare OS in your browser a peer-to-peer operating system, ready when you are.
</h1>
<p className="line-clamp-4 max-w-2xl text-sm leading-relaxed text-white/70 sm:line-clamp-none sm:text-base">
<span className="text-white/85">Bare OS</span> is a system image that lives on a Hyperdrive and reaches the world
through Hyperswarm built for decentralized distribution with Pear.{" "}
<span className="text-white/85">BareCloud</span> hosts personal instances you open from a link: each one keeps its
own state, boots over the network, and must be extended at least every seven days or it is removed automatically.
</p>
<p className="pt-1">
<a
href={BARE_OS_REPO}
target="_blank"
rel="noopener noreferrer"
className="inline-flex max-w-full items-center gap-1.5 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-medium text-sky-200/95 transition-colors hover:border-sky-400/30 hover:bg-sky-500/10 hover:text-white sm:text-sm"
>
<span className="truncate">Bare OS source &amp; docs on Git</span>
<ExternalLink className="h-3.5 w-3.5 shrink-0 opacity-70" aria-hidden />
</a>
</p>
</div>
<div className="flex shrink-0 flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<Button asChild size="lg" className="rounded-2xl px-6 text-base sm:px-8">
<Link to="/launch">
Start an instance
<ArrowRight className="h-5 w-5" />
</Link>
</Button>
<Button asChild variant="secondary" size="lg" className="rounded-2xl">
<Link to="/my-booters">My instances</Link>
</Button>
<Button asChild variant="secondary" size="lg" className="rounded-2xl">
<Link to="/run-locally">Run The OS Locally</Link>
</Button>
<Button asChild variant="secondary" size="lg" className="rounded-2xl">
<Link to="/user-manual">User Manual</Link>
</Button>
</div>
</div>
</section>
<section className="grid grid-cols-1 gap-3 sm:gap-4 md:min-h-0 md:flex-1 md:grid-cols-3 md:grid-rows-1 md:gap-5">
<HomeFeatureCard
accent="sky"
icon={<TerminalSquare className="h-5 w-5" strokeWidth={1.75} />}
title="Real console"
body="A full interactive session in the browser — type commands, resize the window, copy output, and work the way you would in a local terminal from your own machine."
foot="Best on a laptop or desktop. Your session is private to the link you were given."
/>
<HomeFeatureCard
accent="emerald"
icon={<Shield className="h-5 w-5" strokeWidth={1.75} />}
title="Isolated instance"
body="Every link is its own Bare OS environment with separate Corestore storage, and blocks are encrypted on disk. Nothing is shared between instances."
foot="There are no accounts: you keep the URL. If you lose it, we cannot recover access for you."
/>
<HomeFeatureCard
accent="violet"
icon={<BookOpen className="h-5 w-5" strokeWidth={1.75} />}
title="Fair use"
body="After seven days without an extension, instances are removed automatically so capacity stays available for everyone and new users can keep launching links without waiting."
foot='Open your instance before the deadline and tap "Extend seven more days" to add another week.'
/>
</section>
</div>
);
}
+140
View File
@@ -0,0 +1,140 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Sparkles } from "lucide-react";
import { toast } from "sonner";
import { barecloudDebug } from "@/lib/barecloud-debug";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function LaunchPage() {
const navigate = useNavigate();
const [submitting, setSubmitting] = useState(false);
const [startOpen, setStartOpen] = useState(false);
/**
* `POST /api/booters` with JSON body. Optional `NEXT_PUBLIC_PEAR_BOOT_LINK` pins the Pear app link at launch
* (otherwise the server uses `BARECLOUD_PEAR_BOOT_LINK` / its default). Each instance stores its link in SQLite.
*/
async function launch() {
setSubmitting(true);
try {
const pearBootLink = import.meta.env.NEXT_PUBLIC_PEAR_BOOT_LINK?.trim();
const launchBody = pearBootLink ? { pearBootLink } : {};
barecloudDebug("LaunchPage POST /api/booters", {
launchBody,
hasPublicPearLink: Boolean(pearBootLink),
});
const res = await fetch("/api/booters", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(launchBody),
});
const data = (await res.json().catch(() => ({}))) as {
error?: string;
retryAfterSec?: number;
id?: string;
};
barecloudDebug("LaunchPage POST /api/booters response", {
httpStatus: res.status,
ok: res.ok,
id: data.id,
error: data.error,
retryAfterSec: data.retryAfterSec,
});
if (!res.ok) {
const retry = data.retryAfterSec ? ` Try again in about ${data.retryAfterSec}s.` : "";
toast.error(`${data.error ?? "Could not start an instance"}${retry}`);
return;
}
if (data.id) {
barecloudDebug("LaunchPage navigating to booter", { id: data.id });
setStartOpen(false);
navigate(`/booter/${encodeURIComponent(data.id)}?new=1`);
}
} catch (err) {
toast.error(err instanceof Error ? err.message : "Could not start an instance");
} finally {
setSubmitting(false);
}
}
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-6 sm:gap-8">
<div className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight">Start a Bare OS instance</h1>
<p className="text-sm leading-relaxed text-white/65">
We create a fresh environment and open your console. Save the link you get next it is the only key to this
instance. Bare OS loads its image from peers on the network; if nothing answers in time, try again once seeders are
available.
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Ready when you are</CardTitle>
<CardDescription>
One click provisions your instance and takes you straight to the dashboard. No password and no email.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<Button type="button" className="w-full sm:w-auto" disabled={submitting} onClick={() => setStartOpen(true)}>
{submitting ? "Starting…" : "Start instance"}
</Button>
<Link className="text-center text-sm text-white/55 hover:text-white sm:text-left" to="/my-booters">
Already have a link? Open my instances
</Link>
</CardContent>
</Card>
<Dialog open={startOpen} onOpenChange={(open) => !submitting && setStartOpen(open)}>
<DialogContent
className="max-w-[440px] sm:max-w-md"
disableCloseButton={submitting}
onPointerDownOutside={(e) => submitting && e.preventDefault()}
onEscapeKeyDown={(e) => submitting && e.preventDefault()}
>
<div className="flex gap-3 px-4 pb-1 pr-12 pt-5 sm:gap-4 sm:px-6 sm:pr-14 sm:pt-6">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-sky-500/30 bg-sky-950/40 text-sky-200">
<Sparkles className="h-5 w-5" />
</div>
<DialogHeader className="flex-1 gap-3 p-0 pr-0">
<DialogTitle>Start a new instance?</DialogTitle>
<DialogDescription asChild>
<div className="space-y-3 text-sm leading-relaxed text-white/65">
<p className="text-white/70">
We will provision a fresh Bare OS environment and take you to the live console.
</p>
<ul className="list-disc space-y-1.5 pl-4 text-white/60">
<li>Save the URL you get next it is the only key to this instance.</li>
<li>There is no account; we only remember links in this browser if you revisit them.</li>
<li>Images load from peers; if the network is quiet, try again in a minute.</li>
<li>
Each instance must be extended at least every seven days from the dashboard otherwise it is removed
automatically.
</li>
</ul>
</div>
</DialogDescription>
</DialogHeader>
</div>
<DialogFooter className="mt-2">
<Button type="button" variant="secondary" disabled={submitting} onClick={() => setStartOpen(false)}>
Not now
</Button>
<Button type="button" disabled={submitting} onClick={() => void launch()}>
{submitting ? "Starting…" : "Start now"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
+180
View File
@@ -0,0 +1,180 @@
import { useEffect, useMemo, useState } from "react";
import { Link } from "react-router-dom";
import { Link2Off, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { BooterCard, type BooterSummary } from "@/components/BooterCard";
import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { forgetBooterId, getStoredBooterIds } from "@/lib/booter-storage";
import { responseJsonObject } from "@/lib/json-response";
import { appWsClient, wsMode } from "@/lib/app-ws";
export function MyBootersPage() {
const [ids, setIds] = useState<string[]>([]);
const [booters, setBooters] = useState<BooterSummary[]>([]);
const [loading, setLoading] = useState(true);
const [clearSavedOpen, setClearSavedOpen] = useState(false);
const [forgetId, setForgetId] = useState<string | null>(null);
useEffect(() => {
setIds(getStoredBooterIds());
}, []);
const qs = useMemo(() => {
if (ids.length === 0) return "";
const p = new URLSearchParams();
p.set("ids", ids.join(","));
return `?${p.toString()}`;
}, [ids]);
useEffect(() => {
let cancelled = false;
async function run() {
if (wsMode() === "multiplex") {
setLoading(false);
return;
}
if (!qs) {
setBooters([]);
setLoading(false);
return;
}
setLoading(true);
try {
const res = await fetch(`/api/booters${qs}`, { cache: "no-store" });
const json = await responseJsonObject(res);
if (!res.ok) throw new Error(String(json.error ?? "Failed to load"));
const rows = (json.booters ?? []) as BooterSummary[];
if (!cancelled) setBooters(rows);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Could not refresh the list");
} finally {
if (!cancelled) setLoading(false);
}
}
void run();
return () => {
cancelled = true;
};
}, [qs]);
useEffect(() => {
if (wsMode() !== "multiplex") return;
if (ids.length === 0) {
setBooters([]);
setLoading(false);
return;
}
setLoading(true);
const off = appWsClient().subscribe(
"booter.list",
(payload) => {
if (!Array.isArray(payload)) return;
setBooters(payload as BooterSummary[]);
setLoading(false);
},
{ ids },
);
return () => off();
}, [ids]);
return (
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div className="space-y-2">
<h1 className="text-3xl font-semibold tracking-tight">My instances</h1>
<p className="max-w-2xl text-sm leading-relaxed text-white/65">
Links you have opened on this browser are listed here automatically. There is no account if you use another
device or clear site data, paste your saved URL to add it again.
</p>
</div>
<Button asChild variant="secondary">
<Link to="/launch">Start another instance</Link>
</Button>
</div>
{loading ? (
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-6 py-10 text-center text-sm text-white/60 backdrop-blur">
Loading your saved links
</div>
) : ids.length === 0 ? (
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-6 py-10 text-center text-sm text-white/65 backdrop-blur">
Nothing saved here yet. Start an instance and we will remember the link on this device.
</div>
) : booters.length === 0 ? (
<div className="rounded-2xl border border-amber-400/25 bg-amber-950/35 px-6 py-10 text-center text-sm text-amber-50 backdrop-blur">
These links are still on this device, but the server no longer has matching instances ({ids.length} saved). They may
have expired after a period without activity. You can start a new instance whenever you like.
<div className="mt-4 flex justify-center gap-2">
<Button type="button" variant="secondary" onClick={() => setClearSavedOpen(true)}>
Clear saved links
</Button>
</div>
</div>
) : (
<div className="grid gap-5">
{booters.map((b) => (
<div key={b.id} className="flex flex-col gap-3">
<BooterCard booter={b} />
<div className="flex justify-end">
<Button
type="button"
variant="ghost"
className="text-xs text-white/45 hover:text-white"
onClick={() => setForgetId(b.id)}
>
Forget this link on this device
</Button>
</div>
</div>
))}
</div>
)}
<ConfirmDialog
open={clearSavedOpen}
onOpenChange={setClearSavedOpen}
title="Clear saved links?"
description="This only removes URLs stored in this browser. It does not delete anything on the server. You can paste a link again later if you still have it."
icon={<Trash2 className="h-5 w-5" />}
confirmLabel="Clear from this device"
cancelLabel="Keep links"
tone="warning"
onConfirm={async () => {
ids.forEach((x) => forgetBooterId(x));
setIds([]);
toast.message("Removed saved links from this browser");
}}
/>
<ConfirmDialog
open={forgetId !== null}
onOpenChange={(open) => {
if (!open) setForgetId(null);
}}
title="Forget this link?"
description={
forgetId ? (
<>
Remove{" "}
<span className="rounded-md bg-white/[0.08] px-1.5 py-0.5 font-mono text-[11px] text-white/80">
{forgetId}
</span>{" "}
from saved links on this device only. The instance on the server is unchanged.
</>
) : null
}
icon={<Link2Off className="h-5 w-5" />}
confirmLabel="Forget on this device"
cancelLabel="Cancel"
tone="default"
onConfirm={async () => {
if (!forgetId) return;
forgetBooterId(forgetId);
setIds(getStoredBooterIds());
toast.message("Removed from this browser only");
}}
/>
</div>
);
}
+146
View File
@@ -0,0 +1,146 @@
import { Link } from "react-router-dom";
const LAST_UPDATED = "April 24, 2026";
export function PrivacyPage() {
return (
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
<section className="rounded-3xl border border-white/10 bg-gradient-to-b from-white/[0.08] to-white/[0.02] p-6 shadow-2xl shadow-black/40 sm:p-8">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-sky-300/90">Legal</p>
<h1 className="mt-3 text-3xl font-semibold tracking-tight text-white sm:text-4xl">Privacy Policy</h1>
<p className="mt-3 text-sm text-white/70 sm:text-base">
Last updated: {LAST_UPDATED}. This Privacy Policy explains how BareCloud handles information when you use our
platform.
</p>
</section>
<section className="rounded-3xl border border-white/10 bg-black/25 p-5 sm:p-6">
<h2 className="text-sm font-semibold uppercase tracking-[0.14em] text-white/70">Contents</h2>
<div className="mt-3 grid grid-cols-1 gap-2 text-sm text-sky-200 sm:grid-cols-2">
<a href="#scope" className="hover:text-white">1. Scope</a>
<a href="#data-collected" className="hover:text-white">2. Information We Collect</a>
<a href="#how-used" className="hover:text-white">3. How We Use Information</a>
<a href="#local-storage" className="hover:text-white">4. Local Browser Storage</a>
<a href="#retention" className="hover:text-white">5. Retention and Deletion</a>
<a href="#sharing" className="hover:text-white">6. Sharing and Third Parties</a>
<a href="#security" className="hover:text-white">7. Security</a>
<a href="#choices" className="hover:text-white">8. Your Choices</a>
<a href="#children" className="hover:text-white">9. Children&apos;s Privacy</a>
<a href="#transfers" className="hover:text-white">10. International Use</a>
<a href="#changes" className="hover:text-white">11. Policy Changes</a>
<a href="#contact" className="hover:text-white">12. Contact</a>
</div>
</section>
<section id="scope" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">1. Scope</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
This Privacy Policy applies to BareCloud website, APIs, and related instance-hosting services. It describes the
handling of technical and operational data associated with your use of the platform.
</p>
</section>
<section id="data-collected" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">2. Information We Collect</h2>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-white/75 sm:text-base">
<li>Technical request metadata, including IP-related headers used for operation and abuse controls.</li>
<li>Instance identifiers and lifecycle timestamps needed to run and manage your instance.</li>
<li>Terminal session and websocket operational metadata (for connection/session control).</li>
<li>Service logs and diagnostics used for reliability, troubleshooting, and security operations.</li>
</ul>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud currently does not rely on traditional account registration for access to instances.
</p>
</section>
<section id="how-used" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">3. How We Use Information</h2>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-white/75 sm:text-base">
<li>Operate and deliver instance lifecycle workflows (launch, status, extension, restart, deletion).</li>
<li>Apply fair-use controls and launch-rate limiting.</li>
<li>Maintain platform integrity, reliability, and abuse prevention.</li>
<li>Diagnose and remediate service issues.</li>
</ul>
</section>
<section id="local-storage" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">4. Local Browser Storage</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud may store instance identifiers in your browser local storage to help you quickly reopen previously used
instances. This local storage is controlled by your browser and device, and you can clear it at any time.
</p>
</section>
<section id="retention" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">5. Retention and Deletion</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud applies retention windows to instances and associated operational records. Instances not extended in time
may be removed automatically by scheduled cleanup. Manual deletion or data-clearing actions may also remove related
runtime data.
</p>
</section>
<section id="sharing" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">6. Sharing and Third Parties</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
We do not sell personal information. BareCloud may interact with external infrastructure providers and upstream
content hosts to deliver product features (for example, upstream man page resource retrieval). Information may be
disclosed if required by law, legal process, or to protect platform security.
</p>
</section>
<section id="security" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">7. Security</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud uses technical and operational safeguards to reduce risk, including input validation, session controls, and
service monitoring. No system can guarantee absolute security, so you should avoid storing highly sensitive data in
transient instances unless you have your own protective controls.
</p>
</section>
<section id="choices" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">8. Your Choices</h2>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-white/75 sm:text-base">
<li>You may delete instances using platform controls where available.</li>
<li>You may clear browser local storage to remove locally remembered instance identifiers.</li>
<li>You may stop using the service at any time.</li>
</ul>
</section>
<section id="children" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">9. Children&apos;s Privacy</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud is not directed to children under 13. If you believe a child has provided information in a way that
conflicts with applicable law, contact us and we will take reasonable steps to review and address the issue.
</p>
</section>
<section id="transfers" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">10. International Use</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud may be accessed from multiple jurisdictions and may process information in infrastructure located outside
your region. By using the service, you understand that data handling may occur in locations with different legal
frameworks.
</p>
</section>
<section id="changes" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">11. Policy Changes</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
We may update this Privacy Policy as the platform evolves. Revised versions are effective when posted with an updated
date.
</p>
</section>
<section id="contact" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">12. Contact</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
For privacy questions or requests, use BareCloud support/community contact channels listed on this website.
</p>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
Related policy: <Link to="/terms" className="text-sky-200 hover:text-white">Terms of Service</Link>.
</p>
</section>
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
import { Copy, TerminalSquare } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
const LOCAL_RUN_COMMANDS = `npm i pear -g
pear run pear://xc5xw4odfpd7txd1b3xr4u37rfpbjoqtb6zp3xxutgbbmrbtogio`;
export function RunLocallyPage() {
const [copied, setCopied] = useState(false);
async function handleCopy() {
try {
await navigator.clipboard.writeText(LOCAL_RUN_COMMANDS);
setCopied(true);
setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
}
return (
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
<section className="rounded-3xl border border-white/10 bg-gradient-to-b from-white/[0.08] to-white/[0.02] p-6 shadow-2xl shadow-black/40 sm:p-8">
<div className="space-y-4">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-sky-300/90">Run Bare OS locally</p>
<h1 className="max-w-3xl text-3xl font-semibold tracking-tight text-white sm:text-4xl">
Start Bare OS on your own machine with Pear.
</h1>
<p className="max-w-3xl text-sm leading-relaxed text-white/70 sm:text-base">
Node.js is required and includes npm. After Node.js is installed, install Pear globally, then run the Bare OS
peer URL directly from your terminal.
</p>
</div>
</section>
<section className="rounded-3xl border border-white/10 bg-black/40 p-5 shadow-2xl shadow-black/35 sm:p-6">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="inline-flex items-center gap-2 text-sm font-medium text-white/85">
<TerminalSquare className="h-4 w-4 text-sky-300" />
Terminal commands
</div>
<Button type="button" variant="secondary" size="sm" className="rounded-xl" onClick={handleCopy}>
<Copy className="h-4 w-4" />
{copied ? "Copied" : "Copy"}
</Button>
</div>
<pre className="overflow-x-auto rounded-2xl border border-white/10 bg-black/60 p-4 text-sm text-sky-100">
<code>{LOCAL_RUN_COMMANDS}</code>
</pre>
</section>
</div>
);
}
+324
View File
@@ -0,0 +1,324 @@
import { useEffect, useMemo, useState } from "react";
import { formatDistanceToNow } from "date-fns";
import { Activity, AlertCircle, Cpu, HardDrive, MemoryStick, Network } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { parseHostStatsPayload, type HostStatsPayload } from "@/lib/hostStats";
import { appWsClient, wsMode } from "@/lib/app-ws";
function fmtBytes(v: number): string {
if (!Number.isFinite(v) || v <= 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB", "PB"];
let value = v;
let i = 0;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i += 1;
}
return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[i]}`;
}
function fmtPct(v: number): string {
return `${Number.isFinite(v) ? v.toFixed(1) : "0.0"}%`;
}
function fmtAgo(ms: number): string {
return ms > 0 ? formatDistanceToNow(ms, { addSuffix: true }) : "n/a";
}
function SummaryCard({
title,
value,
hint,
icon,
}: {
title: string;
value: string;
hint: string;
icon: React.ReactNode;
}) {
return (
<Card>
<CardHeader className="pb-2">
<CardDescription className="flex items-center gap-2 text-white/65">
{icon}
{title}
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-2xl font-semibold text-white">{value}</div>
<div className="mt-1 text-xs text-white/55">{hint}</div>
</CardContent>
</Card>
);
}
export function StatsPage() {
const [stats, setStats] = useState<HostStatsPayload | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastOkAt, setLastOkAt] = useState<number | null>(null);
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
if (wsMode() === "multiplex") {
const off = appWsClient().subscribe("host.stats", (payload) => {
const parsed = parseHostStatsPayload((payload ?? {}) as Record<string, unknown>);
setStats(parsed);
setLastOkAt(Date.now());
setLoading(false);
setError(null);
});
return () => off();
}
let closed = false;
let ws: WebSocket | null = null;
let reconnectTimer = 0;
let reconnectAttempt = 0;
const connect = () => {
if (closed) return;
const basePath = (import.meta.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/$/, "");
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
const wsUrl = `${scheme}://${window.location.host}${basePath}/api/host-stats/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
reconnectAttempt = 0;
setError(null);
};
ws.onmessage = (ev) => {
try {
const parsed = JSON.parse(String(ev.data)) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return;
const obj = parsed as Record<string, unknown>;
if (typeof obj.error === "string") {
setError(obj.error);
return;
}
const payload = parseHostStatsPayload(obj);
setStats(payload);
setLastOkAt(Date.now());
setLoading(false);
setError(null);
} catch {
setError("Invalid host stats message");
}
};
ws.onerror = () => {
setError("Host stats WebSocket error");
};
ws.onclose = () => {
if (closed) return;
setError("Host stats disconnected, reconnecting...");
const waitMs = Math.min(8000, 800 * Math.max(1, reconnectAttempt + 1));
reconnectAttempt += 1;
reconnectTimer = window.setTimeout(connect, waitMs);
};
};
connect();
return () => {
closed = true;
if (reconnectTimer) window.clearTimeout(reconnectTimer);
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) ws.close();
};
}, []);
useEffect(() => {
const t = window.setInterval(() => setNowMs(Date.now()), 1000);
return () => window.clearInterval(t);
}, []);
const stale = useMemo(() => {
if (!lastOkAt) return false;
return nowMs - lastOkAt > 15_000;
}, [lastOkAt, nowMs]);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-3xl font-semibold tracking-tight">Host Stats</h1>
<p className="mt-2 max-w-3xl text-sm text-white/65">
Live BareCloud host telemetry with redacted instance/session identifiers. No access-capable ids or links are
displayed on this page.
</p>
</div>
<div className="flex items-center gap-2">
{stats?.security.redacted ? <Badge variant="success">Redaction enabled</Badge> : <Badge variant="warning">Check redaction</Badge>}
{stale ? <Badge variant="warning">Data stale</Badge> : <Badge variant="default">Live polling</Badge>}
</div>
</div>
{error ? (
<Card className="border-red-500/30">
<CardContent className="flex items-center gap-3 p-5 text-red-100">
<AlertCircle className="h-5 w-5" />
<span>{error}</span>
</CardContent>
</Card>
) : null}
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
<SummaryCard
title="CPU"
value={loading || !stats ? "..." : fmtPct(stats.host.cpu.usagePct)}
hint={loading || !stats ? "Loading..." : `${stats.host.cpu.cores} cores · load1 ${stats.host.cpu.loadAvg.one.toFixed(2)}`}
icon={<Cpu className="h-4 w-4" />}
/>
<SummaryCard
title="Memory"
value={loading || !stats ? "..." : fmtPct(stats.host.memory.usedPct)}
hint={loading || !stats ? "Loading..." : `${fmtBytes(stats.host.memory.usedBytes)} / ${fmtBytes(stats.host.memory.totalBytes)}`}
icon={<MemoryStick className="h-4 w-4" />}
/>
<SummaryCard
title="Instances"
value={loading || !stats ? "..." : `${stats.instances.fleet.runningBooters}/${stats.instances.fleet.totalBooters}`}
hint={loading || !stats ? "Loading..." : `${stats.instances.fleet.tmuxSessions} tmux sessions · ${stats.instances.fleet.terminalActive} terminal active`}
icon={<Activity className="h-4 w-4" />}
/>
</div>
<div className="grid gap-4 xl:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>System</CardTitle>
<CardDescription>
{loading || !stats
? "Loading system metadata..."
: `${stats.host.distro ?? stats.host.platform} ${stats.host.release} · ${stats.host.hostnameAlias}`}
</CardDescription>
</CardHeader>
<CardContent className="grid gap-2 text-sm text-white/80">
<div>Uptime: {loading || !stats ? "..." : formatDistanceToNow(Date.now() - stats.host.uptimeSec * 1000)}</div>
<div>Swap used: {loading || !stats ? "..." : `${fmtBytes(stats.host.memory.swapUsedBytes)} (${fmtPct(stats.host.memory.swapUsedPct)})`}</div>
<div>Last update: {lastOkAt ? fmtAgo(lastOkAt) : "n/a"}</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>TMUX Runtime</CardTitle>
<CardDescription>BareCloud session persistence is tmux-based.</CardDescription>
</CardHeader>
<CardContent className="grid gap-2 text-sm text-white/80">
<div>Enabled: {loading || !stats ? "..." : stats.tmux.enabled ? "yes" : "no"}</div>
<div>Sessions detected: {loading || !stats ? "..." : stats.tmux.sessionsDetected}</div>
<div>
Fleet tmux sessions: {loading || !stats ? "..." : `${stats.instances.fleet.tmuxSessions} active instance sessions`}
</div>
</CardContent>
</Card>
</div>
<div className="grid gap-4 xl:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive className="h-4 w-4" />
Filesystems
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{(stats?.host.filesystems ?? []).slice(0, 10).map((fs) => (
<div key={`${fs.mount}-${fs.fsType}`} className="rounded-lg border border-white/10 p-3 text-xs text-white/75">
<div className="font-mono text-white/90">{fs.mount}</div>
<div>{fs.fsType}</div>
<div>
{fmtBytes(fs.usedBytes)} / {fmtBytes(fs.sizeBytes)} ({fmtPct(fs.usedPct)})
</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Network className="h-4 w-4" />
Network Interfaces
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{(stats?.host.network ?? []).slice(0, 10).map((iface) => (
<div key={iface.alias} className="rounded-lg border border-white/10 p-3 text-xs text-white/75">
<div className="font-mono text-white/90">{iface.alias}</div>
<div>
RX {fmtBytes(iface.rxRateBps)}/s · TX {fmtBytes(iface.txRateBps)}/s
</div>
<div>
Errors {iface.rxErrors + iface.txErrors} · Drops {iface.rxDrops + iface.txDrops}
</div>
</div>
))}
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Booters & tmux sessions (redacted)</CardTitle>
<CardDescription>Operational status only. Access-capable identifiers are intentionally hidden.</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{(stats?.instances.booters ?? []).slice(0, 30).map((b) => (
<div key={b.alias} className="grid gap-2 rounded-xl border border-white/10 p-3 text-xs text-white/80 md:grid-cols-6">
<div className="font-mono text-white/90">{b.alias}</div>
<div>{b.running ? "running" : "inactive"}</div>
<div>{b.terminalSessionActive ? "terminal attached" : b.detachedTmuxSession ? "detached tmux" : "no session"}</div>
<div>{b.tmux.exists ? `${b.tmux.attachedClients} attached` : "tmux absent"}</div>
<div>expires {fmtAgo(b.expiresAt)}</div>
<div>last active {fmtAgo(b.lastAccessed)}</div>
</div>
))}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Top Processes</CardTitle>
<CardDescription>Sorted by CPU usage. Container runtime processes are excluded.</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full min-w-[680px] text-left text-xs text-white/80">
<thead className="text-white/55">
<tr className="border-b border-white/10">
<th className="py-2 pr-3 font-medium">Process</th>
<th className="py-2 pr-3 font-medium">State</th>
<th className="py-2 pr-3 font-medium">CPU</th>
<th className="py-2 pr-3 font-medium">Memory</th>
<th className="py-2 pr-0 font-medium">RSS</th>
</tr>
</thead>
<tbody>
{(stats?.host.topProcesses ?? []).map((p) => (
<tr key={p.alias} className="border-b border-white/5 align-top">
<td className="py-2 pr-3">
<div className="max-w-[340px] truncate text-white/90">{p.command || "unknown"}</div>
</td>
<td className="py-2 pr-3">
<span className="rounded-md border border-white/15 bg-white/5 px-1.5 py-0.5 font-mono text-[11px]">
{p.state}
</span>
</td>
<td className="py-2 pr-3 font-mono">{fmtPct(p.cpuPct)}</td>
<td className="py-2 pr-3 font-mono">{fmtPct(p.memPct)}</td>
<td className="py-2 pr-0 font-mono">{fmtBytes(p.rssBytes)}</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</div>
);
}
+155
View File
@@ -0,0 +1,155 @@
import { Link } from "react-router-dom";
const LAST_UPDATED = "April 24, 2026";
export function TermsPage() {
return (
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
<section className="rounded-3xl border border-white/10 bg-gradient-to-b from-white/[0.08] to-white/[0.02] p-6 shadow-2xl shadow-black/40 sm:p-8">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-sky-300/90">Legal</p>
<h1 className="mt-3 text-3xl font-semibold tracking-tight text-white sm:text-4xl">Terms of Service</h1>
<p className="mt-3 text-sm text-white/70 sm:text-base">
Last updated: {LAST_UPDATED}. These Terms of Service govern your use of BareCloud and related Bare OS hosting
services.
</p>
</section>
<section className="rounded-3xl border border-white/10 bg-black/25 p-5 sm:p-6">
<h2 className="text-sm font-semibold uppercase tracking-[0.14em] text-white/70">Contents</h2>
<div className="mt-3 grid grid-cols-1 gap-2 text-sm text-sky-200 sm:grid-cols-2">
<a href="#service" className="hover:text-white">1. Service Overview</a>
<a href="#eligibility" className="hover:text-white">2. Eligibility</a>
<a href="#acceptable-use" className="hover:text-white">3. Acceptable Use</a>
<a href="#fair-use" className="hover:text-white">4. Fair Use and Limits</a>
<a href="#instance-lifecycle" className="hover:text-white">5. Instance Lifecycle</a>
<a href="#user-responsibilities" className="hover:text-white">6. User Responsibilities</a>
<a href="#suspension" className="hover:text-white">7. Suspension and Termination</a>
<a href="#disclaimers" className="hover:text-white">8. Disclaimers</a>
<a href="#liability" className="hover:text-white">9. Limitation of Liability</a>
<a href="#indemnity" className="hover:text-white">10. Indemnification</a>
<a href="#law-venue" className="hover:text-white">11. Governing Law and Venue</a>
<a href="#changes" className="hover:text-white">12. Changes to Terms</a>
<a href="#contact" className="hover:text-white">13. Contact</a>
</div>
</section>
<section id="service" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">1. Service Overview</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud provides browser-accessible Bare OS instances for community and development use. The service uses a
link-based access model instead of traditional account authentication. By accessing or using the service, you agree
to these Terms.
</p>
</section>
<section id="eligibility" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">2. Eligibility</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
You represent that you are legally able to enter into this agreement and to use the service in compliance with
applicable law. If you use BareCloud on behalf of an organization, you represent that you are authorized to bind
that organization.
</p>
</section>
<section id="acceptable-use" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">3. Acceptable Use</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">You agree not to use BareCloud to:</p>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-white/75 sm:text-base">
<li>violate law, regulations, or third-party rights;</li>
<li>distribute malware or conduct unauthorized intrusion, scanning, or abuse;</li>
<li>interfere with service integrity, stability, or availability;</li>
<li>use the platform in ways that unreasonably burden shared community resources.</li>
</ul>
</section>
<section id="fair-use" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">4. Fair Use and Limits</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud uses fair-use controls, including launch throttling and operational safeguards. Launch requests may be
temporarily limited (for example, within rolling time windows) to protect platform stability for all users.
</p>
</section>
<section id="instance-lifecycle" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">5. Instance Lifecycle</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
Instances are not permanent by default. BareCloud applies retention windows and cleanup processes. Unless extended,
instances may be removed automatically after the published retention period. Deletion and cleanup actions may remove
associated runtime state and data.
</p>
</section>
<section id="user-responsibilities" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">6. User Responsibilities</h2>
<ul className="list-disc space-y-1 pl-5 text-sm leading-relaxed text-white/75 sm:text-base">
<li>You are responsible for maintaining control of your instance links.</li>
<li>You are responsible for actions performed through your links or active sessions.</li>
<li>You should export or preserve any important work before retention deadlines.</li>
<li>You should not rely on BareCloud as your sole backup or archival system.</li>
</ul>
</section>
<section id="suspension" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">7. Suspension and Termination</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud may suspend, restrict, or terminate access (including specific instances) if we reasonably believe use is
abusive, unlawful, harmful to platform operations, or otherwise violates these Terms.
</p>
</section>
<section id="disclaimers" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">8. Disclaimers</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
BareCloud is provided on an &quot;as is&quot; and &quot;as available&quot; basis. We do not guarantee uninterrupted
availability, error-free operation, or fitness for any specific purpose. We may modify, pause, or discontinue parts
of the service at any time.
</p>
</section>
<section id="liability" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">9. Limitation of Liability</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
To the maximum extent permitted by law, BareCloud and its operators are not liable for indirect, incidental,
special, consequential, or punitive damages, or for loss of data, profits, or business opportunities arising from
use of or inability to use the service.
</p>
</section>
<section id="indemnity" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">10. Indemnification</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
You agree to defend, indemnify, and hold harmless BareCloud and its operators from claims, losses, and expenses
arising out of your use of the service or your violation of these Terms.
</p>
</section>
<section id="law-venue" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">11. Governing Law and Venue</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
These Terms are governed by the laws of the State of Georgia, without regard to conflict-of-laws principles. You
agree that courts located in DeKalb County, Georgia are the exclusive venue for disputes arising out of or relating
to these Terms or the service.
</p>
</section>
<section id="changes" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">12. Changes to Terms</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
We may update these Terms from time to time. Updated versions become effective when posted. Continued use of
BareCloud after updates means you accept the revised Terms.
</p>
</section>
<section id="contact" className="space-y-3 rounded-3xl border border-white/10 bg-black/20 p-6">
<h2 className="text-xl font-semibold text-white">13. Contact</h2>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
For legal or policy questions, contact BareCloud support through project-maintained community channels and the
platform links published on this site.
</p>
<p className="text-sm leading-relaxed text-white/75 sm:text-base">
Related policy: <Link to="/privacy" className="text-sky-200 hover:text-white">Privacy Policy</Link>.
</p>
</section>
</div>
);
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly NEXT_PUBLIC_BASE_PATH?: string;
readonly NEXT_PUBLIC_APP_ORIGIN?: string;
readonly NEXT_PUBLIC_TERMINAL_WS_URL?: string;
/** Optional `pear://…` sent as `pearBootLink` on `POST /api/booters` so new instances use your app key. */
readonly NEXT_PUBLIC_PEAR_BOOT_LINK?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"jsx": "react-jsx",
"isolatedModules": true,
"resolveJsonModule": true,
"types": ["vite/client"],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
}
+40
View File
@@ -0,0 +1,40 @@
import path from "path";
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
const rawBase = (process.env.NEXT_PUBLIC_BASE_PATH ?? process.env.BARECLOUD_BASE_PATH ?? "").trim();
const base = rawBase ? (rawBase.endsWith("/") ? rawBase : `${rawBase}/`) : "/";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, path.join(__dirname, ".."), "");
const apiTarget = env.BARECLOUD_API_PROXY ?? `http://127.0.0.1:${env.PORT ?? "3000"}`;
return {
root: __dirname,
base,
envDir: path.join(__dirname, ".."),
envPrefix: ["VITE_", "NEXT_PUBLIC_"],
plugins: [react(), tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
build: {
outDir: "dist",
emptyOutDir: true,
sourcemap: false,
},
server: {
port: 5173,
proxy: {
"/api": {
target: apiTarget,
changeOrigin: true,
ws: true,
},
},
},
};
});