BugFixes galore
This commit is contained in:
@@ -69,8 +69,9 @@ export default function ConsolePage() {
|
||||
});
|
||||
|
||||
// Receive buffered history on connect
|
||||
socket.on("history", (data: { lines: string[] }) => {
|
||||
const historicalLines = data.lines.map((line) => ({
|
||||
socket.on("history", (data: string[] | { lines: string[] }) => {
|
||||
const rawLines = Array.isArray(data) ? data : (data?.lines ?? []);
|
||||
const historicalLines = rawLines.map((line) => ({
|
||||
text: line,
|
||||
timestamp: Date.now(),
|
||||
type: classifyLine(line) as LogLine["type"],
|
||||
|
||||
183
app/(dashboard)/settings/page.tsx
Normal file
183
app/(dashboard)/settings/page.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { User, Lock, Shield } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient, useSession } from "@/lib/auth/client";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { data: session, isPending } = useSession();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [nameLoading, startNameTransition] = useTransition();
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [passwordLoading, startPasswordTransition] = useTransition();
|
||||
|
||||
// Populate name field once session loads
|
||||
const displayName = name || session?.user?.name || "";
|
||||
|
||||
function handleNameSave() {
|
||||
if (!displayName.trim()) {
|
||||
toast.error("Name cannot be empty");
|
||||
return;
|
||||
}
|
||||
startNameTransition(async () => {
|
||||
const { error } = await authClient.updateUser({ name: displayName.trim() });
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to update name");
|
||||
} else {
|
||||
toast.success("Display name updated");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handlePasswordChange() {
|
||||
if (!currentPassword) {
|
||||
toast.error("Current password is required");
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
toast.error("New password must be at least 8 characters");
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
toast.error("Passwords do not match");
|
||||
return;
|
||||
}
|
||||
startPasswordTransition(async () => {
|
||||
const { error } = await authClient.changePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
revokeOtherSessions: false,
|
||||
});
|
||||
if (error) {
|
||||
toast.error(error.message ?? "Failed to change password");
|
||||
} else {
|
||||
toast.success("Password changed successfully");
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Account Settings</h1>
|
||||
<p className="text-zinc-400 text-sm mt-1">Manage your profile and security preferences</p>
|
||||
</div>
|
||||
|
||||
{/* Profile */}
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-emerald-500" />
|
||||
Profile
|
||||
</CardTitle>
|
||||
<CardDescription className="text-zinc-500">
|
||||
Update your display name and view account info
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Email</Label>
|
||||
<Input
|
||||
value={session?.user?.email ?? ""}
|
||||
disabled
|
||||
className="bg-zinc-800 border-zinc-700 text-zinc-400 cursor-not-allowed"
|
||||
/>
|
||||
<p className="text-xs text-zinc-500">Email address cannot be changed</p>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Display Name</Label>
|
||||
<Input
|
||||
value={name || session?.user?.name || ""}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your name"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
disabled={isPending}
|
||||
/>
|
||||
</div>
|
||||
{session?.user && (
|
||||
<div className="flex items-center gap-2 text-xs text-zinc-500">
|
||||
<Shield className="w-3 h-3" />
|
||||
Role: <span className="text-zinc-300 capitalize">{(session.user as { role?: string }).role ?? "moderator"}</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleNameSave}
|
||||
disabled={nameLoading || isPending}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{nameLoading ? "Saving…" : "Save Name"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator className="bg-zinc-800" />
|
||||
|
||||
{/* Password */}
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Lock className="w-4 h-4 text-emerald-500" />
|
||||
Change Password
|
||||
</CardTitle>
|
||||
<CardDescription className="text-zinc-500">
|
||||
Choose a strong password with at least 8 characters
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Current Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">New Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-zinc-300">Confirm New Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handlePasswordChange}
|
||||
disabled={passwordLoading}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
{passwordLoading ? "Changing…" : "Change Password"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
254
app/(dashboard)/updates/page.tsx
Normal file
254
app/(dashboard)/updates/page.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Download, RefreshCw, AlertTriangle, CheckCircle2, Server } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const SERVER_TYPES = ["vanilla", "paper", "fabric"] as const;
|
||||
type ServerType = (typeof SERVER_TYPES)[number];
|
||||
|
||||
interface Settings {
|
||||
serverType?: string;
|
||||
serverVersion?: string;
|
||||
serverJar?: string;
|
||||
}
|
||||
|
||||
export default function UpdatesPage() {
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<ServerType>("paper");
|
||||
const [versions, setVersions] = useState<string[]>([]);
|
||||
const [selectedVersion, setSelectedVersion] = useState("");
|
||||
const [loadingSettings, setLoadingSettings] = useState(true);
|
||||
const [loadingVersions, setLoadingVersions] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/server/settings");
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.settings) {
|
||||
setSettings(data.settings);
|
||||
setSelectedType((data.settings.serverType as ServerType) ?? "paper");
|
||||
setSelectedVersion(data.settings.serverVersion ?? "");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setLoadingSettings(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchVersions = useCallback(async (type: string) => {
|
||||
setLoadingVersions(true);
|
||||
setVersions([]);
|
||||
try {
|
||||
const res = await fetch(`/api/server/versions?type=${type}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setVersions(data.versions ?? []);
|
||||
} else {
|
||||
toast.error("Failed to fetch versions");
|
||||
}
|
||||
} catch {
|
||||
toast.error("Network error fetching versions");
|
||||
} finally {
|
||||
setLoadingVersions(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
}, [fetchSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchVersions(selectedType);
|
||||
}, [selectedType, fetchVersions]);
|
||||
|
||||
const isUpToDate =
|
||||
settings?.serverVersion === selectedVersion &&
|
||||
settings?.serverType === selectedType;
|
||||
|
||||
async function handleApply() {
|
||||
if (!selectedVersion) {
|
||||
toast.error("Please select a version");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const res = await fetch("/api/server/settings", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ serverType: selectedType, serverVersion: selectedVersion }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error ?? "Failed to apply");
|
||||
}
|
||||
setSettings((prev) => ({ ...prev, serverType: selectedType, serverVersion: selectedVersion }));
|
||||
toast.success(`Server version set to ${selectedType} ${selectedVersion}`);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "Failed to apply version");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersion = settings?.serverVersion
|
||||
? `${settings.serverType ?? "unknown"} ${settings.serverVersion}`
|
||||
: "Not configured";
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Server Updates</h1>
|
||||
<p className="text-zinc-400 text-sm mt-1">
|
||||
Manage your Minecraft server version
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current version */}
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Server className="w-4 h-4 text-emerald-500" />
|
||||
Current Version
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loadingSettings ? (
|
||||
<Skeleton className="h-6 w-48 bg-zinc-800" />
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-white font-mono text-sm">{currentVersion}</span>
|
||||
{settings?.serverVersion && (
|
||||
<Badge
|
||||
className={
|
||||
isUpToDate
|
||||
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
|
||||
: "bg-amber-500/10 text-amber-400 border-amber-500/20"
|
||||
}
|
||||
>
|
||||
{isUpToDate ? "Up to date" : "Update available"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Version picker */}
|
||||
<Card className="bg-zinc-900 border-zinc-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
|
||||
<Download className="w-4 h-4 text-emerald-500" />
|
||||
Select Version
|
||||
</CardTitle>
|
||||
<CardDescription className="text-zinc-500">
|
||||
Choose a server type and version to apply
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm text-zinc-300">Server Type</label>
|
||||
<Select
|
||||
value={selectedType}
|
||||
onValueChange={(v) => { if (v) setSelectedType(v as ServerType); }}
|
||||
>
|
||||
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-zinc-800 border-zinc-700">
|
||||
{SERVER_TYPES.map((t) => (
|
||||
<SelectItem key={t} value={t} className="text-zinc-300 focus:bg-zinc-700 focus:text-white capitalize">
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-sm text-zinc-300">Version</label>
|
||||
<Select
|
||||
value={selectedVersion}
|
||||
onValueChange={(v) => { if (v) setSelectedVersion(v); }}
|
||||
disabled={loadingVersions || versions.length === 0}
|
||||
>
|
||||
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
loadingVersions
|
||||
? "Loading…"
|
||||
: versions.length === 0
|
||||
? "No versions found"
|
||||
: "Select version"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-64">
|
||||
{versions.map((v) => (
|
||||
<SelectItem key={v} value={v} className="text-zinc-300 focus:bg-zinc-700 focus:text-white font-mono text-sm">
|
||||
{v}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
disabled={saving || !selectedVersion || loadingVersions}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
size="sm"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 mr-1.5" />
|
||||
{saving ? "Applying…" : "Apply Version"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fetchVersions(selectedType)}
|
||||
disabled={loadingVersions}
|
||||
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 mr-1.5 ${loadingVersions ? "animate-spin" : ""}`} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedVersion && settings?.serverVersion && selectedVersion !== settings.serverVersion && (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-400 text-sm">
|
||||
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
|
||||
<p>
|
||||
Changing from <span className="font-mono">{settings.serverVersion}</span> to{" "}
|
||||
<span className="font-mono">{selectedVersion}</span> requires a server restart.
|
||||
Make sure to create a backup first.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUpToDate && selectedVersion && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-emerald-400 text-sm">
|
||||
<CheckCircle2 className="w-4 h-4 shrink-0" />
|
||||
<p>This version is already configured.</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user