import { useEffect, useMemo, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
AlertTriangle,
Check,
Database,
FileJson,
Globe2,
Loader2,
Plus,
RefreshCcw,
Route,
Server,
ShieldCheck,
Zap,
} from "lucide-react";
const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:4000/api";
const emptyServer = { name: "", ipAddress: "", description: "" };
const emptyService = {
serverId: "",
name: "",
port: "",
endpoint: "",
method: "POST",
requestTemplate: "{}",
notes: "",
};
const sampleJson = JSON.stringify(
{
servers: [
{
name: "Billing Server",
ipAddress: "192.168.1.50",
services: [
{
name: "Invoice API",
port: 8080,
endpoint: "/api/invoices/sync",
method: "POST",
requestTemplate: { limit: 10 },
},
],
},
],
},
null,
2,
);
async function api(path, options = {}) {
const response = await fetch(`${API_BASE}${path}`, {
headers: { "content-type": "application/json", ...(options.headers || {}) },
...options,
});
const data = await response.json().catch(() => null);
if (!response.ok) {
const error = new Error(data?.message || "Request failed.");
error.data = data;
throw error;
}
return data;
}
function parseJsonField(value) {
const trimmed = value.trim();
if (!trimmed) return null;
return JSON.parse(trimmed);
}
function Stat({ icon: Icon, label, value }) {
return (
);
}
function Field({ label, children }) {
return (
);
}
function App() {
const [servers, setServers] = useState([]);
const [serverForm, setServerForm] = useState(emptyServer);
const [serviceForm, setServiceForm] = useState(emptyService);
const [jsonInput, setJsonInput] = useState(sampleJson);
const [preview, setPreview] = useState(null);
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [proxyResult, setProxyResult] = useState(null);
const totals = useMemo(() => {
const services = servers.reduce((sum, server) => sum + server.services.length, 0);
const ports = new Set(servers.flatMap((server) => server.services.map((service) => service.port)));
return { servers: servers.length, services, ports: ports.size };
}, [servers]);
async function loadServers() {
setLoading(true);
setError("");
try {
const data = await api("/servers");
setServers(data);
if (data[0] && !serviceForm.serverId) {
setServiceForm((current) => ({ ...current, serverId: data[0].id }));
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
useEffect(() => {
loadServers();
}, []);
async function createServer(event) {
event.preventDefault();
setError("");
setMessage("");
try {
await api("/servers", { method: "POST", body: JSON.stringify(serverForm) });
setServerForm(emptyServer);
setMessage("Server added.");
await loadServers();
} catch (err) {
setError(err.message);
}
}
async function createService(event) {
event.preventDefault();
setError("");
setMessage("");
try {
await api("/services", {
method: "POST",
body: JSON.stringify({
...serviceForm,
requestTemplate: parseJsonField(serviceForm.requestTemplate),
}),
});
setServiceForm((current) => ({
...emptyService,
serverId: current.serverId,
method: "POST",
requestTemplate: "{}",
}));
setMessage("Service added.");
await loadServers();
} catch (err) {
setError(err.message);
}
}
async function previewImport() {
setError("");
setMessage("");
try {
const payload = JSON.parse(jsonInput);
const data = await api("/imports/preview", { method: "POST", body: JSON.stringify(payload) });
setPreview(data);
setMessage(
data.conflicts.length
? "Conflicts found. Review and confirm before overwrite."
: "Import is ready. Exact duplicates will be skipped.",
);
} catch (err) {
setError(err.message);
}
}
async function applyImport(mode = "skip") {
setError("");
setMessage("");
try {
const payload = JSON.parse(jsonInput);
const data = await api("/imports/apply", {
method: "POST",
body: JSON.stringify({ payload, mode }),
});
setPreview(null);
setMessage(
`Import applied: ${data.applied.serversCreated} servers and ${data.applied.servicesCreated} services created.`,
);
await loadServers();
} catch (err) {
setError(err.message);
if (err.data) setPreview(err.data);
}
}
async function reverseProxy(serviceId, body) {
setError("");
setProxyResult(null);
try {
const data = await api(`/services/${serviceId}/reverse-proxy`, {
method: "POST",
body: JSON.stringify({ body }),
});
setProxyResult(data);
} catch (err) {
setError(err.message);
}
}
return (
MTC Server Reverse Proxy
Reverse Proxy Manager
{(message || error) && (
{error || message}
)}
{preview && (
{preview.conflicts?.length ? : }
Import Preview
{preview.conflicts?.length > 0 && (
)}
)}
{servers.map((server) => (
{server.name}
{server.ipAddress}
{server.description &&
{server.description}
}
{server.services.length} services
{server.services.map((service) => (
{service.name}
{service.method} http://{server.ipAddress}:{service.port}
{service.endpoint}
))}
{server.services.length === 0 &&
No services added for this server yet.
}
))}
{servers.length === 0 && !loading && Add a server manually or import JSON to start building the proxy table.}
{proxyResult && (
Reverse Proxy Result
{JSON.stringify(proxyResult, null, 2)}
)}
);
}
export default App;