feat(v5): add WireGuard and coold/corrosion config to clusters/servers

Add WireGuard networking fields (interface, management pool, listen port),
container network pool, coold/corrosion versioning, and builder CPU quota
to clusters and servers via new migrations and model fillables.

Expose full cluster and server CRUD on the Clusters page with private key
selection, pending state tracking, and new form primitives (Field, Input,
Textarea). Add server status check fields and a lima test VM config for
development.
This commit is contained in:
Andras Bacsai committed 2026-06-19 07:23:45 +02:00
1 parent 5d57b131ff
commit 3ae87334bd
30 files changed
+3876 -103

No files matched your search

+8
View File
@@ -105,6 +105,14 @@ body {
@apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400;
}
@media (max-width: 767px) {
input,
textarea,
select {
font-size: 16px;
}
}
body {
@apply min-h-screen text-sm font-sans antialiased scrollbar overflow-x-hidden;
}
+8
View File
@@ -152,6 +152,14 @@
@apply font-sans;
}
@media (max-width: 767px) {
input,
textarea,
select {
font-size: 16px;
}
}
body {
@apply bg-background text-foreground;
min-height: 100vh;
File diff suppressed because it is too large. Load diff
+1
View File
@@ -19,6 +19,7 @@ const buttonVariants = cva(
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
delete: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
+24 -3
View File
@@ -1,6 +1,8 @@
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { XIcon } from '@phosphor-icons/react';
import type * as React from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
@@ -21,18 +23,37 @@ function DialogOverlay({ className, ...props }: React.ComponentProps<typeof Dial
);
}
function DialogContent({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Popup>) {
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Popup> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
'fixed left-1/2 top-1/2 w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border border-border bg-card p-6 shadow-lg outline-none',
'fixed left-1/2 top-1/2 z-50 max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-lg border border-border bg-card p-6 shadow-lg outline-none',
className,
)}
{...props}
/>
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
aria-label="Close dialog"
render={<Button type="button" variant="ghost" size="icon-sm" className="absolute top-3 right-3" />}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
);
}
+35
View File
@@ -0,0 +1,35 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
type FieldProps = React.ComponentProps<'label'>;
function Field({ className, ...props }: FieldProps) {
return <label data-slot="field" className={cn('flex flex-col gap-1 text-sm', className)} {...props} />;
}
type FieldLabelProps = React.ComponentProps<'span'>;
function FieldLabel({ className, ...props }: FieldLabelProps) {
return <span data-slot="field-label" className={cn('font-medium text-foreground', className)} {...props} />;
}
type FieldErrorProps = React.ComponentProps<'span'> & {
message?: string;
};
function FieldError({ className, message, ...props }: FieldErrorProps) {
return (
<span
aria-hidden={message ? undefined : true}
aria-live="polite"
data-slot="field-error"
className={cn('min-h-4 text-xs leading-4 text-destructive', className)}
{...props}
>
{message ?? ''}
</span>
);
}
export { Field, FieldError, FieldLabel };
+18
View File
@@ -0,0 +1,18 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Input({ className, ...props }: React.ComponentProps<'input'>) {
return (
<input
data-slot="input"
className={cn(
'rounded-md border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-ring focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
className,
)}
{...props}
/>
);
}
export { Input };
@@ -0,0 +1,18 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'min-h-24 rounded-md border border-border bg-background px-3 py-2 text-sm outline-none transition focus:border-ring focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
className,
)}
{...props}
/>
);
}
export { Textarea };
+28
View File
@@ -0,0 +1,28 @@
import { useCallback, useState } from 'react';
export function usePendingIds<T extends string | number>() {
const [pendingIds, setPendingIds] = useState<Set<T>>(() => new Set());
const start = useCallback((id: T): void => {
setPendingIds((currentIds) => new Set(currentIds).add(id));
}, []);
const finish = useCallback((id: T): void => {
setPendingIds((currentIds) => {
const nextIds = new Set(currentIds);
nextIds.delete(id);
return nextIds;
});
}, []);
const has = useCallback((id: T): boolean => pendingIds.has(id), [pendingIds]);
return {
pendingIds,
has,
hasAny: pendingIds.size > 0,
start,
finish,
};
}
+36 -2
View File
@@ -9,20 +9,48 @@ export type V5Server = {
id: string;
name: string;
host: string;
sshUser: string;
sshPort: number;
status: string;
capabilities: string[];
builderEnabled: boolean;
builderCapacity: number;
builderCpuQuota: string;
nodeAddress: string | null;
wireguardListenPortOverride: number | null;
wireguardEndpointOverride: string | null;
wireguardManagementIp: string | null;
wireguardPublicKey: string | null;
containerSubnets: Record<string, string> | string[];
privateKeyName: string | null;
lastBootstrappedAt: string | null;
lastStatusCheck: string | null;
lastStatusOutput: string | null;
lastStatusCheckedAt: string | null;
};
export type V5Cluster = {
id: string;
name: string;
description: string | null;
wireguardInterface: string;
wireguardManagementPool: string;
wireguardListenPort: number;
containerNetworkPool: string;
containerNetworkPrefix: number;
namespaces: string[];
defaultDenyContainers: boolean;
cooldVersion: string;
corrosionVersion: string;
corrosionGossipPort: number;
corrosionApiPort: number;
builderEnabled: boolean;
builderCapacity: number;
builderCpuQuota: string;
builderMemoryMax: string;
builderTimeoutSecs: number;
lastCliAction: string | null;
lastCliStatus: string | null;
lastCliSummary: string | null;
lastCliRanAt: string | null;
serversCount: number;
servers: V5Server[];
};
@@ -38,9 +66,15 @@ export type V5Project = {
environments: V5Environment[];
};
export type V5PrivateKey = {
id: string;
name: string;
};
export type V5DashboardProps = {
flux: FluxStatus | null;
clusters?: V5Cluster[];
privateKeys?: V5PrivateKey[];
projects?: V5Project[];
selectedProjectUuid?: string | null;
selectedEnvironmentUuid?: string | null;