const { useState, useEffect, useCallback, useRef } = React;
const TOKEN_KEY = "jimportal_token";
const getToken = () => window.sessionStorage.getItem(TOKEN_KEY);
const setToken = (t) => window.sessionStorage.setItem(TOKEN_KEY, t);
const clearToken = () => window.sessionStorage.removeItem(TOKEN_KEY);
async function api(path, { method = "GET", body } = {}) {
const headers = { "Content-Type": "application/json" };
const t = getToken();
if (t) headers["Authorization"] = `Bearer ${t}`;
const res = await fetch(`/api/v1${path}`, {
method, headers, body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401) { clearToken(); window.dispatchEvent(new Event("bp-unauth")); throw new Error("Session expired."); }
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Request failed (${res.status})`);
return data;
}
function Cube({ size = 26 }) {
return (
);
}
function Toast({ msg }) { return msg ?
{msg}
: null; }
const STATUS_LABEL = { creating: "creating", active: "active", suspended: "suspended", error: "error", deleted: "deleted" };
function StatusPill({ status }) {
const cls = status === "active" ? "st-running" : status === "creating" ? "st-installing"
: status === "error" ? "st-error" : "st-queued";
return {STATUS_LABEL[status] || status};
}
/* ── Auth ── */
function AuthScreen({ onAuthed }) {
const [mode, setMode] = useState("login");
const [email, setEmail] = useState(""); const [password, setPassword] = useState("");
const [err, setErr] = useState(""); const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault(); setErr(""); setBusy(true);
try {
const path = mode === "login" ? "/auth/login" : "/auth/register";
const { access_token } = await api(path, { method: "POST", body: { email, password } });
setToken(access_token); onAuthed();
} catch (e) { setErr(e.message); } finally { setBusy(false); }
};
return (
jimshosting
{mode === "login" ? "Sign in" : "Create your free account"}
{err &&
{err}
}
{mode === "login" ? "New here? " : "Already have an account? "}
);
}
/* ── Create free server ── */
function CreateFree({ onClose, onCreated, notify }) {
const [name, setName] = useState("");
const [types, setTypes] = useState(null);
const [eggId, setEggId] = useState(null);
const [version, setVersion] = useState("");
const [err, setErr] = useState(""); const [busy, setBusy] = useState(false);
useEffect(() => {
(async () => {
try {
const t = await api("/server-types");
setTypes(t);
if (t.length) setEggId(t[0].egg_id);
} catch (e) { setErr(e.message); setTypes([]); }
})();
}, []);
const submit = async (e) => {
e.preventDefault(); setErr(""); setBusy(true);
try {
await api("/servers", { method: "POST",
body: { name, egg_id: eggId, mc_version: version || null } });
notify("Free server is being created!"); onCreated(); onClose();
} catch (e) { setErr(e.message); } finally { setBusy(false); }
};
const selected = types && types.find((t) => t.egg_id === eggId);
return (
e.stopPropagation()}>
Create your free server
);
}
/* ── Server card ── */
function ServerCard({ server, onDelete, onManage, notify }) {
const connect = server.connect_address;
const copy = () => { navigator.clipboard.writeText(connect).then(() => notify("Address copied.")); };
return (
{(server.memory_mb / 1024)}G
RAM
{(server.disk_mb / 1024)}G
Disk
{server.type_label || server.tier}
Type
{connect && server.status === "active" ? (
{connect}
) : server.status === "creating" ? (
Building your server… this takes a minute or two.
) : server.status === "error" ? (
{server.error_msg || "Something went wrong."}
) : null}
);
}
function EmptyState({ onCreate }) {
return (
You don't have a server yet
Spin up your free Minecraft server — ready in a couple minutes.
);
}
/* ── Admin panel ── */
function AdminServers({ notify }) {
const [rows, setRows] = useState(null);
const load = useCallback(async () => {
try { setRows(await api("/admin/servers")); } catch (e) { notify(e.message); }
}, [notify]);
useEffect(() => { load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, [load]);
if (rows === null) return Loading…
;
if (rows.length === 0) return No servers yet.
;
return (
| Server | Owner | Status | Node |
Allocation | Address |
{rows.map((s) => (
| {s.name} |
{s.owner_email} |
|
{s.node_id ?? "—"} |
{s.allocation ?? "—"} |
{s.connect_address ?? "—"} |
))}
);
}
function AdminUsers({ me, notify }) {
const [rows, setRows] = useState(null);
const load = useCallback(async () => {
try { setRows(await api("/admin/users")); } catch (e) { notify(e.message); }
}, [notify]);
useEffect(() => { load(); }, [load]);
const patch = async (id, body) => {
try { await api(`/admin/users/${id}`, { method: "PATCH", body }); notify("Updated."); load(); }
catch (e) { notify(e.message); }
};
const delUser = async (u) => {
if (!window.confirm(`Delete ${u.email}? This removes their account AND destroys their servers.`)) return;
try { await api(`/admin/users/${u.id}`, { method: "DELETE" }); notify("User deleted."); load(); }
catch (e) { notify(e.message); }
};
const setQuota = (u) => {
const v = window.prompt(`Free-server limit for ${u.email}:`, u.max_free_servers);
if (v === null) return;
const n = parseInt(v, 10);
if (Number.isNaN(n) || n < 0) { notify("Enter a non-negative number."); return; }
patch(u.id, { max_free_servers: n });
};
if (rows === null) return Loading…
;
return (
| Email | Servers | Limit | Admin | Actions |
{rows.map((u) => (
| {u.email}{u.id === me.id && you} |
{u.active_servers} |
{u.max_free_servers} |
{u.is_admin ? admin : user} |
|
))}
);
}
function AdminPanel({ me, notify }) {
const [sub, setSub] = useState("servers");
return (
Admin
Manage all servers and users.
{sub === "servers" ?
:
}
);
}
function Dashboard({ me, onLogout, reloadMe }) {
const [tab, setTab] = useState("mine");
const [servers, setServers] = useState(null);
const [showCreate, setShowCreate] = useState(false);
const [manageServer, setManageServer] = useState(null);
const [toast, setToast] = useState("");
const notify = useCallback((m) => { setToast(m); setTimeout(() => setToast(""), 2600); }, []);
const load = useCallback(async () => {
try { setServers(await api("/servers")); reloadMe(); } catch (e) { notify(e.message); }
}, [notify, reloadMe]);
useEffect(() => { if (tab === "mine") { load(); const t = setInterval(load, 5000); return () => clearInterval(t); } }, [load, tab]);
const onDelete = async (server) => {
if (!window.confirm(`Delete "${server.name}"? This permanently removes the server and its world.`)) return;
try { await api(`/servers/${server.id}`, { method: "DELETE" }); notify("Server deleted."); load(); }
catch (e) { notify(e.message); }
};
const atLimit = me.active_servers >= me.max_free_servers;
return (
jimshosting
{me.is_admin && }
{me.email}
{me.is_admin && ADMIN}
{tab === "admin" && me.is_admin ? (
) : (
Your server
Free tier · {me.active_servers}/{me.max_free_servers} used
{servers === null ?
Loading…
: servers.length === 0 ?
setShowCreate(true)} />
: {servers.map((s) => )}
}
)}
{showCreate &&
setShowCreate(false)} onCreated={load} notify={notify} />}
{manageServer && setManageServer(null)} notify={notify} />}
);
}
function App() {
const [me, setMe] = useState(undefined);
const loadMe = useCallback(async () => {
if (!getToken()) { setMe(null); return; }
try { setMe(await api("/auth/me")); } catch { clearToken(); setMe(null); }
}, []);
useEffect(() => {
loadMe();
const onUnauth = () => setMe(null);
window.addEventListener("bp-unauth", onUnauth);
return () => window.removeEventListener("bp-unauth", onUnauth);
}, [loadMe]);
const logout = () => { clearToken(); setMe(null); };
if (me === undefined) return Loading…
;
if (me === null) return ;
return ;
}
/* Full server management view — tabbed, Pterodactyl-parity.
Loaded after app.jsx; uses the same global `api`, `getToken`, helpers. */
function fmtBytes(mb) {
if (mb == null) return "—";
if (mb >= 1024) return (mb / 1024).toFixed(1) + " GB";
return mb + " MB";
}
function fmtSize(bytes) {
if (bytes == null) return "—";
const u = ["B", "KB", "MB", "GB"]; let i = 0; let n = bytes;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return n.toFixed(i ? 1 : 0) + " " + u[i];
}
/* ── Console tab (live) ── */
function ConsoleTab({ server, notify }) {
const [lines, setLines] = useState([]);
const [stats, setStats] = useState(null);
const [state, setState] = useState("connecting");
const [cmd, setCmd] = useState("");
const [history, setHistory] = useState([]);
const wsRef = useRef(null);
const logRef = useRef(null);
const append = useCallback((t) => setLines((p) => {
const n = [...p, t]; return n.length > 800 ? n.slice(-800) : n;
}), []);
useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss:" : "ws:";
const url = `${proto}//${window.location.host}/api/v1/servers/${server.id}/console?token=${encodeURIComponent(getToken())}`;
const ws = new WebSocket(url); wsRef.current = ws;
ws.onmessage = (ev) => {
let m; try { m = JSON.parse(ev.data); } catch { return; }
const { event, args } = m;
if (event === "auth success") {
setState((s) => s === "connecting" ? "running" : s);
ws.send(JSON.stringify({ event: "send logs", args: [null] }));
ws.send(JSON.stringify({ event: "send stats", args: [null] }));
} else if (event === "console output" && args) append(args[0]);
else if (event === "status" && args) setState(args[0]);
else if (event === "stats" && args) {
try { const s = JSON.parse(args[0]); setStats({
cpu: s.cpu_absolute, mem: Math.round(s.memory_bytes / 1048576),
disk: Math.round(s.disk_bytes / 1048576), state: s.state,
netrx: s.network?.rx_bytes, nettx: s.network?.tx_bytes, uptime: s.uptime,
}); setState(s.state); } catch {}
} else if (event === "portal error" && args) { append(`[portal] ${args[0]}`); setState("error"); }
};
ws.onclose = () => setState((s) => s === "error" ? s : "disconnected");
ws.onerror = () => setState("error");
return () => { try { ws.close(); } catch {} };
}, [server.id, append]);
useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [lines]);
const power = async (signal) => {
try { await api(`/servers/${server.id}/power`, { method: "POST", body: { signal } }); notify(`Sent ${signal}.`); }
catch (e) { notify(e.message); }
};
const send = (e) => {
e.preventDefault(); if (!cmd.trim()) return;
const ws = wsRef.current;
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ event: "send command", args: [cmd] }));
setHistory((h) => [...h, cmd]); setCmd("");
};
const sc = state === "running" ? "on" : state === "error" ? "err" : "off";
return (
{state}
{stats && (
{stats.cpu?.toFixed(0) ?? 0}%
CPU
{fmtBytes(stats.mem)}
Memory
{fmtBytes(stats.disk)}
Disk
{stats.uptime ? Math.floor(stats.uptime / 60000) + "m" : "—"}
Uptime
)}
{lines.length === 0 ?
Connecting to console…
: lines.map((l, i) =>
{l}
)}
);
}
/* ── Files tab ── */
function FilesTab({ server, notify }) {
const [dir, setDir] = useState("/");
const [items, setItems] = useState(null);
const [editing, setEditing] = useState(null);
const [content, setContent] = useState("");
const load = useCallback(async () => {
try { setItems(await api(`/servers/${server.id}/files?directory=${encodeURIComponent(dir)}`)); }
catch (e) { notify(e.message); }
}, [server.id, dir, notify]);
useEffect(() => { load(); }, [load]);
const open = async (f) => {
const path = (dir === "/" ? "" : dir) + "/" + f.name;
if (!f.is_file) { setDir(path); return; }
if (f.size > 2 * 1024 * 1024) { notify("File too large to edit inline."); return; }
try { const r = await api(`/servers/${server.id}/files/contents?file=${encodeURIComponent(path)}`);
setContent(r.content); setEditing(path); } catch (e) { notify(e.message); }
};
const save = async () => {
try { await api(`/servers/${server.id}/files/write`, { method: "POST", body: { file: editing, content } });
notify("Saved."); setEditing(null); } catch (e) { notify(e.message); }
};
const del = async (f) => {
if (!window.confirm(`Delete ${f.name}?`)) return;
try { await api(`/servers/${server.id}/files/delete`, { method: "POST", body: { root: dir, files: [f.name] } });
notify("Deleted."); load(); } catch (e) { notify(e.message); }
};
const up = () => { if (dir === "/") return; setDir(dir.split("/").slice(0, -1).join("/") || "/"); };
if (editing !== null) {
return (
{editing}
);
}
return (
{dir}
{items === null ?
Loading…
: (
{items.length === 0 ?
Empty directory.
: items.map((f) => (
{f.is_file ? fmtSize(f.size) : ""}
))}
)}
);
}
/* ── Backups tab ── */
function BackupsTab({ server, notify }) {
const [items, setItems] = useState(null);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
try { setItems(await api(`/servers/${server.id}/backups`)); } catch (e) { notify(e.message); }
}, [server.id, notify]);
useEffect(() => { load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, [load]);
const create = async () => {
setBusy(true);
try { await api(`/servers/${server.id}/backups`, { method: "POST", body: { name: "" } });
notify("Backup started."); load(); } catch (e) { notify(e.message); } finally { setBusy(false); }
};
const restore = async (b) => {
if (!window.confirm(`Restore "${b.name}"? This overwrites current files.`)) return;
try { await api(`/servers/${server.id}/backups/${b.uuid}/restore`, { method: "POST" }); notify("Restore started."); }
catch (e) { notify(e.message); }
};
const del = async (b) => {
if (!window.confirm(`Delete backup "${b.name}"?`)) return;
try { await api(`/servers/${server.id}/backups/${b.uuid}`, { method: "DELETE" }); notify("Deleted."); load(); }
catch (e) { notify(e.message); }
};
const download = async (b) => {
try { const r = await api(`/servers/${server.id}/backups/${b.uuid}/download`); window.open(r.url, "_blank"); }
catch (e) { notify(e.message); }
};
return (
{items === null ?
Loading…
: items.length === 0 ?
No backups yet.
: (
| Name | Size | Status | Actions |
{items.map((b) => (
| {b.name} |
{fmtSize(b.bytes)} |
{b.is_successful ? done : …} |
|
))}
)}
);
}
/* ── Startup tab ── */
function StartupTab({ server, notify }) {
const [data, setData] = useState(null);
const load = useCallback(async () => {
try { setData(await api(`/servers/${server.id}/startup`)); } catch (e) { notify(e.message); }
}, [server.id, notify]);
useEffect(() => { load(); }, [load]);
const save = async (key, value) => {
try { await api(`/servers/${server.id}/startup`, { method: "PUT", body: { key, value } }); notify("Saved."); load(); }
catch (e) { notify(e.message); }
};
if (data === null) return Loading…
;
return (
{data.startup_command}
{data.variables.map((v) => (
))}
);
}
function VarRow({ v, onSave }) {
const [val, setVal] = useState(v.value ?? "");
return (
);
}
/* ── Schedules tab ── */
function SchedulesTab({ server, notify }) {
const [items, setItems] = useState(null);
const [show, setShow] = useState(false);
const load = useCallback(async () => {
try { setItems(await api(`/servers/${server.id}/schedules`)); } catch (e) { notify(e.message); }
}, [server.id, notify]);
useEffect(() => { load(); }, [load]);
const del = async (s) => {
if (!window.confirm(`Delete schedule "${s.name}"?`)) return;
try { await api(`/servers/${server.id}/schedules/${s.id}`, { method: "DELETE" }); notify("Deleted."); load(); }
catch (e) { notify(e.message); }
};
return (
{items === null ?
Loading…
: items.length === 0 ?
No schedules.
: (
| Name | Cron | Active | Next run | |
{items.map((s) => (
| {s.name} |
{s.cron ? `${s.cron.minute} ${s.cron.hour} ${s.cron.day_of_month} ${s.cron.month} ${s.cron.day_of_week}` : "—"} |
{s.active ? on : off} |
{s.next_run_at ? new Date(s.next_run_at).toLocaleString() : "—"} |
|
))}
)}
{show &&
setShow(false)} onCreated={load} notify={notify} />}
);
}
function ScheduleCreate({ server, onClose, onCreated, notify }) {
const [f, setF] = useState({ name: "", minute: "0", hour: "*/6", day_of_month: "*", month: "*", day_of_week: "*" });
const set = (k) => (e) => setF({ ...f, [k]: e.target.value });
const submit = async (e) => {
e.preventDefault();
try { await api(`/servers/${server.id}/schedules`, { method: "POST", body: { ...f, is_active: true } });
notify("Schedule created."); onCreated(); onClose(); } catch (e) { notify(e.message); }
};
return (
e.stopPropagation()}>
New schedule
);
}
/* ── Subusers tab ── */
function SubusersTab({ server, notify }) {
const [items, setItems] = useState(null);
const [email, setEmail] = useState("");
const load = useCallback(async () => {
try { setItems(await api(`/servers/${server.id}/subusers`)); } catch (e) { notify(e.message); }
}, [server.id, notify]);
useEffect(() => { load(); }, [load]);
const add = async (e) => {
e.preventDefault(); if (!email.trim()) return;
try { await api(`/servers/${server.id}/subusers`, { method: "POST", body: { email } });
notify("Subuser added."); setEmail(""); load(); } catch (e) { notify(e.message); }
};
const del = async (u) => {
if (!window.confirm(`Remove ${u.email}?`)) return;
try { await api(`/servers/${server.id}/subusers/${u.uuid}`, { method: "DELETE" }); notify("Removed."); load(); }
catch (e) { notify(e.message); }
};
return (
{items === null ?
Loading…
: items.length === 0 ?
No subusers.
: (
| Email | Permissions | |
{items.map((u) => (
| {u.email} |
{u.permissions.length} perms |
|
))}
)}
);
}
/* ── The full manage modal with tabs ── */
const MANAGE_TABS = [
["console", "Console"], ["files", "Files"], ["backups", "Backups"],
["startup", "Startup"], ["schedules", "Schedules"], ["subusers", "Subusers"],
];
function ManageModalFull({ server, onClose, notify }) {
const [tab, setTab] = useState("console");
return (
e.stopPropagation()}>
{server.name}
{server.connect_address &&
{server.connect_address}
}
{MANAGE_TABS.map(([k, label]) => (
))}
{tab === "console" && }
{tab === "files" && }
{tab === "backups" && }
{tab === "startup" && }
{tab === "schedules" && }
{tab === "subusers" && }
);
}
ReactDOM.createRoot(document.getElementById("root")).render();