Format the repository with Prettier

CI has been running `prettier --check` against a tree that was never
formatted, so the check reported 63 files and failed every run. Nothing
here is a behaviour change: `pnpm lint` and the four typecheck builds
pass exactly as before.

conduit-bringup-artifacts is added to .prettierignore instead. Those
files are captured bring-up reports, not maintained sources; reflowing
them would only churn a record of what happened.
This commit is contained in:
hibna
2026-08-02 21:08:12 +03:00
parent d0d3a58907
commit c1adb94abb
59 changed files with 1516 additions and 1364 deletions
+49 -49
View File
@@ -76,62 +76,62 @@ function AuthGuard() {
export function App() {
return (
<ErrorBoundary>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes */}
<Route element={<AuthGuard />}>
<Route element={<AppLayout />}>
{/* Organizations */}
<Route path="/" element={<OrganizationsPage />} />
{/* Protected routes */}
<Route element={<AuthGuard />}>
<Route element={<AppLayout />}>
{/* Organizations */}
<Route path="/" element={<OrganizationsPage />} />
{/* Org-scoped routes */}
<Route path="/org/:orgId/dashboard" element={<DashboardPage />} />
<Route path="/org/:orgId/servers" element={<ServersPage />} />
<Route path="/org/:orgId/servers/new" element={<CreateServerPage />} />
<Route path="/org/:orgId/nodes" element={<NodesPage />} />
<Route path="/org/:orgId/nodes/:nodeId" element={<NodeDetailPage />} />
<Route path="/org/:orgId/settings" element={<Navigate to="members" replace />} />
<Route path="/org/:orgId/settings/members" element={<MembersPage />} />
{/* Org-scoped routes */}
<Route path="/org/:orgId/dashboard" element={<DashboardPage />} />
<Route path="/org/:orgId/servers" element={<ServersPage />} />
<Route path="/org/:orgId/servers/new" element={<CreateServerPage />} />
<Route path="/org/:orgId/nodes" element={<NodesPage />} />
<Route path="/org/:orgId/nodes/:nodeId" element={<NodeDetailPage />} />
<Route path="/org/:orgId/settings" element={<Navigate to="members" replace />} />
<Route path="/org/:orgId/settings/members" element={<MembersPage />} />
{/* Account */}
<Route path="/account/security" element={<AccountSecurityPage />} />
{/* Account */}
<Route path="/account/security" element={<AccountSecurityPage />} />
{/* Server detail */}
<Route path="/org/:orgId/servers/:serverId" element={<ServerLayout />}>
<Route index element={<Navigate to="console" replace />} />
<Route path="console" element={<ConsolePage />} />
<Route path="files" element={<FilesPage />} />
<Route path="config" element={<ConfigPage />} />
<Route path="databases" element={<DatabasesPage />} />
<Route path="plugins" element={<PluginsPage />} />
<Route path="backups" element={<BackupsPage />} />
<Route path="schedules" element={<SchedulesPage />} />
<Route path="players" element={<PlayersPage />} />
<Route path="settings" element={<ServerSettingsPage />} />
{/* Server detail */}
<Route path="/org/:orgId/servers/:serverId" element={<ServerLayout />}>
<Route index element={<Navigate to="console" replace />} />
<Route path="console" element={<ConsolePage />} />
<Route path="files" element={<FilesPage />} />
<Route path="config" element={<ConfigPage />} />
<Route path="databases" element={<DatabasesPage />} />
<Route path="plugins" element={<PluginsPage />} />
<Route path="backups" element={<BackupsPage />} />
<Route path="schedules" element={<SchedulesPage />} />
<Route path="players" element={<PlayersPage />} />
<Route path="settings" element={<ServerSettingsPage />} />
</Route>
{/* Admin */}
<Route path="/admin/users" element={<AdminUsersPage />} />
<Route path="/admin/games" element={<AdminGamesPage />} />
<Route path="/admin/plugins" element={<AdminPluginsPage />} />
<Route path="/admin/nodes" element={<AdminNodesPage />} />
<Route path="/admin/audit-logs" element={<AdminAuditLogsPage />} />
</Route>
{/* Admin */}
<Route path="/admin/users" element={<AdminUsersPage />} />
<Route path="/admin/games" element={<AdminGamesPage />} />
<Route path="/admin/plugins" element={<AdminPluginsPage />} />
<Route path="/admin/nodes" element={<AdminNodesPage />} />
<Route path="/admin/audit-logs" element={<AdminAuditLogsPage />} />
</Route>
</Route>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
<Toaster position="bottom-right" richColors />
</TooltipProvider>
</QueryClientProvider>
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
<Toaster position="bottom-right" richColors />
</TooltipProvider>
</QueryClientProvider>
</ErrorBoundary>
);
}
@@ -1,6 +1,16 @@
import { Outlet, useParams, Link, useLocation } from 'react-router';
import { useQuery } from '@tanstack/react-query';
import { Terminal, FolderOpen, Settings, Calendar, HardDrive, Users, Puzzle, Settings2, Database as DatabaseIcon } from 'lucide-react';
import {
Terminal,
FolderOpen,
Settings,
Calendar,
HardDrive,
Users,
Puzzle,
Settings2,
Database as DatabaseIcon,
} from 'lucide-react';
import { cn } from '@source/ui';
import { api } from '@/lib/api';
import { Badge } from '@/components/ui/badge';
@@ -52,9 +62,7 @@ export function ServerLayout() {
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{server?.name ?? 'Loading...'}</h1>
{server && (
<Badge variant={statusBadgeVariant(server.status)}>{server.status}</Badge>
)}
{server && <Badge variant={statusBadgeVariant(server.status)}>{server.status}</Badge>}
</div>
{server && (
<p className="mt-1 text-sm text-muted-foreground">
+1 -2
View File
@@ -96,8 +96,7 @@ function NavSection({ items, currentPath }: { items: NavItem[]; currentPath: str
return (
<nav className="flex flex-col gap-1">
{items.map((item) => {
const isActive =
currentPath === item.href || currentPath.startsWith(item.href + '/');
const isActive = currentPath === item.href || currentPath.startsWith(item.href + '/');
return (
<Link key={item.href} to={item.href}>
<Button
@@ -93,11 +93,7 @@ export function PowerControls({ serverId, orgId, status }: PowerControlsProps) {
<Dialog>
<DialogTrigger asChild>
<Button
size="sm"
variant="destructive"
disabled={isTransitioning && !isRunning}
>
<Button size="sm" variant="destructive" disabled={isTransitioning && !isRunning}>
<Skull className="h-4 w-4" />
Kill
</Button>
+1 -2
View File
@@ -18,8 +18,7 @@ const badgeVariants = cva(
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
+6 -4
View File
@@ -10,7 +10,8 @@ const buttonVariants = cva(
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
@@ -30,15 +31,16 @@ const buttonVariants = cva(
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
);
},
);
Button.displayName = 'Button';
+13 -3
View File
@@ -3,7 +3,11 @@ import { cn } from '@source/ui';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('rounded-xl border bg-card text-card-foreground shadow', className)} {...props} />
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
{...props}
/>
),
);
Card.displayName = 'Card';
@@ -17,7 +21,11 @@ CardHeader.displayName = 'CardHeader';
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('font-semibold leading-none tracking-tight', className)} {...props} />
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
),
);
CardTitle.displayName = 'CardTitle';
@@ -30,7 +38,9 @@ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HT
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />,
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
);
CardContent.displayName = 'CardContent';
+9 -2
View File
@@ -53,7 +53,10 @@ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
);
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
const DialogTitle = React.forwardRef<
@@ -72,7 +75,11 @@ const DialogDescription = React.forwardRef<
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
+5 -1
View File
@@ -44,7 +44,11 @@ const DropdownMenuSeparator = React.forwardRef<
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
+5 -1
View File
@@ -6,7 +6,11 @@ const ScrollArea = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
+2 -4
View File
@@ -111,8 +111,7 @@ async function refreshToken(): Promise<boolean> {
}
export const api = {
get: <T>(path: string, params?: Record<string, string>) =>
request<T>(path, { params }),
get: <T>(path: string, params?: Record<string, string>) => request<T>(path, { params }),
post: <T>(path: string, body?: unknown) =>
request<T>(path, {
@@ -132,8 +131,7 @@ export const api = {
body: toRequestBody(body),
}),
delete: <T>(path: string) =>
request<T>(path, { method: 'DELETE' }),
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
};
export { ApiError };
+7 -5
View File
@@ -186,9 +186,7 @@ export function AdminGamesPage() {
<Label>Slug</Label>
<Input
value={slug}
onChange={(e) =>
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))
}
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
required
/>
</div>
@@ -241,7 +239,10 @@ export function AdminGamesPage() {
</p>
<p className="mt-2 font-mono text-xs">{game.dockerImage}</p>
<p>Port: {game.defaultPort}</p>
<p>Automation: {Array.isArray(game.automationRules) ? game.automationRules.length : 0} workflow</p>
<p>
Automation:{' '}
{Array.isArray(game.automationRules) ? game.automationRules.length : 0} workflow
</p>
</div>
<Button
variant="outline"
@@ -277,7 +278,8 @@ export function AdminGamesPage() {
<div className="space-y-2">
<Label>JSON</Label>
<p className="text-xs text-muted-foreground">
Supported events: server.created, server.install.completed, server.power.started, server.power.stopped
Supported events: server.created, server.install.completed, server.power.started,
server.power.stopped
</p>
<textarea
value={automationJson}
+9 -3
View File
@@ -42,14 +42,20 @@ export function AdminNodesPage() {
</div>
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
{node.isOnline ? (
<><Wifi className="mr-1 h-3 w-3" /> Online</>
<>
<Wifi className="mr-1 h-3 w-3" /> Online
</>
) : (
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
<>
<WifiOff className="mr-1 h-3 w-3" /> Offline
</>
)}
</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{node.fqdn}:{node.daemonPort}</p>
<p className="text-sm text-muted-foreground">
{node.fqdn}:{node.daemonPort}
</p>
<div className="mt-3 flex gap-4 text-sm">
<span>{formatBytes(node.memoryTotal)} RAM</span>
<span>{formatBytes(node.diskTotal)} Disk</span>
+66 -32
View File
@@ -197,12 +197,14 @@ export function AdminPluginsPage() {
const map = new Map<string, File>();
for (const item of prev) {
const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
const relative =
(item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
map.set(`${relative}::${item.size}::${item.lastModified}`, item);
}
for (const item of Array.from(incoming)) {
const relative = (item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
const relative =
(item as File & { webkitRelativePath?: string }).webkitRelativePath || item.name;
map.set(`${relative}::${item.size}::${item.lastModified}`, item);
}
@@ -211,12 +213,8 @@ export function AdminPluginsPage() {
};
const createPluginMutation = useMutation({
mutationFn: (body: {
gameId: string;
name: string;
slug?: string;
description?: string;
}) => api.post('/admin/plugins', body),
mutationFn: (body: { gameId: string; name: string; slug?: string; description?: string }) =>
api.post('/admin/plugins', body),
onSuccess: () => {
toast.success('Global plugin created');
setCreatePluginOpen(false);
@@ -366,15 +364,25 @@ export function AdminPluginsPage() {
</div>
<div className="space-y-2">
<Label>Name</Label>
<Input value={createPluginName} onChange={(e) => setCreatePluginName(e.target.value)} required />
<Input
value={createPluginName}
onChange={(e) => setCreatePluginName(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label>Slug (optional)</Label>
<Input value={createPluginSlug} onChange={(e) => setCreatePluginSlug(e.target.value)} />
<Input
value={createPluginSlug}
onChange={(e) => setCreatePluginSlug(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Description (optional)</Label>
<Input value={createPluginDescription} onChange={(e) => setCreatePluginDescription(e.target.value)} />
<Input
value={createPluginDescription}
onChange={(e) => setCreatePluginDescription(e.target.value)}
/>
</div>
<DialogFooter>
<Button type="submit" disabled={createPluginMutation.isPending}>
@@ -424,7 +432,9 @@ export function AdminPluginsPage() {
type="button"
onClick={() => setSelectedPluginId(plugin.id)}
className={`w-full rounded-md border px-3 py-2 text-left transition ${
selectedPluginId === plugin.id ? 'border-primary bg-primary/5' : 'hover:bg-muted/40'
selectedPluginId === plugin.id
? 'border-primary bg-primary/5'
: 'hover:bg-muted/40'
}`}
>
<div className="flex items-center gap-2">
@@ -454,11 +464,7 @@ export function AdminPluginsPage() {
>
<Copy className="h-4 w-4" /> Clone Latest
</Button>
<Button
size="sm"
onClick={() => openReleaseDialogFrom()}
disabled={!selectedPlugin}
>
<Button size="sm" onClick={() => openReleaseDialogFrom()} disabled={!selectedPlugin}>
<UploadCloud className="h-4 w-4" /> New Release
</Button>
</div>
@@ -478,9 +484,12 @@ export function AdminPluginsPage() {
<Badge variant="secondary">{release.artifactType}</Badge>
{!release.isPublished && <Badge variant="destructive">Unpublished</Badge>}
</div>
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">{release.artifactUrl}</p>
<p className="mt-1 line-clamp-1 text-xs text-muted-foreground">
{release.artifactUrl}
</p>
<p className="mt-1 text-xs text-muted-foreground">
Schema: {Array.isArray(release.installSchema) ? release.installSchema.length : 0} fields Templates:{' '}
Schema: {Array.isArray(release.installSchema) ? release.installSchema.length : 0}{' '}
fields Templates:{' '}
{Array.isArray(release.configTemplates) ? release.configTemplates.length : 0}
</p>
<div className="mt-2">
@@ -517,7 +526,9 @@ export function AdminPluginsPage() {
>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Publish Release{selectedPlugin ? ` - ${selectedPlugin.name}` : ''}</DialogTitle>
<DialogTitle>
Publish Release{selectedPlugin ? ` - ${selectedPlugin.name}` : ''}
</DialogTitle>
</DialogHeader>
<form
className="space-y-4"
@@ -540,7 +551,8 @@ export function AdminPluginsPage() {
const formData = new FormData();
formData.append('version', releaseVersion);
formData.append('channel', releaseChannel);
if (releaseDestination.trim()) formData.append('destination', releaseDestination.trim());
if (releaseDestination.trim())
formData.append('destination', releaseDestination.trim());
if (releaseFileName.trim()) formData.append('fileName', releaseFileName.trim());
if (releaseChangelog.trim()) formData.append('changelog', releaseChangelog);
if (releaseInstallSchemaFile) {
@@ -563,8 +575,12 @@ export function AdminPluginsPage() {
}
for (const file of releaseArtifactFiles) {
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath;
formData.append('relativePath', relativePath && relativePath.length > 0 ? relativePath : file.name);
const relativePath = (file as File & { webkitRelativePath?: string })
.webkitRelativePath;
formData.append(
'relativePath',
relativePath && relativePath.length > 0 ? relativePath : file.name,
);
formData.append('files', file, file.name);
}
@@ -592,7 +608,11 @@ export function AdminPluginsPage() {
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Label>Version</Label>
<Input value={releaseVersion} onChange={(e) => setReleaseVersion(e.target.value)} required />
<Input
value={releaseVersion}
onChange={(e) => setReleaseVersion(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label>Channel</Label>
@@ -658,8 +678,8 @@ export function AdminPluginsPage() {
{releaseInputMode === 'upload' && (
<div className="space-y-3 rounded-md border p-3">
<p className="text-xs text-muted-foreground">
Tek dosya secersen tekil upload olur. Birden fazla dosya veya klasor secersen otomatik zip
yapilip CDN&apos;e yuklenir.
Tek dosya secersen tekil upload olur. Birden fazla dosya veya klasor secersen
otomatik zip yapilip CDN&apos;e yuklenir.
</p>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
@@ -683,9 +703,16 @@ export function AdminPluginsPage() {
</div>
</div>
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Selected: {releaseArtifactFiles.length} file(s)</p>
<p className="text-xs text-muted-foreground">
Selected: {releaseArtifactFiles.length} file(s)
</p>
{releaseArtifactFiles.length > 0 && (
<Button type="button" variant="ghost" size="sm" onClick={() => setReleaseArtifactFiles([])}>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setReleaseArtifactFiles([])}
>
Clear
</Button>
)}
@@ -693,7 +720,8 @@ export function AdminPluginsPage() {
{releaseArtifactFiles.length > 0 && (
<div className="max-h-28 space-y-1 overflow-auto rounded bg-muted/40 p-2 text-xs">
{releaseArtifactFiles.map((file, index) => {
const relativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath;
const relativePath = (file as File & { webkitRelativePath?: string })
.webkitRelativePath;
return (
<p key={`${relativePath || file.name}-${index}`} className="truncate">
{relativePath || file.name}
@@ -745,7 +773,10 @@ export function AdminPluginsPage() {
/>
{releaseInstallSchemaFile && (
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<p>File secili: {releaseInstallSchemaFile.name}. Bu dosya, alttaki metni override eder.</p>
<p>
File secili: {releaseInstallSchemaFile.name}. Bu dosya, alttaki metni override
eder.
</p>
<Button
type="button"
variant="ghost"
@@ -776,7 +807,10 @@ export function AdminPluginsPage() {
/>
{releaseTemplatesFile && (
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<p>File secili: {releaseTemplatesFile.name}. Bu dosya, alttaki metni override eder.</p>
<p>
File secili: {releaseTemplatesFile.name}. Bu dosya, alttaki metni override
eder.
</p>
<Button
type="button"
variant="ghost"
@@ -807,7 +841,7 @@ export function AdminPluginsPage() {
!selectedPlugin
}
>
{(createReleaseMutation.isPending || createUploadReleaseMutation.isPending)
{createReleaseMutation.isPending || createUploadReleaseMutation.isPending
? 'Publishing...'
: 'Publish Release'}
</Button>
+11 -2
View File
@@ -4,7 +4,14 @@ import { Shield } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { useAuthStore } from '@/stores/auth';
import { ApiError } from '@/lib/api';
@@ -47,7 +54,9 @@ export function LoginPage() {
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
+11 -2
View File
@@ -4,7 +4,14 @@ import { Shield } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { useAuthStore } from '@/stores/auth';
import { ApiError } from '@/lib/api';
@@ -48,7 +55,9 @@ export function RegisterPage() {
<form onSubmit={handleSubmit}>
<CardContent className="space-y-4">
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{error}</div>
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
+3 -1
View File
@@ -54,7 +54,9 @@ export function DashboardPage() {
<div className="grid gap-4 sm:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Servers</CardTitle>
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Servers
</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
+20 -27
View File
@@ -93,17 +93,13 @@ export function NodeDetailPage() {
const { data: serversData } = useQuery({
queryKey: ['node-servers', orgId, nodeId],
queryFn: () =>
api.get<{ data: ServerSummary[] }>(
`/organizations/${orgId}/nodes/${nodeId}/servers`,
),
api.get<{ data: ServerSummary[] }>(`/organizations/${orgId}/nodes/${nodeId}/servers`),
});
const { data: allocData } = useQuery({
queryKey: ['allocations', orgId, nodeId],
queryFn: () =>
api.get<{ data: Allocation[] }>(
`/organizations/${orgId}/nodes/${nodeId}/allocations`,
),
api.get<{ data: Allocation[] }>(`/organizations/${orgId}/nodes/${nodeId}/allocations`),
});
const allocations = allocData?.data ?? [];
@@ -142,12 +138,10 @@ export function NodeDetailPage() {
);
}
const memPercent = stats && stats.memoryTotal > 0
? Math.round((stats.memoryUsed / stats.memoryTotal) * 100)
: 0;
const diskPercent = stats && stats.diskTotal > 0
? Math.round((stats.diskUsed / stats.diskTotal) * 100)
: 0;
const memPercent =
stats && stats.memoryTotal > 0 ? Math.round((stats.memoryUsed / stats.memoryTotal) * 100) : 0;
const diskPercent =
stats && stats.diskTotal > 0 ? Math.round((stats.diskUsed / stats.diskTotal) * 100) : 0;
return (
<div className="space-y-6">
@@ -167,9 +161,13 @@ export function NodeDetailPage() {
</div>
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
{node.isOnline ? (
<><Wifi className="mr-1 h-3 w-3" /> Online</>
<>
<Wifi className="mr-1 h-3 w-3" /> Online
</>
) : (
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
<>
<WifiOff className="mr-1 h-3 w-3" /> Offline
</>
)}
</Badge>
</div>
@@ -197,9 +195,7 @@ export function NodeDetailPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{stats
? `${formatBytes(stats.memoryUsed)} / ${formatBytes(stats.memoryTotal)}`
: '—'}
{stats ? `${formatBytes(stats.memoryUsed)} / ${formatBytes(stats.memoryTotal)}` : '—'}
</div>
<Progress value={memPercent} className="mt-2 h-2" />
</CardContent>
@@ -212,9 +208,7 @@ export function NodeDetailPage() {
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{stats
? `${formatBytes(stats.diskUsed)} / ${formatBytes(stats.diskTotal)}`
: '—'}
{stats ? `${formatBytes(stats.diskUsed)} / ${formatBytes(stats.diskTotal)}` : '—'}
</div>
<Progress value={diskPercent} className="mt-2 h-2" />
</CardContent>
@@ -248,9 +242,7 @@ export function NodeDetailPage() {
<InfoRow label="gRPC Port" value={String(node.grpcPort)} />
<InfoRow label="Total Memory" value={formatBytes(node.memoryTotal)} />
<InfoRow label="Total Disk" value={formatBytes(node.diskTotal)} />
{node.daemonVersion && (
<InfoRow label="Daemon Version" value={node.daemonVersion} />
)}
{node.daemonVersion && <InfoRow label="Daemon Version" value={node.daemonVersion} />}
<InfoRow label="Created" value={new Date(node.createdAt).toLocaleDateString()} />
</CardContent>
</Card>
@@ -277,9 +269,7 @@ export function NodeDetailPage() {
<p className="text-xs text-muted-foreground">{srv.gameName}</p>
</div>
<div className="flex items-center gap-2">
<Badge
variant={srv.status === 'running' ? 'default' : 'outline'}
>
<Badge variant={srv.status === 'running' ? 'default' : 'outline'}>
{srv.status}
</Badge>
<span className="text-xs text-muted-foreground">
@@ -373,7 +363,10 @@ export function NodeDetailPage() {
/** Parse port input like "25565, 25566-25570, 27015" into flat number array */
function parsePorts(input: string): number[] {
const ports: number[] = [];
const parts = input.split(',').map((s) => s.trim()).filter(Boolean);
const parts = input
.split(',')
.map((s) => s.trim())
.filter(Boolean);
for (const part of parts) {
if (part.includes('-')) {
const [startStr, endStr] = part.split('-');
+32 -33
View File
@@ -183,16 +183,8 @@ export function NodesPage() {
<div className="space-y-3">
<Label>Daemon Token</Label>
<div className="flex gap-2">
<Input
readOnly
value={createdToken}
className="font-mono text-xs"
/>
<Button
variant="outline"
size="icon"
onClick={handleCopyToken}
>
<Input readOnly value={createdToken} className="font-mono text-xs" />
<Button variant="outline" size="icon" onClick={handleCopyToken}>
{copied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
@@ -201,7 +193,8 @@ export function NodesPage() {
</Button>
</div>
<p className="text-xs text-muted-foreground">
Use this token in your daemon configuration file (config.yml) to authenticate with the panel.
Use this token in your daemon configuration file (config.yml) to authenticate with the
panel.
</p>
</div>
<DialogFooter>
@@ -213,28 +206,34 @@ export function NodesPage() {
<div className="grid gap-4 sm:grid-cols-2">
{nodes.map((node) => (
<Link key={node.id} to={`/org/${orgId}/nodes/${node.id}`}>
<Card className="transition-colors hover:bg-muted/50 cursor-pointer">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="flex items-center gap-3">
<Network className="h-5 w-5 text-primary" />
<CardTitle className="text-base">{node.name}</CardTitle>
</div>
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
{node.isOnline ? (
<><Wifi className="mr-1 h-3 w-3" /> Online</>
) : (
<><WifiOff className="mr-1 h-3 w-3" /> Offline</>
)}
</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{node.fqdn}:{node.daemonPort}</p>
<div className="mt-3 flex gap-4 text-sm">
<span>{formatBytes(node.memoryTotal)} RAM</span>
<span>{formatBytes(node.diskTotal)} Disk</span>
</div>
</CardContent>
</Card>
<Card className="transition-colors hover:bg-muted/50 cursor-pointer">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="flex items-center gap-3">
<Network className="h-5 w-5 text-primary" />
<CardTitle className="text-base">{node.name}</CardTitle>
</div>
<Badge variant={node.isOnline ? 'default' : 'destructive'}>
{node.isOnline ? (
<>
<Wifi className="mr-1 h-3 w-3" /> Online
</>
) : (
<>
<WifiOff className="mr-1 h-3 w-3" /> Offline
</>
)}
</Badge>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{node.fqdn}:{node.daemonPort}
</p>
<div className="mt-3 flex gap-4 text-sm">
<span>{formatBytes(node.memoryTotal)} RAM</span>
<span>{formatBytes(node.diskTotal)} Disk</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
+8 -19
View File
@@ -54,16 +54,13 @@ export function BackupsPage() {
const { data } = useQuery({
queryKey: ['backups', orgId, serverId],
queryFn: () =>
api.get<{ backups: Backup[] }>(
`/organizations/${orgId}/servers/${serverId}/backups`,
),
api.get<{ backups: Backup[] }>(`/organizations/${orgId}/servers/${serverId}/backups`),
});
const deleteMutation = useMutation({
mutationFn: (backupId: string) =>
api.delete(`/organizations/${orgId}/servers/${serverId}/backups/${backupId}`),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
});
const restoreMutation = useMutation({
@@ -75,8 +72,7 @@ export function BackupsPage() {
const lockMutation = useMutation({
mutationFn: (backupId: string) =>
api.patch(`/organizations/${orgId}/servers/${serverId}/backups/${backupId}/lock`, {}),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['backups', orgId, serverId] }),
});
const backupList = data?.backups ?? [];
@@ -89,7 +85,8 @@ export function BackupsPage() {
<div>
<h2 className="text-lg font-semibold">Backups</h2>
<p className="text-xs text-muted-foreground">
{backupList.length} backup{backupList.length !== 1 ? 's' : ''} {formatBytes(totalSize)} total
{backupList.length} backup{backupList.length !== 1 ? 's' : ''} {' '}
{formatBytes(totalSize)} total
</p>
</div>
<Dialog open={showCreate} onOpenChange={setShowCreate}>
@@ -154,9 +151,7 @@ export function BackupsPage() {
<span>{formatBytes(backup.sizeBytes)}</span>
<span>{new Date(backup.createdAt).toLocaleString()}</span>
{backup.checksum && (
<span className="font-mono">
{backup.checksum.slice(0, 12)}...
</span>
<span className="font-mono">{backup.checksum.slice(0, 12)}...</span>
)}
</div>
</div>
@@ -238,9 +233,7 @@ function CreateBackupForm({
onClose: () => void;
}) {
const queryClient = useQueryClient();
const [name, setName] = useState(
`backup-${new Date().toISOString().slice(0, 10)}`,
);
const [name, setName] = useState(`backup-${new Date().toISOString().slice(0, 10)}`);
const createMutation = useMutation({
mutationFn: (data: { name: string }) =>
@@ -261,11 +254,7 @@ function CreateBackupForm({
>
<div className="grid gap-1.5">
<Label>Backup Name</Label>
<Input
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<Input value={name} onChange={(e) => setName(e.target.value)} required />
</div>
<div className="flex justify-end gap-2 pt-2">
<Button type="button" variant="outline" onClick={onClose}>
+9 -21
View File
@@ -30,10 +30,7 @@ interface ConfigDetail {
raw: string;
}
function mergeConfigEntries(
entries: ConfigEntry[],
editableKeys: string[] | null,
): ConfigEntry[] {
function mergeConfigEntries(entries: ConfigEntry[], editableKeys: string[] | null): ConfigEntry[] {
if (!editableKeys || editableKeys.length === 0) return entries;
const existing = new Map(entries.map((entry) => [entry.key, entry]));
@@ -55,9 +52,7 @@ export function ConfigPage() {
const { data: configsData } = useQuery({
queryKey: ['configs', orgId, serverId],
queryFn: () =>
api.get<{ configs: ConfigFile[] }>(
`/organizations/${orgId}/servers/${serverId}/config`,
),
api.get<{ configs: ConfigFile[] }>(`/organizations/${orgId}/servers/${serverId}/config`),
});
const configs = configsData?.configs ?? [];
@@ -114,9 +109,7 @@ function ConfigEditor({
const { data: detail } = useQuery({
queryKey: ['config-detail', orgId, serverId, configIndex],
queryFn: () =>
api.get<ConfigDetail>(
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
),
api.get<ConfigDetail>(`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`),
});
const [entries, setEntries] = useState<ConfigEntry[]>([]);
@@ -128,10 +121,7 @@ function ConfigEditor({
const saveMutation = useMutation({
mutationFn: (data: { entries: ConfigEntry[] }) =>
api.put(
`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`,
data,
),
api.put(`/organizations/${orgId}/servers/${serverId}/config/${configIndex}`, data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ['config-detail', orgId, serverId, configIndex],
@@ -140,9 +130,7 @@ function ConfigEditor({
});
const updateEntry = (key: string, value: string) => {
setEntries((prev) =>
prev.map((e) => (e.key === key ? { ...e, value } : e)),
);
setEntries((prev) => prev.map((e) => (e.key === key ? { ...e, value } : e)));
};
return (
@@ -171,15 +159,15 @@ function ConfigEditor({
<CardContent>
{entries.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
{detail ? 'No entries found. The server may need to be started first to generate config files.' : 'Loading...'}
{detail
? 'No entries found. The server may need to be started first to generate config files.'
: 'Loading...'}
</p>
) : (
<div className="space-y-3">
{entries.map((entry) => (
<div key={entry.key} className="grid gap-1.5">
<Label className="font-mono text-xs text-muted-foreground">
{entry.key}
</Label>
<Label className="font-mono text-xs text-muted-foreground">{entry.key}</Label>
<Input
value={entry.value}
onChange={(e) => updateEntry(entry.key, e.target.value)}
+13 -13
View File
@@ -6,7 +6,14 @@ import { toast } from 'sonner';
import { ApiError, api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -38,9 +45,7 @@ function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="space-y-1">
<p className="text-xs uppercase tracking-wide text-muted-foreground">{label}</p>
<div className="rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs">
{value}
</div>
<div className="rounded-md border bg-muted/40 px-3 py-2 font-mono text-xs">{value}</div>
</div>
);
}
@@ -59,9 +64,7 @@ export function DatabasesPage() {
const { data, isLoading } = useQuery({
queryKey: ['server-databases', orgId, serverId],
queryFn: () =>
api.get<{ data: ManagedDatabase[] }>(
`/organizations/${orgId}/servers/${serverId}/databases`,
),
api.get<{ data: ManagedDatabase[] }>(`/organizations/${orgId}/servers/${serverId}/databases`),
});
useEffect(() => {
@@ -270,11 +273,7 @@ export function DatabasesPage() {
</a>
</Button>
) : null}
<Button
size="sm"
variant="outline"
onClick={() => setEditingDatabase(database)}
>
<Button size="sm" variant="outline" onClick={() => setEditingDatabase(database)}>
<RefreshCw className="h-4 w-4" /> Edit
</Button>
<Button
@@ -307,7 +306,8 @@ export function DatabasesPage() {
/>
{!database.phpMyAdminUrl ? (
<p className="text-xs text-muted-foreground">
phpMyAdmin link is not configured. Set `managed_mysql.phpmyadmin_url` in the daemon config for this node.
phpMyAdmin link is not configured. Set `managed_mysql.phpmyadmin_url` in the
daemon config for this node.
</p>
) : null}
</CardContent>
+11 -35
View File
@@ -68,10 +68,7 @@ function joinRemotePath(basePath: string, relativePath: string): string {
.split('/')
.filter((segment) => segment && segment !== '.' && segment !== '..');
const baseSegments = basePath
.replace(/\\/g, '/')
.split('/')
.filter(Boolean);
const baseSegments = basePath.replace(/\\/g, '/').split('/').filter(Boolean);
return `/${[...baseSegments, ...safeSegments].join('/')}`.replace(/\/{2,}/g, '/');
}
@@ -133,18 +130,16 @@ export function FilesPage() {
null,
);
const hasUnsavedChanges =
!!editingFile && editingFile.content !== editingFile.originalContent;
const hasUnsavedChanges = !!editingFile && editingFile.content !== editingFile.originalContent;
const isUploading = !!uploadProgress;
const filesQuery = useQuery({
queryKey: ['files', orgId, serverId, currentPath],
enabled: Boolean(orgId && serverId) && !editingFile,
queryFn: () =>
api.get<{ files: FileEntry[] }>(
`/organizations/${orgId}/servers/${serverId}/files`,
{ path: currentPath },
),
api.get<{ files: FileEntry[] }>(`/organizations/${orgId}/servers/${serverId}/files`, {
path: currentPath,
}),
});
const deleteMutation = useMutation({
@@ -528,9 +523,7 @@ export function FilesPage() {
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
{hasUnsavedChanges && (
<span className="text-xs text-amber-600">Unsaved changes</span>
)}
{hasUnsavedChanges && <span className="text-xs text-amber-600">Unsaved changes</span>}
<Button
size="sm"
onClick={saveCurrentFile}
@@ -550,9 +543,7 @@ export function FilesPage() {
ref={editorRef}
value={editingFile.content}
onChange={(event) =>
setEditingFile((prev) =>
prev ? { ...prev, content: event.target.value } : prev,
)
setEditingFile((prev) => (prev ? { ...prev, content: event.target.value } : prev))
}
onKeyDown={handleEditorKeyDown}
className="min-h-[560px] w-full resize-y rounded-md border bg-background p-3 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-ring"
@@ -611,21 +602,11 @@ export function FilesPage() {
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
size="sm"
variant="outline"
onClick={triggerUploadFiles}
disabled={isUploading}
>
<Button size="sm" variant="outline" onClick={triggerUploadFiles} disabled={isUploading}>
<Upload className="h-4 w-4" />
Upload Files
</Button>
<Button
size="sm"
variant="outline"
onClick={triggerUploadFolder}
disabled={isUploading}
>
<Button size="sm" variant="outline" onClick={triggerUploadFolder} disabled={isUploading}>
<Upload className="h-4 w-4" />
Upload Folder
</Button>
@@ -746,11 +727,7 @@ export function FilesPage() {
{filesQuery.isError && (
<div className="space-y-2 py-8 text-center">
<p className="text-sm text-destructive">Failed to load directory</p>
<Button
size="sm"
variant="outline"
onClick={() => filesQuery.refetch()}
>
<Button size="sm" variant="outline" onClick={() => filesQuery.refetch()}>
Retry
</Button>
</div>
@@ -826,8 +803,7 @@ export function FilesPage() {
<DialogTitle>{deleteTarget?.isDirectory ? 'Delete Folder' : 'Delete File'}</DialogTitle>
</DialogHeader>
<p className="text-sm text-muted-foreground">
Are you sure you want to delete{' '}
<code className="font-mono">{deleteTarget?.path}</code>?
Are you sure you want to delete <code className="font-mono">{deleteTarget?.path}</code>?
</p>
<DialogFooter>
<DialogClose asChild>
+1 -3
View File
@@ -21,9 +21,7 @@ export function PlayersPage() {
const { data, isLoading, refetch } = useQuery({
queryKey: ['players', orgId, serverId],
queryFn: () =>
api.get<PlayerListResponse>(
`/organizations/${orgId}/servers/${serverId}/players`,
),
api.get<PlayerListResponse>(`/organizations/${orgId}/servers/${serverId}/players`),
refetchInterval: 30000,
});
+19 -8
View File
@@ -293,7 +293,9 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
const [installTarget, setInstallTarget] = useState<MarketplacePlugin | null>(null);
const [installOptions, setInstallOptions] = useState<Record<string, unknown>>({});
const [installPinVersion, setInstallPinVersion] = useState(false);
const [installAutoUpdateChannel, setInstallAutoUpdateChannel] = useState<'stable' | 'beta' | 'alpha'>('stable');
const [installAutoUpdateChannel, setInstallAutoUpdateChannel] = useState<
'stable' | 'beta' | 'alpha'
>('stable');
const { data, isLoading } = useQuery({
queryKey: ['plugin-marketplace', orgId, serverId, searchTerm],
@@ -317,7 +319,10 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
autoUpdateChannel?: 'stable' | 'beta' | 'alpha';
};
}) =>
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/install/${pluginId}`, payload ?? {}),
api.post(
`/organizations/${orgId}/servers/${serverId}/plugins/install/${pluginId}`,
payload ?? {},
),
onSuccess: () => {
toast.success('Plugin installed');
setInstallDialogOpen(false);
@@ -364,8 +369,7 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
description?: string;
downloadUrl: string;
version?: string;
}) =>
api.post(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, body),
}) => api.post(`/organizations/${orgId}/servers/${serverId}/plugins/marketplace`, body),
onSuccess: () => {
toast.success('Marketplace plugin added');
setCreateOpen(false);
@@ -655,7 +659,9 @@ function MarketplacePlugins({ orgId, serverId }: { orgId: string; serverId: stri
<Button
size="sm"
variant="secondary"
onClick={() => updateInstallMutation.mutate({ installId: plugin.installId! })}
onClick={() =>
updateInstallMutation.mutate({ installId: plugin.installId! })
}
disabled={updateInstallMutation.isPending}
>
<Download className="h-4 w-4" />
@@ -893,14 +899,18 @@ function InstalledPlugins({
<div className="flex flex-wrap items-center gap-2">
<p className="font-medium">{plugin.name}</p>
<Badge variant="outline">{plugin.source}</Badge>
{plugin.installedVersion && <Badge variant="secondary">v{plugin.installedVersion}</Badge>}
{plugin.installedVersion && (
<Badge variant="secondary">v{plugin.installedVersion}</Badge>
)}
{!plugin.isActive && <Badge variant="secondary">Disabled</Badge>}
{plugin.status !== 'installed' && (
<Badge variant={plugin.status === 'failed' ? 'destructive' : 'outline'}>
{plugin.status}
</Badge>
)}
{plugin.updateAvailable && <Badge variant="destructive">Update Available</Badge>}
{plugin.updateAvailable && (
<Badge variant="destructive">Update Available</Badge>
)}
</div>
{plugin.description && (
<p className="text-sm text-muted-foreground">{plugin.description}</p>
@@ -1157,7 +1167,8 @@ function ManualInstall({ orgId, serverId }: { orgId: string; serverId: string })
required
/>
<p className="text-xs text-muted-foreground">
Files sekmesinden dosyayı önce sunucuya yükleyin. Relative path girerseniz oyunun varsayılan plugin dizinine göre çözülür.
Files sekmesinden dosyayı önce sunucuya yükleyin. Relative path girerseniz oyunun
varsayılan plugin dizinine göre çözülür.
</p>
</div>
<div className="space-y-2">
+34 -28
View File
@@ -63,30 +63,25 @@ export function SchedulesPage() {
const { data } = useQuery({
queryKey: ['schedules', orgId, serverId],
queryFn: () =>
api.get<{ tasks: ScheduledTask[] }>(
`/organizations/${orgId}/servers/${serverId}/schedules`,
),
api.get<{ tasks: ScheduledTask[] }>(`/organizations/${orgId}/servers/${serverId}/schedules`),
});
const deleteMutation = useMutation({
mutationFn: (taskId: string) =>
api.delete(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}`),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
});
const triggerMutation = useMutation({
mutationFn: (taskId: string) =>
api.post(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}/trigger`, {}),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
});
const toggleMutation = useMutation({
mutationFn: ({ taskId, isActive }: { taskId: string; isActive: boolean }) =>
api.patch(`/organizations/${orgId}/servers/${serverId}/schedules/${taskId}`, { isActive }),
onSuccess: () =>
queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['schedules', orgId, serverId] }),
});
const tasks = data?.tasks ?? [];
@@ -150,14 +145,10 @@ export function SchedulesPage() {
{formatSchedule(task.scheduleType, task.scheduleData)}
</span>
{task.nextRunAt && (
<span>
Next: {new Date(task.nextRunAt).toLocaleString()}
</span>
<span>Next: {new Date(task.nextRunAt).toLocaleString()}</span>
)}
{task.lastRunAt && (
<span>
Last: {new Date(task.lastRunAt).toLocaleString()}
</span>
<span>Last: {new Date(task.lastRunAt).toLocaleString()}</span>
)}
</div>
{task.action === 'command' && (
@@ -188,11 +179,7 @@ export function SchedulesPage() {
}
title={task.isActive ? 'Pause' : 'Resume'}
>
{task.isActive ? (
<Pause className="h-4 w-4" />
) : (
<Play className="h-4 w-4" />
)}
{task.isActive ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
@@ -243,7 +230,9 @@ function CreateScheduleForm({
const [name, setName] = useState('');
const [action, setAction] = useState<'command' | 'power' | 'backup'>('command');
const [payload, setPayload] = useState('');
const [scheduleType, setScheduleType] = useState<'interval' | 'daily' | 'weekly' | 'cron'>('interval');
const [scheduleType, setScheduleType] = useState<'interval' | 'daily' | 'weekly' | 'cron'>(
'interval',
);
// Schedule data fields
const [minutes, setMinutes] = useState('60');
@@ -268,7 +257,11 @@ function CreateScheduleForm({
case 'daily':
return { hour: parseInt(hour, 10), minute: parseInt(minute, 10) };
case 'weekly':
return { dayOfWeek: parseInt(dayOfWeek, 10), hour: parseInt(hour, 10), minute: parseInt(minute, 10) };
return {
dayOfWeek: parseInt(dayOfWeek, 10),
hour: parseInt(hour, 10),
minute: parseInt(minute, 10),
};
case 'cron':
return { expression: cronExpression };
}
@@ -301,7 +294,9 @@ function CreateScheduleForm({
<div className="grid gap-1.5">
<Label>Action</Label>
<Select value={action} onValueChange={(v) => setAction(v as typeof action)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="command">Run Command</SelectItem>
<SelectItem value="power">Power Action</SelectItem>
@@ -322,7 +317,9 @@ function CreateScheduleForm({
/>
) : (
<Select value={payload} onValueChange={setPayload}>
<SelectTrigger><SelectValue placeholder="Select..." /></SelectTrigger>
<SelectTrigger>
<SelectValue placeholder="Select..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="start">Start</SelectItem>
<SelectItem value="stop">Stop</SelectItem>
@@ -337,8 +334,13 @@ function CreateScheduleForm({
<div className="grid gap-1.5">
<Label>Schedule Type</Label>
<Select value={scheduleType} onValueChange={(v) => setScheduleType(v as typeof scheduleType)}>
<SelectTrigger><SelectValue /></SelectTrigger>
<Select
value={scheduleType}
onValueChange={(v) => setScheduleType(v as typeof scheduleType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="interval">Interval</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
@@ -366,10 +368,14 @@ function CreateScheduleForm({
<div className="col-span-2 grid gap-1.5">
<Label>Day of Week</Label>
<Select value={dayOfWeek} onValueChange={setDayOfWeek}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{DAYS_OF_WEEK.map((day, i) => (
<SelectItem key={day} value={String(i)}>{day}</SelectItem>
<SelectItem key={day} value={String(i)}>
{day}
</SelectItem>
))}
</SelectContent>
</Select>
+52 -28
View File
@@ -8,7 +8,13 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { formatBytes } from '@/lib/utils';
interface ServerDetail {
@@ -244,9 +250,13 @@ export function ServerSettingsPage() {
const [description, setDescription] = useState('');
const [startupOverride, setStartupOverride] = useState('');
const [environmentFields, setEnvironmentFields] = useState<EnvironmentField[]>([]);
const [automationEvent, setAutomationEvent] = useState<AutomationEvent>('server.install.completed');
const [automationEvent, setAutomationEvent] = useState<AutomationEvent>(
'server.install.completed',
);
const [forceAutomationRun, setForceAutomationRun] = useState(false);
const [lastAutomationResult, setLastAutomationResult] = useState<AutomationRunResult | null>(null);
const [lastAutomationResult, setLastAutomationResult] = useState<AutomationRunResult | null>(
null,
);
const { data: gamesData } = useQuery({
queryKey: ['games'],
@@ -290,7 +300,10 @@ export function ServerSettingsPage() {
const automationRunMutation = useMutation({
mutationFn: (body: { event: AutomationEvent; force: boolean }) =>
api.post<AutomationRunResponse>(`/organizations/${orgId}/servers/${serverId}/automation/run`, body),
api.post<AutomationRunResponse>(
`/organizations/${orgId}/servers/${serverId}/automation/run`,
body,
),
onSuccess: (response) => {
setLastAutomationResult(response.result);
if (response.result.workflowsFailed > 0 || response.result.actionFailures > 0) {
@@ -442,7 +455,7 @@ export function ServerSettingsPage() {
</p>
) : (
<div className="space-y-3">
{environmentFields.map((field, index) => (
{environmentFields.map((field, index) =>
field.isCustom ? (
<div
key={`custom-${index}`}
@@ -477,7 +490,8 @@ export function ServerSettingsPage() {
{field.label}
</Label>
<span className="text-[11px] text-muted-foreground">
Default: <span className="font-mono">{field.defaultValue || 'empty'}</span>
Default:{' '}
<span className="font-mono">{field.defaultValue || 'empty'}</span>
</span>
</div>
{field.inputType === 'boolean' ? (
@@ -513,16 +527,13 @@ export function ServerSettingsPage() {
</p>
)}
</div>
)
))}
),
)}
</div>
)}
</div>
<Button
onClick={saveStartupSettings}
disabled={updateMutation.isPending || !server}
>
<Button onClick={saveStartupSettings} disabled={updateMutation.isPending || !server}>
{updateMutation.isPending ? 'Applying...' : 'Save Startup Settings'}
</Button>
</CardContent>
@@ -536,7 +547,10 @@ export function ServerSettingsPage() {
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Event</Label>
<Select value={automationEvent} onValueChange={(value) => setAutomationEvent(value as AutomationEvent)}>
<Select
value={automationEvent}
onValueChange={(value) => setAutomationEvent(value as AutomationEvent)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -560,7 +574,9 @@ export function ServerSettingsPage() {
</Button>
<Button
type="button"
onClick={() => automationRunMutation.mutate({ event: automationEvent, force: forceAutomationRun })}
onClick={() =>
automationRunMutation.mutate({ event: automationEvent, force: forceAutomationRun })
}
disabled={automationRunMutation.isPending}
>
{automationRunMutation.isPending ? 'Running...' : 'Run Automation Event'}
@@ -601,8 +617,12 @@ export function ServerSettingsPage() {
<p className="text-sm font-medium text-destructive">Failure Details</p>
<div className="space-y-1">
{lastAutomationResult.failures.slice(0, 5).map((failure, index) => (
<p key={`${failure.workflowId}-${failure.actionId ?? 'workflow'}-${index}`} className="text-xs text-destructive">
[{failure.workflowId}{failure.actionId ? ` > ${failure.actionId}` : ''}] {failure.message}
<p
key={`${failure.workflowId}-${failure.actionId ?? 'workflow'}-${index}`}
className="text-xs text-destructive"
>
[{failure.workflowId}
{failure.actionId ? ` > ${failure.actionId}` : ''}] {failure.message}
</p>
))}
</div>
@@ -611,20 +631,22 @@ export function ServerSettingsPage() {
</div>
)}
{automationRunMutation.isSuccess && lastAutomationResult && lastAutomationResult.failures.length === 0 && (
<p className="text-xs text-green-600">Automation run completed successfully.</p>
)}
{automationRunMutation.isSuccess &&
lastAutomationResult &&
lastAutomationResult.failures.length === 0 && (
<p className="text-xs text-green-600">Automation run completed successfully.</p>
)}
{automationRunMutation.isSuccess && lastAutomationResult && lastAutomationResult.failures.length > 0 && (
<p className="text-xs text-destructive">
Automation run completed with {lastAutomationResult.failures.length} error(s).
</p>
)}
{automationRunMutation.isSuccess &&
lastAutomationResult &&
lastAutomationResult.failures.length > 0 && (
<p className="text-xs text-destructive">
Automation run completed with {lastAutomationResult.failures.length} error(s).
</p>
)}
{automationRunMutation.isError && (
<p className="text-xs text-destructive">
Failed to run automation event.
</p>
<p className="text-xs text-destructive">Failed to run automation event.</p>
)}
</CardContent>
</Card>
@@ -639,7 +661,9 @@ export function ServerSettingsPage() {
variant="destructive"
disabled={deleteMutation.isPending}
onClick={() => {
if (!window.confirm('Delete this server permanently? This action cannot be undone.')) {
if (
!window.confirm('Delete this server permanently? This action cannot be undone.')
) {
return;
}
deleteMutation.mutate();
+9 -10
View File
@@ -16,7 +16,13 @@ import {
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
interface Member {
id: string;
@@ -78,21 +84,14 @@ export function MembersPage() {
});
const removeMutation = useMutation({
mutationFn: (memberId: string) =>
api.delete(`/organizations/${orgId}/members/${memberId}`),
mutationFn: (memberId: string) => api.delete(`/organizations/${orgId}/members/${memberId}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['members', orgId] });
},
});
const updateMutation = useMutation({
mutationFn: ({
memberId,
preset,
}: {
memberId: string;
preset: MembershipPreset;
}) =>
mutationFn: ({ memberId, preset }: { memberId: string; preset: MembershipPreset }) =>
api.patch(`/organizations/${orgId}/members/${memberId}`, buildPresetPayload(preset)),
onMutate: ({ memberId }) => {
setUpdatingMemberId(memberId);