Added the console tab

This commit is contained in:
2026-06-10 21:17:12 -05:00
parent f028b2fc15
commit 2eee7c96b9
5 changed files with 149 additions and 5 deletions

View File

@@ -37,10 +37,10 @@ WIP
- [x] Server Name
- [x] Port
- [ ] Full instance configuration
- [ ] Console
- [ ] Fully interactive instance console
- [ ] Send commands to servers
- [ ] View logs
- [x] Console
- [x] Fully interactive instance console
- [x] Send commands to servers
- [x] View logs
- [ ] Automated server backups
- [ ] Access authentication

View File

@@ -21,6 +21,15 @@ export async function fetchInstanceList(): Promise<Instance[]> {
return res.json();
}
export async function fetchInstanceLogs(name: string): Promise<string[]> {
const res = await fetch(`${ADDRESS}/instances/logs?name=${name}`);
if (!res.ok) {
const message = await res.text();
throw new Error(`Could not fetch instance logs: ${message.trim()}`);
}
return res.json();
}
export async function sendStartInstanceRequest(name: string) {
const res = await fetch(`${ADDRESS}/instances/start?name=${name}`, { method: "POST" });
if (!res.ok) {
@@ -64,4 +73,17 @@ export async function sendModifyInstanceRequest(name: string, newName: string, n
throw new Error(`Modify instance request failed: ${message.trim()}`);
}
return res.json;
}
export async function sendCommandRequest(name: string, payload: string) {
const res = await fetch(`${ADDRESS}/instances/command?name=${name}`, {
method: "POST",
body: JSON.stringify({ command: payload }),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) {
const message = await res.text();
throw new Error(`Send Instance Command failed: ${message.trim()}`);
}
return res.json;
}

View File

@@ -0,0 +1,68 @@
import type { Instance } from "../types";
import { useEffect, useRef, useState } from "react";
import { fetchInstanceLogs, sendCommandRequest } from "../api";
type Props = {
instance: Instance;
};
export default function InstanceConsole({ instance }: Props) {
const [logs, setLogs] = useState<string[]>([]);
const [command, setCommand] = useState("");
const [autoScroll, setAutoScroll] = useState(true);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
async function poll() {
try {
const lines = await fetchInstanceLogs(instance.name);
setLogs(lines);
} catch (e) {
console.error(e);
}
}
poll();
const interval = setInterval(poll, 2000);
return () => clearInterval(interval);
}, [instance.name]);
// useEffect(() => {
// bottomRef.current?.scrollIntoView({ behavior: "smooth" });
// }, [logs]);
useEffect(() => {
if (autoScroll) {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [logs, autoScroll]);
async function handleSendCommand() {
if (!command.trim()) return;
try {
await sendCommandRequest(instance.name, command);
setCommand("");
} catch (e: any) {
console.error(e);
}
}
return (
<div className="consoleWrapper">
<div className="consoleOutput">
{logs.map((line, i) => <p key={i} className="consoleLine" dangerouslySetInnerHTML={{ __html: line }} />)}
<div ref={bottomRef} />
</div>
<div className="consoleInput flexboxHorizontal">
<input value={command} onChange={e => setCommand(e.target.value)}
onKeyDown={e => { if (e.key === "Enter") handleSendCommand(); }}
placeholder="Enter command..." />
<button onClick={handleSendCommand}>Send</button>
<label className="autoScrollLabel">
<input type="checkbox" checked={autoScroll} onChange={e => setAutoScroll(e.target.checked)} />
Auto-scroll
</label>
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useState } from "react";
import type { Instance } from "../types";
import { sendDeleteInstanceRequest, sendModifyInstanceRequest } from "../api";
import InstanceConsole from "./InstanceConsole";
type Props = {
instance: Instance;
@@ -84,7 +85,7 @@ export default function InstanceDetail({ instance, onClose, onDeleted, onSaved }
{activeTab === "config" && <p>Config (coming soon)</p>}
{activeTab === "console" && <p>Console (coming soon)</p>}
{activeTab === "console" && <InstanceConsole instance={instance} />}
{activeTab === "danger" &&
<div className="dangerZone">

View File

@@ -389,4 +389,57 @@ input[type="checkbox"] {
.deleteButton:hover {
background-color: #cc0000;
}
/* CONSOLE */
.consoleWrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.consoleOutput {
background-color: #1a1a1a;
border-radius: 6px;
padding: 12px;
height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.consoleLine {
font-family: var(--mono);
font-size: 0.85em;
color: #d4d4d4;
text-align: left;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
.consoleInput input {
flex: 1;
}
.autoScrollLabel {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.85em;
color: var(--text);
}
.consoleLine code {
background-color: #2d2d2d;
color: #91a357;
padding: 1px 5px;
border-radius: 3px;
font-family: var(--mono);
font-size: 0.95em;
}
.consoleLine i {
color: #a0a0c0;
font-style: italic;
}