Phase7
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
|
||||
30
apps/web/src/lib/api/billing.ts
Normal file
30
apps/web/src/lib/api/billing.ts
Normal 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`);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user