From 683cdc4016dcd895589b4beb07df7742e44992d1 Mon Sep 17 00:00:00 2001 From: Aryan Singh Date: Fri, 16 Oct 2026 20:34:12 -0500 Subject: [PATCH] Repair uploads and let the console follow any registered server --- dashboard/next.config.ts | 2 + dashboard/src/app/(panel)/console/page.tsx | 20 +++-- dashboard/src/app/(panel)/files/page.tsx | 23 +++--- dashboard/src/app/(panel)/network/page.tsx | 22 ++++-- dashboard/src/app/api/console/route.ts | 19 ++++- dashboard/src/app/api/files/upload/route.ts | 1 + dashboard/src/app/api/network/files/route.ts | 6 +- dashboard/src/app/api/stream/logs/route.ts | 77 ++++++++++++-------- dashboard/src/lib/network.ts | 2 +- dashboard/src/server/console-target.ts | 9 +++ dashboard/src/server/docker.ts | 49 ++++++++----- dashboard/src/server/network-config.ts | 2 +- dashboard/src/server/network.ts | 2 +- dashboard/tests/console.test.ts | 36 +++++++++ dashboard/tests/network.test.ts | 7 ++ 15 files changed, 193 insertions(+), 84 deletions(-) create mode 100644 dashboard/src/server/console-target.ts create mode 100644 dashboard/tests/console.test.ts diff --git a/dashboard/next.config.ts b/dashboard/next.config.ts index a10bc84..1a583fb 100644 --- a/dashboard/next.config.ts +++ b/dashboard/next.config.ts @@ -2,6 +2,8 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + // Auth proxy clones multipart requests; its 10 MB default truncates uploads. + experimental: { proxyClientMaxBodySize: "260mb" }, // The panel is self-hosted on a LAN; no image CDN available. images: { unoptimized: true }, }; diff --git a/dashboard/src/app/(panel)/console/page.tsx b/dashboard/src/app/(panel)/console/page.tsx index f0bbe89..f7ef461 100644 --- a/dashboard/src/app/(panel)/console/page.tsx +++ b/dashboard/src/app/(panel)/console/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { SendHorizonal, ArrowDownToLine, Eraser } from "lucide-react"; -import { api, useSSE, toast } from "@/lib/api"; +import { api, useSSE, useApi, toast } from "@/lib/api"; import { Button, Input } from "@/components/ui"; const QUICK_COMMANDS = [ @@ -24,6 +24,9 @@ function colorFor(line: string): string { } export default function ConsolePage() { + const [server, setServer] = useState("primary"); + const [status, setStatus] = useState("Connecting…"); + const { data: targets } = useApi>("/api/console"); const [lines, setLines] = useState([]); const [command, setCommand] = useState(""); const [history, setHistory] = useState([]); @@ -33,7 +36,9 @@ export default function ConsolePage() { const [busy, setBusy] = useState(false); const paneRef = useRef(null); - useSSE("/api/stream/logs", { + useEffect(() => { setLines([]); setStatus("Connecting…"); }, [server]); + useSSE(`/api/stream/logs?server=${encodeURIComponent(server)}`, { + status: value => setStatus(String(value)), message: (line) => { setLines((xs) => { const next = [...xs, line as string]; @@ -60,7 +65,9 @@ export default function ConsolePage() { if (!c) return; setBusy(true); try { - await api("/api/console", { body: { command: c } }); + const target = server; + const result = await api<{ output: string }>("/api/console", { body: { command: c, server: target } }); + setLines(xs => [...xs, `> ${c}`, ...result.output.trim().split("\n").filter(Boolean)]); setHistory((h) => [c, ...h.filter((x) => x !== c)].slice(0, 50)); setHistIdx(-1); setCommand(""); @@ -97,9 +104,12 @@ export default function ConsolePage() {

Console

-

Live server log with RCON command input

+

{status}

-
+
+ (null); + const [uploading, setUploading] = useState(false); const uploadRef = useRef(null); const load = useCallback(async (p: string) => { @@ -92,18 +93,18 @@ export default function FilesPage() { }; const upload = async (file: File) => { - const form = new FormData(); - form.append("dir", path); - form.append("file", file); - toast(`Uploading ${file.name}…`, "info"); - const res = await fetch("/api/files/upload", { method: "POST", body: form }); - const payload = await res.json(); - if (!res.ok || payload.ok === false) { - toast(payload.error ?? "Upload failed", "error"); - } else { + if (file.size > 256 * 1024 * 1024) { toast("File too large (256 MB max)", "error"); return; } + setUploading(true); + try { + const form = new FormData(); + form.append("dir", path); form.append("file", file); + const res = await fetch("/api/files/upload", { method: "POST", body: form }); + const payload = await res.json().catch(() => ({ error: res.status === 413 ? "Upload rejected by the web proxy: file is too large" : `Upload failed (${res.status})` })); + if (!res.ok || payload.ok === false) throw new Error(payload.error ?? "Upload failed"); toast(`${file.name} uploaded`, "success"); void load(path); - } + } catch (err) { toast((err as Error).message, "error"); } + finally { setUploading(false); } }; const crumbs = path ? path.split("/") : []; @@ -126,7 +127,7 @@ export default function FilesPage() { e.target.value = ""; }} /> -