Phase7
Some checks failed
CI / Node — lint, typecheck, test, build (push) Failing after 9s
CI / Go — node-agent tests (push) Failing after 8s
CI / Go — edge-gateway build (push) Successful in 14s

This commit is contained in:
smueller
2026-06-26 13:20:55 +02:00
parent 49a98ee31d
commit ab21f53cdd
86 changed files with 1207 additions and 63 deletions

View File

@@ -108,6 +108,9 @@
"allocatedRam": "Zugewiesener RAM",
"activitySubtitle": "Übersicht",
"totalServers": "Server gesamt",
"credits": "Credits",
"creditsSubtitle": "Laufzeit-Guthaben",
"creditsRemaining": "Verbleibende Credits",
"viewAllServers": "Alle Server anzeigen",
"serversSummary": "Server in Ihrem Konto"
},
@@ -154,6 +157,7 @@
"ram": "RAM",
"start": "Starten",
"stop": "Stoppen",
"idleCountdown": "Auto-Stop in {seconds}s (keine Spieler)",
"details": "Details",
"detailSubtitle": "Serverdetails und Lifecycle-Steuerung",
"backToList": "Zurück zur Serverliste",

View File

@@ -108,6 +108,9 @@
"allocatedRam": "Allocated RAM",
"activitySubtitle": "Overview",
"totalServers": "Total servers",
"credits": "Credits",
"creditsSubtitle": "Runtime balance",
"creditsRemaining": "Credits remaining",
"viewAllServers": "View all servers",
"serversSummary": "Servers in your account"
},
@@ -154,6 +157,7 @@
"ram": "RAM",
"start": "Start",
"stop": "Stop",
"idleCountdown": "Auto-stop in {seconds}s (no players)",
"details": "Details",
"detailSubtitle": "Server details and lifecycle controls",
"backToList": "Back to servers",

View File

@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { Link, useRouter } from "@/i18n/navigation";
import { EmptyState } from "@/components/dashboard/empty-state";
import { DashboardSkeleton } from "@/components/dashboard/skeleton";
import { getWallet } from "@/lib/api/billing";
import { listServers } from "@/lib/api/servers";
export function DashboardContent() {
@@ -15,6 +16,10 @@ export function DashboardContent() {
queryKey: ["servers"],
queryFn: listServers,
});
const { data: wallet } = useQuery({
queryKey: ["wallet"],
queryFn: getWallet,
});
const servers = data?.servers ?? [];
const activeCount = servers.filter((server) =>
@@ -63,14 +68,14 @@ export function DashboardContent() {
<Card>
<CardHeader>
<CardTitle>{t("activity")}</CardTitle>
<CardDescription>{t("activitySubtitle")}</CardDescription>
<CardTitle>{t("credits")}</CardTitle>
<CardDescription>{t("creditsSubtitle")}</CardDescription>
</CardHeader>
<CardContent>
<p className="font-mono text-2xl font-semibold text-zinc-900 dark:text-zinc-50">
{servers.length}
{wallet?.balance ?? "—"}
</p>
<p className="text-sm text-zinc-600 dark:text-zinc-400">{t("totalServers")}</p>
<p className="text-sm text-zinc-600 dark:text-zinc-400">{t("creditsRemaining")}</p>
</CardContent>
</Card>
</div>

View File

@@ -1,11 +1,12 @@
"use client";
import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@hexahost/ui";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { Link } from "@/i18n/navigation";
import { ApiError } from "@/lib/api/auth";
import { getServerIdleStatus } from "@/lib/api/billing";
import {
type Server,
startServer,
@@ -23,6 +24,13 @@ export function ServerCard({ server, showDetailsLink = true }: ServerCardProps)
const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null);
const { data: idleStatus } = useQuery({
queryKey: ["server-idle", server.id],
queryFn: () => getServerIdleStatus(server.id),
enabled: server.status === "RUNNING",
refetchInterval: server.status === "RUNNING" ? 15_000 : false,
});
const startMutation = useMutation({
mutationFn: () => startServer(server.id),
onSuccess: () => {
@@ -47,9 +55,11 @@ export function ServerCard({ server, showDetailsLink = true }: ServerCardProps)
},
});
const canStart = server.status === "STOPPED";
const canStart = server.status === "STOPPED" || server.status === "UNKNOWN";
const canStop = server.status === "RUNNING";
const isBusy = ["PROVISIONING", "INSTALLING", "STARTING", "STOPPING"].includes(server.status);
const isBusy = ["PROVISIONING", "INSTALLING", "STARTING", "STOPPING", "QUEUED"].includes(
server.status,
);
const address =
server.hostPort !== null ? `localhost:${server.hostPort}` : t("addressPending");
@@ -76,6 +86,12 @@ export function ServerCard({ server, showDetailsLink = true }: ServerCardProps)
</div>
</dl>
{server.status === "RUNNING" && idleStatus?.secondsRemaining ? (
<p className="text-sm text-amber-700 dark:text-amber-300" role="status">
{t("idleCountdown", { seconds: idleStatus.secondsRemaining })}
</p>
) : null}
{actionError ? (
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
{actionError}

View File

@@ -8,9 +8,11 @@ const statusStyles: Record<GameServerStatus, string> = {
PROVISIONING: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
INSTALLING: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300",
STOPPED: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300",
QUEUED: "bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-300",
STARTING: "bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300",
RUNNING: "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300",
STOPPING: "bg-sky-100 text-sky-800 dark:bg-sky-950 dark:text-sky-300",
UNKNOWN: "bg-orange-100 text-orange-800 dark:bg-orange-950 dark:text-orange-300",
ERROR: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300",
DELETING: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300",
DELETED: "bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-500",

View File

@@ -0,0 +1,30 @@
import { apiFetch } from "./auth";
export interface WalletResponse {
balance: number;
periodStart: string;
transactions: Array<{
id: string;
amount: number;
type: string;
referenceType: string | null;
referenceId: string | null;
createdAt: string;
}>;
}
export interface IdleStatusResponse {
enabled: boolean;
playerCount: number | null;
idleStopAt: string | null;
secondsRemaining: number | null;
lastPlayerSeenAt: string | null;
}
export async function getWallet(): Promise<WalletResponse> {
return apiFetch<WalletResponse>("/account/wallet");
}
export async function getServerIdleStatus(serverId: string): Promise<IdleStatusResponse> {
return apiFetch<IdleStatusResponse>(`/servers/${serverId}/idle`);
}

View File

@@ -5,9 +5,11 @@ export type GameServerStatus =
| "PROVISIONING"
| "INSTALLING"
| "STOPPED"
| "QUEUED"
| "STARTING"
| "RUNNING"
| "STOPPING"
| "UNKNOWN"
| "ERROR"
| "DELETING"
| "DELETED";
@@ -38,6 +40,8 @@ export interface Server {
containerId: string | null;
dataPath: string | null;
ramMb: number;
idleShutdownEnabled: boolean;
idleStopAt: string | null;
createdAt: string;
updatedAt: string;
}
@@ -49,6 +53,7 @@ export interface ServerListResponse {
export interface ServerActionResponse {
server: Server;
jobId?: string;
queuePosition?: number;
}
export interface CreateServerPayload {