This commit is contained in:
LukeGus
2026-05-28 22:29:20 -05:00
parent 33dcde0827
commit 5777351145
238 changed files with 62301 additions and 98953 deletions
+31 -26
View File
@@ -405,6 +405,11 @@ function ServerStatsInner({
try {
if (!totpVerified) {
addLog({
type: "info",
stage: "stats_connecting",
message: `Connecting to ${currentHostConfig.username}@${currentHostConfig.ip}:${currentHostConfig.port}`,
});
const result = await startMetricsPolling(currentHostConfig.id);
if (cancelled) return;
@@ -459,10 +464,10 @@ function ServerStatsInner({
if (data) {
setMetrics(data);
setServerStatus("online");
if (!hasExistingMetrics) {
setIsLoadingMetrics(false);
logServerActivity();
setTimeout(() => clearLogs(), 1000);
}
}
@@ -567,17 +572,24 @@ function ServerStatsInner({
const handleRefresh = async () => {
if (!currentHostConfig?.id) return;
if (hasConnectionError) {
setHasConnectionError(false);
clearLogs();
return;
}
try {
setIsRefreshing(true);
const res = await getServerStatusById(currentHostConfig.id);
setServerStatus(res?.status === "online" ? "online" : "offline");
const data = await getServerMetricsById(currentHostConfig.id);
if (data) setMetrics(data);
setShowStatsUI(true);
if (data) {
setMetrics(data);
setShowStatsUI(true);
}
} catch {
setServerStatus("offline");
setMetrics(null);
setShowStatsUI(false);
} finally {
setIsRefreshing(false);
}
@@ -598,7 +610,7 @@ function ServerStatsInner({
}}
>
<div className="flex-1 overflow-y-auto overflow-x-hidden flex flex-col">
{!totpRequired && !isLoadingMetrics && (
{!totpRequired && !isLoadingMetrics && !hasConnectionError && (
<div className="mx-3 mt-3 flex items-center justify-between border border-border bg-card px-3 py-3 shrink-0">
<div className="flex items-center gap-3">
<div className="size-10 border border-border bg-muted flex items-center justify-center shrink-0">
@@ -606,16 +618,6 @@ function ServerStatsInner({
</div>
<div>
<h1 className="text-lg md:text-2xl font-bold">{title}</h1>
<div className="flex items-center gap-2">
<span
className={`size-2 rounded-full ${serverStatus === "online" ? "bg-accent-brand" : "bg-destructive"}`}
/>
<span className="text-xs text-muted-foreground uppercase tracking-widest font-semibold">
{serverStatus === "online"
? t("serverStats.online")
: t("serverStats.offline")}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-0">
@@ -735,16 +737,19 @@ function ServerStatsInner({
showStatsUI &&
!isLoadingMetrics &&
!metrics &&
serverStatus === "offline" && (
serverStatus === "offline" &&
!hasConnectionError && (
<div className="flex-1 flex items-center justify-center py-20">
<div className="text-center opacity-40">
<Server className="size-16 mx-auto mb-4" />
<p className="text-xl font-bold uppercase tracking-widest">
{t("serverStats.serverOffline")}
</p>
<p className="text-sm font-semibold">
{t("serverStats.cannotFetchMetrics")}
</p>
<div className="text-center">
<div className="opacity-40">
<Server className="size-16 mx-auto mb-4" />
<p className="text-xl font-bold uppercase tracking-widest">
{t("serverStats.serverOffline")}
</p>
<p className="text-sm font-semibold">
{t("serverStats.cannotFetchMetrics")}
</p>
</div>
</div>
</div>
)}
@@ -777,7 +782,7 @@ function ServerStatsInner({
/>
<ConnectionLog
isConnecting={isLoadingMetrics}
isConnected={serverStatus === "online"}
isConnected={serverStatus === "online" && !hasConnectionError}
hasConnectionError={hasConnectionError}
position={hasConnectionError ? "top" : "bottom"}
/>
@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ServerStats } from "@/features/server-stats/ServerStats.tsx";
import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx";
@@ -7,6 +8,7 @@ interface ServerStatsAppProps {
}
const ServerStatsApp: React.FC<ServerStatsAppProps> = ({ hostId }) => {
const { t } = useTranslation();
return (
<FullScreenAppWrapper hostId={hostId}>
{(hostConfig, loading) => {
@@ -15,7 +17,9 @@ const ServerStatsApp: React.FC<ServerStatsAppProps> = ({ hostId }) => {
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div>
<p className="text-muted-foreground">Loading host...</p>
<p className="text-muted-foreground">
{t("hosts.loadingHost")}
</p>
</div>
</div>
);
@@ -25,7 +29,7 @@ const ServerStatsApp: React.FC<ServerStatsAppProps> = ({ hostId }) => {
return (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<p className="text-red-500 mb-4">Host not found</p>
<p className="text-red-500 mb-4">{t("hosts.hostNotFound")}</p>
</div>
</div>
);
@@ -21,36 +21,40 @@ function Sparkline({
current ?? 0,
].slice(-20);
if (points.length < 2) return null;
const w = 300;
const h = 48;
const max = Math.max(...points, 1);
const coords = points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
});
const d = `M ${coords.join(" L ")}`;
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
const hasData = points.length >= 2;
const max = hasData ? Math.max(...points, 1) : 1;
const coords = hasData
? points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
})
: [];
const d = hasData ? `M ${coords.join(" L ")}` : "";
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
return (
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
{hasData && (
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
)}
</div>
);
}
@@ -20,36 +20,40 @@ function Sparkline({
current ?? 0,
].slice(-20);
if (points.length < 2) return null;
const w = 300;
const h = 48;
const max = Math.max(...points, 1);
const coords = points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
});
const d = `M ${coords.join(" L ")}`;
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
const hasData = points.length >= 2;
const max = hasData ? Math.max(...points, 1) : 1;
const coords = hasData
? points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
})
: [];
const d = hasData ? `M ${coords.join(" L ")}` : "";
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
return (
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
{hasData && (
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
)}
</div>
);
}
@@ -132,7 +132,7 @@ export function FirewallWidget({ metrics }: FirewallWidgetProps) {
</span>
)}
{firewall && firewall.chains.length > 0 ? (
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 overflow-y-auto max-h-[320px]">
{firewall.chains.map((chain) => (
<ChainSection key={chain.name} chain={chain} />
))}
@@ -46,40 +46,42 @@ export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
{t("serverStats.noRecentLoginData")}
</span>
) : (
allLogins.map((login, i) => (
<div
key={i}
className={`flex items-center justify-between p-2 border ${login.status === "success" ? "border-border bg-muted/30" : "border-destructive/30 bg-destructive/5"}`}
>
<div className="flex flex-col">
<div className="flex items-center gap-1.5">
{login.status === "failed" ? (
<UserX className="size-3 text-destructive" />
) : (
<UserCheck className="size-3 text-accent-brand" />
)}
<div className="flex flex-col gap-2 overflow-y-auto max-h-[300px]">
{allLogins.map((login, i) => (
<div
key={i}
className={`flex items-center justify-between p-2 border ${login.status === "success" ? "border-border bg-muted/30" : "border-destructive/30 bg-destructive/5"}`}
>
<div className="flex flex-col">
<div className="flex items-center gap-1.5">
{login.status === "failed" ? (
<UserX className="size-3 text-destructive" />
) : (
<UserCheck className="size-3 text-accent-brand" />
)}
<span
className={`text-xs font-bold ${login.status === "failed" ? "text-destructive" : ""}`}
>
{login.user}
</span>
</div>
<span className="text-[10px] text-muted-foreground font-mono">
{login.ip}
</span>
</div>
<div className="flex flex-col items-end gap-1">
<span
className={`text-xs font-bold ${login.status === "failed" ? "text-destructive" : ""}`}
className={`text-[9px] font-bold uppercase px-1.5 py-px border ${login.status === "success" ? "border-accent-brand/40 text-accent-brand bg-accent-brand/10" : "border-destructive/40 text-destructive"}`}
>
{login.user}
{login.status}
</span>
<span className="text-[10px] text-muted-foreground">
{new Date(login.time).toLocaleTimeString()}
</span>
</div>
<span className="text-[10px] text-muted-foreground font-mono">
{login.ip}
</span>
</div>
<div className="flex flex-col items-end gap-1">
<span
className={`text-[9px] font-bold uppercase px-1.5 py-px border ${login.status === "success" ? "border-accent-brand/40 text-accent-brand bg-accent-brand/10" : "border-destructive/40 text-destructive"}`}
>
{login.status}
</span>
<span className="text-[10px] text-muted-foreground">
{new Date(login.time).toLocaleTimeString()}
</span>
</div>
</div>
))
))}
</div>
)}
</div>
</SectionCard>
@@ -20,36 +20,40 @@ function Sparkline({
current ?? 0,
].slice(-20);
if (points.length < 2) return null;
const w = 300;
const h = 48;
const max = Math.max(...points, 1);
const coords = points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
});
const d = `M ${coords.join(" L ")}`;
const fill = `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z`;
const hasData = points.length >= 2;
const max = hasData ? Math.max(...points, 1) : 1;
const coords = hasData
? points.map((v, i) => {
const x = (i / (points.length - 1)) * w;
const y = h - (v / max) * h;
return `${x},${y}`;
})
: [];
const d = hasData ? `M ${coords.join(" L ")}` : "";
const fill = hasData ? `M 0,${h} L ${coords.join(" L ")} L ${w},${h} Z` : "";
return (
<div className="h-12 md:h-16 w-full mt-2 bg-muted/20 border border-border/50 relative overflow-hidden">
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
{hasData && (
<svg
className="absolute inset-0 h-full w-full"
viewBox={`0 0 ${w} ${h}`}
preserveAspectRatio="none"
>
<path d={fill} fill="currentColor" className="text-accent-brand/10" />
<path
d={d}
fill="none"
stroke="currentColor"
strokeWidth="1.5"
className="text-accent-brand/60"
/>
</svg>
)}
</div>
);
}
@@ -38,34 +38,36 @@ export function NetworkWidget({ metrics }: NetworkWidgetProps) {
</span>
</div>
) : (
interfaces.map((iface, i) => (
<div
key={i}
className="flex flex-col p-2 border border-border bg-muted/30 gap-1"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className={`size-1.5 rounded-full ${iface.state === "UP" ? "bg-accent-brand" : "bg-muted-foreground"}`}
/>
<span className="text-sm font-bold font-mono">
{iface.name}
<div className="flex flex-col gap-2 overflow-y-auto max-h-[260px]">
{interfaces.map((iface, i) => (
<div
key={i}
className="flex flex-col p-2 border border-border bg-muted/30 gap-1"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className={`size-1.5 rounded-full ${iface.state === "UP" ? "bg-accent-brand" : "bg-muted-foreground"}`}
/>
<span className="text-sm font-bold font-mono">
{iface.name}
</span>
</div>
<span className="text-[10px] font-semibold px-1.5 py-px border border-border text-muted-foreground uppercase">
{iface.state}
</span>
</div>
<span className="text-[10px] font-semibold px-1.5 py-px border border-border text-muted-foreground uppercase">
{iface.state}
</span>
<div className="flex justify-between text-[10px] font-mono text-muted-foreground">
<span>{iface.ip}</span>
{(iface.rx || iface.tx) && (
<span>
{iface.rx ?? "—"} / {iface.tx ?? "—"}
</span>
)}
</div>
</div>
<div className="flex justify-between text-[10px] font-mono text-muted-foreground">
<span>{iface.ip}</span>
{(iface.rx || iface.tx) && (
<span>
{iface.rx ?? "—"} / {iface.tx ?? "—"}
</span>
)}
</div>
</div>
))
))}
</div>
)}
</div>
</SectionCard>
@@ -14,15 +14,17 @@ function PortRow({ port }: { port: ListeningPort }) {
addr === "0.0.0.0" || addr === "*" || addr === "::" ? "*" : addr;
return (
<div className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0">
<span className="text-accent-brand font-bold">{port.localPort}</span>
<span className="text-muted-foreground">
<div className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0 overflow-hidden">
<span className="text-accent-brand font-bold truncate">
{port.localPort}
</span>
<span className="text-muted-foreground truncate">
{port.protocol.toUpperCase()}
</span>
<span className="font-semibold truncate">
{port.process ?? (port.pid ? `PID:${port.pid}` : "—")}
</span>
<span className="text-right text-muted-foreground">
<span className="text-right text-muted-foreground truncate">
{formatAddress(port.localAddress)}
</span>
</div>
@@ -41,7 +43,7 @@ export function PortsWidget({ metrics }: PortsWidgetProps) {
title={t("serverStats.ports.title")}
icon={<Unplug className="size-3.5" />}
>
<div className="flex flex-col gap-1.5 py-1">
<div className="flex flex-col gap-1.5 py-1 overflow-x-hidden">
<div className="grid grid-cols-4 text-[10px] text-muted-foreground font-bold uppercase pb-1 border-b border-border min-w-0">
<span>{t("serverStats.ports.port")}</span>
<span>{t("serverStats.ports.protocol")}</span>
@@ -53,12 +55,14 @@ export function PortsWidget({ metrics }: PortsWidgetProps) {
{t("serverStats.ports.noData")}
</span>
) : (
ports.map((port, i) => (
<PortRow
key={`${port.protocol}-${port.localPort}-${i}`}
port={port}
/>
))
<div className="overflow-y-auto max-h-[300px]">
{ports.map((port, i) => (
<PortRow
key={`${port.protocol}-${port.localPort}-${i}`}
port={port}
/>
))}
</div>
)}
</div>
</SectionCard>
@@ -45,19 +45,21 @@ export function ProcessesWidget({ metrics }: ProcessesWidgetProps) {
<span className="text-xs">{t("serverStats.noProcessesFound")}</span>
</div>
) : (
topProcesses.map((proc, i) => (
<div
key={i}
className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0"
>
<span className="text-muted-foreground">{proc.pid}</span>
<span className="text-accent-brand font-bold">{proc.cpu}%</span>
<span>{proc.mem}%</span>
<span className="truncate font-semibold" title={proc.command}>
{proc.command.split("/").pop()}
</span>
</div>
))
<div className="overflow-y-auto max-h-[280px]">
{topProcesses.map((proc, i) => (
<div
key={i}
className="grid grid-cols-4 text-xs font-mono py-1 border-b border-border/50 last:border-0 min-w-0"
>
<span className="text-muted-foreground">{proc.pid}</span>
<span className="text-accent-brand font-bold">{proc.cpu}%</span>
<span>{proc.mem}%</span>
<span className="truncate font-semibold" title={proc.command}>
{proc.command.split("/").pop()}
</span>
</div>
))}
</div>
)}
</div>
</SectionCard>