Merge branch 'canary' into canary

This commit is contained in:
Mohab Gabber
2025-01-03 19:11:33 +02:00
committed by GitHub
21 changed files with 523 additions and 316 deletions

View File

@@ -13,10 +13,12 @@ import { CardTitle } from "@/components/ui/card";
import { import {
InputOTP, InputOTP,
InputOTPGroup, InputOTPGroup,
InputOTPSeparator,
InputOTPSlot, InputOTPSlot,
} from "@/components/ui/input-otp"; } from "@/components/ui/input-otp";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect } from "react"; import { useEffect } from "react";
@@ -96,16 +98,22 @@ export const Login2FA = ({ authId }: Props) => {
<FormItem className="flex flex-col justify-center max-sm:items-center"> <FormItem className="flex flex-col justify-center max-sm:items-center">
<FormLabel>Pin</FormLabel> <FormLabel>Pin</FormLabel>
<FormControl> <FormControl>
<InputOTP maxLength={6} {...field}> <div className="flex justify-center">
<InputOTPGroup> <InputOTP
<InputOTPSlot index={0} /> maxLength={6}
<InputOTPSlot index={1} /> {...field}
<InputOTPSlot index={2} /> pattern={REGEXP_ONLY_DIGITS}
<InputOTPSlot index={3} /> >
<InputOTPSlot index={4} /> <InputOTPGroup>
<InputOTPSlot index={5} /> <InputOTPSlot index={0} className="border-border" />
</InputOTPGroup> <InputOTPSlot index={1} className="border-border" />
</InputOTP> <InputOTPSlot index={2} className="border-border" />
<InputOTPSlot index={3} className="border-border" />
<InputOTPSlot index={4} className="border-border" />
<InputOTPSlot index={5} className="border-border" />
</InputOTPGroup>
</InputOTP>
</div>
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Please enter the 6 digits code provided by your authenticator Please enter the 6 digits code provided by your authenticator

View File

@@ -81,7 +81,8 @@ export const AddCommand = ({ applicationId }: Props) => {
<div> <div>
<CardTitle className="text-xl">Run Command</CardTitle> <CardTitle className="text-xl">Run Command</CardTitle>
<CardDescription> <CardDescription>
Run a custom command in the container Run a custom command in the container after the application
initialized
</CardDescription> </CardDescription>
</div> </div>
</CardHeader> </CardHeader>

View File

@@ -55,13 +55,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
}); });
}; };
return ( return (
<Form {...form}> <Card className="bg-background px-6 pb-6">
<form <Form {...form}>
onSubmit={form.handleSubmit(onSubmit)} <form
className="flex w-full flex-col gap-5 " onSubmit={form.handleSubmit(onSubmit)}
> className="flex w-full flex-col gap-4"
<Card className="bg-background p-6"> >
<Secrets <Secrets
name="env" name="env"
title="Environment Settings" title="Environment Settings"
@@ -89,15 +89,13 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder="NPM_TOKEN=xyz" placeholder="NPM_TOKEN=xyz"
/> />
)} )}
<CardContent> <div className="flex flex-row justify-end">
<div className="flex flex-row justify-end"> <Button isLoading={isLoading} className="w-fit" type="submit">
<Button isLoading={isLoading} className="w-fit" type="submit"> Save
Save </Button>
</Button> </div>
</div> </form>
</CardContent> </Form>
</Card> </Card>
</form> );
</Form>
);
}; };

View File

@@ -94,10 +94,18 @@ export const ShowRequests = () => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RequestDistributionChart /> {isActive ? (
<RequestDistributionChart />
) : (
<div className="flex items-center justify-center min-h-[25vh]">
<span className="text-muted-foreground py-6">
You need to activate requests
</span>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
<RequestsTable /> {isActive && <RequestsTable />}
</> </>
); );
}; };

View File

@@ -18,6 +18,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -114,7 +115,15 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</DialogTitle> </DialogTitle>
<DialogDescription className="text-base w-full"> <DialogDescription className="text-base w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{t("settings.server.webServer.traefik.managePortsDescription")} <div className="flex flex-col gap-1">
{t(
"settings.server.webServer.traefik.managePortsDescription",
)}
<span className="text-sm text-muted-foreground">
{fields.length} port mapping{fields.length !== 1 ? "s" : ""}{" "}
configured
</span>
</div>
<Button <Button
onClick={handleAddPort} onClick={handleAddPort}
variant="default" variant="default"
@@ -141,109 +150,111 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</p> </p>
</div> </div>
) : ( ) : (
<div className="grid gap-4"> <ScrollArea className="h-[400px] pr-4">
{fields.map((field, index) => ( <div className="grid gap-4">
<Card key={field.id}> {fields.map((field, index) => (
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent"> <Card key={field.id}>
<FormField <CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
control={form.control} <FormField
name={`ports.${index}.targetPort`} control={form.control}
render={({ field }) => ( name={`ports.${index}.targetPort`}
<FormItem> render={({ field }) => (
<FormLabel className="text-sm font-medium text-muted-foreground"> <FormItem>
{t( <FormLabel className="text-sm font-medium text-muted-foreground">
"settings.server.webServer.traefik.targetPort", {t(
)} "settings.server.webServer.traefik.targetPort",
</FormLabel> )}
<FormControl> </FormLabel>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishedPort`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishedPort",
)}
</FormLabel>
<FormControl>
<Input
type="number"
{...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishMode`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishMode",
)}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl> <FormControl>
<SelectTrigger className="dark:bg-black"> <Input
<SelectValue /> type="number"
</SelectTrigger> {...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 8080"
/>
</FormControl> </FormControl>
<SelectContent> <FormMessage />
<SelectItem value="host"> </FormItem>
Host Mode )}
</SelectItem> />
<SelectItem value="ingress">
Ingress Mode
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end"> <FormField
<Button control={form.control}
onClick={() => remove(index)} name={`ports.${index}.publishedPort`}
variant="ghost" render={({ field }) => (
size="icon" <FormItem>
className="text-muted-foreground hover:text-destructive" <FormLabel className="text-sm font-medium text-muted-foreground">
> {t(
<Trash2 className="h-4 w-4" /> "settings.server.webServer.traefik.publishedPort",
</Button> )}
</div> </FormLabel>
</CardContent> <FormControl>
</Card> <Input
))} type="number"
</div> {...field}
onChange={(e) =>
field.onChange(Number(e.target.value))
}
className="w-full dark:bg-black"
placeholder="e.g. 80"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`ports.${index}.publishMode`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishMode",
)}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger className="dark:bg-black">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="host">
Host Mode
</SelectItem>
<SelectItem value="ingress">
Ingress Mode
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end">
<Button
onClick={() => remove(index)}
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
)} )}
{fields.length > 0 && ( {fields.length > 0 && (
@@ -281,7 +292,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
</AlertBlock> </AlertBlock>
)} )}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
type="submit" type="submit"

View File

@@ -22,16 +22,25 @@ import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates";
import { UpdateWebServer } from "./update-webserver"; import { UpdateWebServer } from "./update-webserver";
import type { IUpdateData } from "@dokploy/server/index";
export const UpdateServer = () => { interface Props {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false); updateData?: IUpdateData;
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); }
export const UpdateServer = ({ updateData }: Props) => {
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable,
);
const { mutateAsync: getUpdateData, isLoading } = const { mutateAsync: getUpdateData, isLoading } =
api.settings.getUpdateData.useMutation(); api.settings.getUpdateData.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery(); const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [latestVersion, setLatestVersion] = useState(""); const [latestVersion, setLatestVersion] = useState(
updateData?.latestVersion ?? "",
);
const handleCheckUpdates = async () => { const handleCheckUpdates = async () => {
try { try {
@@ -61,9 +70,24 @@ export const UpdateServer = () => {
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="secondary" className="gap-2"> <Button
<Sparkles className="h-4 w-4" /> variant={updateData ? "outline" : "secondary"}
Updates className="gap-2"
>
{updateData ? (
<>
<span className="flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
Update available
</>
) : (
<>
<Sparkles className="h-4 w-4" />
Updates
</>
)}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg p-6"> <DialogContent className="max-w-lg p-6">
@@ -99,10 +123,6 @@ export const UpdateServer = () => {
<div className="mb-8"> <div className="mb-8">
<div className="inline-flex items-center gap-2 rounded-lg px-3 py-2 border border-emerald-900 bg-emerald-900 dark:bg-emerald-900/40 mb-4 w-full"> <div className="inline-flex items-center gap-2 rounded-lg px-3 py-2 border border-emerald-900 bg-emerald-900 dark:bg-emerald-900/40 mb-4 w-full">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
</span>
<Download className="h-4 w-4 text-emerald-400" /> <Download className="h-4 w-4 text-emerald-400" />
<span className="text font-medium text-emerald-400 "> <span className="text font-medium text-emerald-400 ">
New version available: New version available:

View File

@@ -11,30 +11,50 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HardDriveDownload } from "lucide-react"; import { HardDriveDownload, Loader2 } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
interface Props { export const UpdateWebServer = () => {
isNavbar?: boolean; const [updating, setUpdating] = useState(false);
} const [open, setOpen] = useState(false);
export const UpdateWebServer = ({ isNavbar }: Props) => { const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
const { mutateAsync: updateServer, isLoading } =
api.settings.updateServer.useMutation();
const buttonLabel = isNavbar ? "Update available" : "Update Server"; const checkIsUpdateFinished = async () => {
const handleConfirm = async () => {
try { try {
await updateServer(); const response = await fetch("/api/health");
if (!response.ok) {
throw new Error("Health check failed");
}
toast.success( toast.success(
"The server has been updated. The page will be reloaded to reflect the changes...", "The server has been updated. The page will be reloaded to reflect the changes...",
); );
setTimeout(() => { setTimeout(() => {
// Allow seeing the toast before reloading // Allow seeing the toast before reloading
window.location.reload(); window.location.reload();
}, 2000); }, 2000);
} catch {
// Delay each request
await new Promise((resolve) => setTimeout(resolve, 2000));
// Keep running until it returns 200
void checkIsUpdateFinished();
}
};
const handleConfirm = async () => {
try {
setUpdating(true);
await updateServer();
// Give some time for docker service restart before starting to check status
await new Promise((resolve) => setTimeout(resolve, 8000));
await checkIsUpdateFinished();
} catch (error) { } catch (error) {
setUpdating(false);
console.error("Error updating server:", error); console.error("Error updating server:", error);
toast.error( toast.error(
"An error occurred while updating the server, please try again.", "An error occurred while updating the server, please try again.",
@@ -43,35 +63,54 @@ export const UpdateWebServer = ({ isNavbar }: Props) => {
}; };
return ( return (
<AlertDialog> <AlertDialog open={open}>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button <Button
className="relative w-full" className="relative w-full"
variant={isNavbar ? "outline" : "secondary"} variant="secondary"
isLoading={isLoading} onClick={() => setOpen(true)}
> >
{!isLoading && <HardDriveDownload className="h-4 w-4" />} <HardDriveDownload className="h-4 w-4" />
{!isLoading && ( <span className="absolute -right-1 -top-2 flex h-3 w-3">
<span className="absolute -right-1 -top-2 flex h-3 w-3"> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" /> <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" /> </span>
</span> Update Server
)}
{isLoading ? "Updating..." : buttonLabel}
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogTitle>
{updating
? "Server update in progress"
: "Are you absolutely sure?"}
</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will update the web server to the {updating ? (
new version. The page will be reloaded once the update is finished. <span className="flex items-center gap-1">
<Loader2 className="animate-spin" />
The server is being updated, please wait...
</span>
) : (
<>
This action cannot be undone. This will update the web server to
the new version. You will not be able to use the panel during
the update process. The page will be reloaded once the update is
finished.
</>
)}
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> {!updating && (
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogFooter>
<AlertDialogAction onClick={handleConfirm}>Confirm</AlertDialogAction> <AlertDialogCancel onClick={() => setOpen(false)}>
</AlertDialogFooter> Cancel
</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirm}>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
)}
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
); );

View File

@@ -63,7 +63,7 @@ export function NodeCard({ node, serverId }: Props) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-wrap items-center justify-between"> <div className="flex flex-wrap gap-y-2 items-center justify-between">
<div className="flex items-center space-x-4 p-2 rounded-xl border"> <div className="flex items-center space-x-4 p-2 rounded-xl border">
<div className={`h-2.5 w-2.5 rounded-full ${node.Status === "Ready" ? "bg-green-500" : "bg-red-500"}`} /> <div className={`h-2.5 w-2.5 rounded-full ${node.Status === "Ready" ? "bg-green-500" : "bg-red-500"}`} />
<div className="font-medium">{node.Hostname}</div> <div className="font-medium">{node.Hostname}</div>

View File

@@ -2,29 +2,23 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import { Activity, Loader2, Monitor, Settings, Server } from "lucide-react";
Activity,
Loader2,
Monitor,
Settings,
Server,
} from "lucide-react";
import { NodeCard } from "./details/details-card"; import { NodeCard } from "./details/details-card";
interface Props { interface Props {
serverId?: string; serverId?: string;
} }
export default function SwarmMonitorCard({ serverId }: Props) { export default function SwarmMonitorCard({ serverId }: Props) {
const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({ const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({
serverId, serverId,
}); });
if (isLoading) { if (isLoading) {
return ( return (
@@ -50,116 +44,132 @@ export default function SwarmMonitorCard({ serverId }: Props) {
); );
} }
const totalNodes = nodes.length; const totalNodes = nodes.length;
const activeNodesCount = nodes.filter((node) => node.Status === "Ready").length; const activeNodesCount = nodes.filter(
const managerNodesCount = nodes.filter((node) =>node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable").length; (node) => node.Status === "Ready",
const activeNodes = nodes.filter((node) => node.Status === "Ready"); ).length;
const managerNodes = nodes.filter((node) => node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable"); const managerNodesCount = nodes.filter(
(node) =>
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
).length;
const activeNodes = nodes.filter((node) => node.Status === "Ready");
const managerNodes = nodes.filter(
(node) =>
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
);
return ( return (
<div className="min-h-screen"> <div>
<div className="w-full max-w-7xl mx-auto space-y-6 py-4"> <div className="w-full max-w-7xl mx-auto space-y-6 py-4">
<header className="flex items-center justify-between"> <header className="flex items-center justify-between">
<div className="space-y-1"> <div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight">Docker Swarm Overview</h1> <h1 className="text-2xl font-semibold tracking-tight">
<p className="text-sm text-muted-foreground">Monitor and manage your Docker Swarm cluster</p> Docker Swarm Overview
</div> </h1>
{!serverId && ( <p className="text-sm text-muted-foreground">
<Button onClick={() => window.location.replace("/dashboard/settings/cluster")}> Monitor and manage your Docker Swarm cluster
<Settings className="mr-2 h-4 w-4" /> </p>
Manage Cluster </div>
</Button> {!serverId && (
)} <Button
</header> onClick={() =>
window.location.replace("/dashboard/settings/cluster")
}
>
<Settings className="mr-2 h-4 w-4" />
Manage Cluster
</Button>
)}
</header>
<div className="grid gap-6 md:grid-cols-3"> <div className="grid gap-6 md:grid-cols-3">
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Nodes</CardTitle> <CardTitle className="text-sm font-medium">Total Nodes</CardTitle>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md"> <div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" /> <Server className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold">{totalNodes}</div> <div className="text-2xl font-bold">{totalNodes}</div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <div className="flex items-center gap-2">
Active Nodes <CardTitle className="text-sm font-medium">
<Badge variant="green"> Active Nodes
Online </CardTitle>
</Badge> <Badge variant="green">Online</Badge>
</CardTitle> </div>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md"> <div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" /> <Activity className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{activeNodesCount} / {totalNodes} {activeNodesCount} / {totalNodes}
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{activeNodes.map((node) => ( {activeNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2"> <div key={node.ID} className="flex items-center gap-2">
{node.Hostname} {node.Hostname}
</div> </div>
))} ))}
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-background"> <Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> <div className="flex items-center gap-2">
Manager Nodes <CardTitle className="text-sm font-medium">
<Badge variant="green"> Manager Nodes
Online </CardTitle>
</Badge> <Badge variant="green">Online</Badge>
</CardTitle> </div>
<div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md"> <div className="p-2 bg-emerald-600/20 text-emerald-600 rounded-md">
<Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" /> <Monitor className="h-4 w-4 text-muted-foreground dark:text-emerald-600" />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<div className="text-2xl font-bold"> <div className="text-2xl font-bold">
{managerNodesCount} / {totalNodes} {managerNodesCount} / {totalNodes}
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<div className="max-h-48 overflow-y-auto"> <div className="max-h-48 overflow-y-auto">
{managerNodes.map((node) => ( {managerNodes.map((node) => (
<div key={node.ID} className="flex items-center gap-2"> <div key={node.ID} className="flex items-center gap-2">
{node.Hostname} {node.Hostname}
</div> </div>
))} ))}
</div> </div>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
{nodes.map((node) => ( {nodes.map((node) => (
<NodeCard key={node.ID} node={node} serverId={serverId} /> <NodeCard key={node.ID} node={node} serverId={serverId} />
))} ))}
</div> </div>
</div> </div>
</div> </div>
); );
} }

View File

@@ -13,15 +13,19 @@ import { HeartIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver";
import { Logo } from "../shared/logo"; import { Logo } from "../shared/logo";
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
import { buttonVariants } from "../ui/button"; import { buttonVariants } from "../ui/button";
import UpdateServer from "../dashboard/settings/web-server/update-server";
import type { IUpdateData } from "@dokploy/server/index";
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
export const Navbar = () => { export const Navbar = () => {
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false); const [updateData, setUpdateData] = useState<IUpdateData>({
latestVersion: null,
updateAvailable: false,
});
const router = useRouter(); const router = useRouter();
const { data } = api.auth.get.useQuery(); const { data } = api.auth.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -62,12 +66,12 @@ export const Navbar = () => {
return; return;
} }
const { updateAvailable } = await getUpdateData(); const fetchedUpdateData = await getUpdateData();
if (updateAvailable) { if (fetchedUpdateData?.updateAvailable) {
// Stop interval when update is available // Stop interval when update is available
clearUpdatesInterval(); clearUpdatesInterval();
setIsUpdateAvailable(true); setUpdateData(fetchedUpdateData);
} }
} catch (error) { } catch (error) {
console.error("Error auto-checking for updates:", error); console.error("Error auto-checking for updates:", error);
@@ -101,9 +105,9 @@ export const Navbar = () => {
</span> </span>
</Link> </Link>
</div> </div>
{isUpdateAvailable && ( {updateData.updateAvailable && (
<div> <div>
<UpdateWebServer isNavbar /> <UpdateServer updateData={updateData} />
</div> </div>
)} )}
<Link <Link

View File

@@ -0,0 +1,8 @@
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
return res.status(200).json({ ok: true });
}

View File

@@ -1,5 +1,12 @@
<svg width="6323" height="5778" viewBox="0 0 6323 5778" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="6323" height="5778" viewBox="0 0 6323 5778" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z" fill="white"/> <style>
<path d="M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z" fill="white"/> :root { color-scheme: light dark; }
<path d="M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z" fill="white"/> path { fill: black; }
@media (prefers-color-scheme: dark) {
path { fill: white; }
}
</style>
<path d="M4638.51 44.5295C4616.52 81.8286 4611.45 115.575 4619.9 213.263C4636.82 433.505 4772.12 710.584 4924.33 842.019C5002.12 909.512 5196.61 1012.53 5245.66 1012.53C5284.56 1012.53 5282.87 1019.63 5213.53 1129.75C5140.8 1243.43 5024.11 1339.34 4890.5 1389.07C4743.36 1445.91 4455.85 1453.01 4234.3 1405.06C4016.13 1357.1 3931.57 1323.35 3211.11 977.006C2265.71 522.312 2253.87 516.984 2125.34 481.461C2017.1 451.267 1917.32 445.938 1316.93 435.281C853.533 428.177 601.539 429.953 538.964 444.162C334.325 485.013 156.745 632.434 70.4925 829.586C12.9907 961.021 -7.30411 1191.92 2.84328 1589.78C7.91697 1841.99 16.3731 1911.26 46.8153 2005.39C114.465 2213.2 226.086 2342.86 422.269 2445.88C1594.29 3055.1 1969.74 3206.07 2529.54 3294.88C2732.49 3326.85 3258.46 3330.4 3459.72 3303.76C3755.69 3261.13 4107.46 3161.66 4403.43 3033.78C4540.42 2975.17 4904.03 2776.24 5220.29 2587.97C5910.31 2177.68 6006.71 2111.96 6037.16 2030.26C6070.98 1934.35 5988.11 1811.79 5888.33 1811.79C5851.12 1811.79 5862.96 1806.47 5426.62 2069.34C4352.69 2715.85 4026.28 2865.05 3485.09 2957.41C3162.06 3014.24 2587.04 2987.6 2274.17 2902.35C1924.08 2806.44 1839.52 2770.91 1051.41 2383.71C552.493 2140.38 444.255 2079.99 395.209 2023.16C363.076 1984.08 336.016 1945.01 336.016 1934.35C336.016 1920.14 467.932 1916.59 787.575 1921.92L1240.82 1929.02L1435.32 2001.84C1541.86 2040.92 1744.81 2126.17 1883.49 2190.11C2296.15 2381.94 2610.72 2451.21 3058.9 2451.21C3490.16 2451.21 3872.38 2374.83 4305.33 2198.99C4910.8 1955.66 5342.06 1596.88 5545.01 1172.38C5565.3 1127.98 5585.6 1090.68 5587.29 1087.13C5590.67 1083.57 5660.01 1074.69 5742.88 1065.81C5940.76 1046.28 6084.51 978.782 6221.5 842.019L6322.97 740.779V520.536V302.071L6253.63 353.579C6177.53 412.192 6062.52 444.162 5920.46 444.162C5795.31 444.162 5661.7 508.104 5568.69 614.672L5497.65 692.823L5487.51 646.643C5451.99 500.999 5304.85 364.236 5115.44 300.294C4956.46 248.786 4893.88 206.159 4831.31 108.471C4800.87 64.0671 4770.42 21.4395 4760.28 14.335C4721.38 -14.0833 4665.57 1.90186 4638.51 44.5295ZM2057.69 806.496C2162.55 834.914 2250.49 873.99 2517.7 1007.2C2605.65 1051.6 2796.76 1142.19 2940.51 1211.46C3084.27 1280.73 3332.88 1397.95 3490.16 1472.55C3948.49 1691.02 4049.96 1726.54 4301.95 1754.96L4437.25 1770.94L4310.41 1833.11C4153.12 1911.26 4016.13 1960.99 3804.73 2016.05C3512.15 2090.65 3402.22 2104.86 3050.44 2104.86C2590.43 2103.08 2370.57 2056.9 1974.82 1872.18C1413.33 1611.09 1386.27 1603.99 801.104 1589.78C457.784 1580.9 356.311 1572.01 336.016 1552.48C278.514 1492.09 303.882 1019.63 373.223 914.841C412.121 854.452 474.697 806.496 552.493 779.854C577.862 770.973 904.27 767.421 1278.03 772.749C1814.15 778.078 1978.2 785.182 2057.69 806.496Z"/>
<path d="M1266.2 1060.49C1173.18 1097.79 1129.21 1207.91 1171.49 1294.94C1222.22 1394.4 1332.15 1417.49 1413.33 1342.89C1477.6 1286.06 1479.29 1174.16 1418.41 1112C1374.44 1065.82 1308.48 1042.73 1266.2 1060.49Z"/>
<path d="M87.4063 2513.37C7.91846 2548.89 -8.99385 2616.39 4.536 2836.63C19.7571 3072.86 46.8168 3222.05 124.613 3488.48C427.344 4532.85 1129.2 5287.71 2106.74 5623.4C2641.17 5806.35 3236.48 5827.66 3752.3 5682.01C4596.23 5445.79 5315 4836.57 5692.15 4040.86C5886.64 3630.57 6018.55 3111.93 6018.55 2753.15C6018.55 2582.64 5991.49 2518.7 5910.31 2497.39C5820.68 2474.3 5575.45 2609.28 5164.48 2911.23C4484.61 3410.32 4229.23 3563.07 3890.98 3676.75C3635.61 3763.78 3466.49 3797.52 3194.2 3818.84C2651.31 3863.24 2057.69 3731.81 1570.62 3458.28C1394.73 3358.82 846.769 2980.5 581.246 2772.69C285.28 2540.01 270.059 2529.36 199.028 2508.04C155.056 2495.61 124.613 2497.39 87.4063 2513.37ZM5678.62 3076.41C5661.7 3138.57 5646.48 3202.52 5646.48 3218.5C5646.48 3236.26 5626.19 3262.9 5600.82 3280.67C5573.76 3296.65 5482.43 3371.25 5396.18 3445.85C5308.24 3518.67 5198.31 3611.03 5150.95 3650.1C5101.91 3689.18 4990.28 3781.54 4902.34 3856.14C4699.39 4026.65 4406.81 4236.23 4242.76 4330.37C4085.48 4420.95 3767.52 4532.85 3532.44 4582.58C2847.5 4724.67 2054.31 4570.15 1516.5 4190.05C1173.18 3946.72 412.123 3314.41 388.445 3254.02C363.077 3182.98 330.944 3042.66 337.708 3021.35C341.091 3012.47 417.196 3060.42 505.14 3129.69C1056.48 3559.52 1563.85 3863.24 1942.69 3992.9C2328.29 4124.34 2565.06 4163.41 2991.25 4163.41C3380.23 4163.41 3628.84 4126.11 3963.71 4012.44C4345.93 3884.56 4531.96 3781.54 5052.86 3405C5391.11 3161.66 5676.92 2968.06 5700.6 2966.29C5705.68 2966.29 5697.22 3016.02 5678.62 3076.41ZM5426.62 3881C5426.62 3886.33 5409.71 3925.41 5391.11 3966.26C5318.38 4115.45 5144.19 4364.11 5003.81 4518.64C4587.77 4973.33 4090.55 5271.73 3540.9 5392.5C3309.2 5444.01 2708.81 5440.46 2483.88 5387.17C1716.06 5204.23 1105.53 4754.87 696.249 4071.05C647.204 3987.57 609.997 3916.53 613.379 3912.97C616.762 3909.42 774.046 4028.42 965.155 4177.62C1154.57 4326.82 1371.05 4486.67 1443.77 4532.85C1974.82 4863.21 2463.59 4991.09 3118.09 4968C3461.41 4955.57 3691.42 4912.94 3997.53 4806.38C4357.76 4680.27 4623.29 4513.31 5130.66 4095.92C5382.65 3888.11 5426.62 3856.14 5426.62 3881Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -19,6 +19,14 @@
"settings.server.webServer.server.label": "Serwer", "settings.server.webServer.server.label": "Serwer",
"settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko", "settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko",
"settings.server.webServer.traefik.managePorts": "Dodatkowe mapowania portów",
"settings.server.webServer.traefik.managePortsDescription": "Dodaj lub usuń dodatkowe porty dla Traefik",
"settings.server.webServer.traefik.targetPort": "Port docelowy",
"settings.server.webServer.traefik.publishedPort": "Port opublikowany",
"settings.server.webServer.traefik.addPort": "Dodaj port",
"settings.server.webServer.traefik.portsUpdated": "Porty zaktualizowane pomyślnie",
"settings.server.webServer.traefik.portsUpdateError": "Nie udało się zaktualizować portów",
"settings.server.webServer.traefik.publishMode": "Tryb publikacji",
"settings.server.webServer.storage.label": "Przestrzeń", "settings.server.webServer.storage.label": "Przestrzeń",
"settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy", "settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy",
"settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny", "settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny",

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -359,7 +359,9 @@ export const settingsRouter = createTRPCRouter({
await pullLatestRelease(); await pullLatestRelease();
await spawnAsync("docker", [ // This causes restart of dokploy, thus it will not finish executing properly, so don't await it
// Status after restart is checked via frontend /api/health endpoint
void spawnAsync("docker", [
"service", "service",
"update", "update",
"--force", "--force",

View File

@@ -0,0 +1,47 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://${DOMAIN}/api/v1"
ports:
- 80
- 443
depends_on:
- server
networks:
- dokploy-network
server:
image: bluewaveuptime/uptime_server:latest
restart: always
ports:
- 5000
depends_on:
- redis
- mongodb
environment:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
- REDIS_HOST=redis
networks:
- dokploy-network
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: bluewaveuptime/uptime_redis:latest
restart: always
ports:
- 6379
volumes:
- ../files/redis/data:/data
networks:
- dokploy-network
mongodb:
image: bluewaveuptime/uptime_database_mongo:latest
restart: always
volumes:
- ../files/mongo/data:/data/db
command: ["mongod", "--quiet"]
ports:
- 27017
networks:
- dokploy-network

View File

@@ -0,0 +1,25 @@
import {
type DomainSchema,
type Schema,
type Template,
generateRandomDomain,
} from "../utils";
export function generate(schema: Schema): Template {
const mainDomain = generateRandomDomain(schema);
const envs = [`DOMAIN=${mainDomain}`];
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 80,
serviceName: "client",
},
];
return {
domains,
envs,
};
}

View File

@@ -11,7 +11,7 @@ export function generate(schema: Schema): Template {
const hulySecret = generateBase64(64); const hulySecret = generateBase64(64);
const domains: DomainSchema[] = [ const domains: DomainSchema[] = [
{ {
host: generateRandomDomain(schema), host: mainDomain,
port: 80, port: 80,
serviceName: "nginx", serviceName: "nginx",
}, },

View File

@@ -1180,5 +1180,20 @@ export const templates: TemplateData[] = [
}, },
tags: ["self-hosted", "project-management", "management"], tags: ["self-hosted", "project-management", "management"],
load: () => import("./glpi/index").then((m) => m.generate), load: () => import("./glpi/index").then((m) => m.generate),
},
{
id: "checkmate",
name: "Checkmate",
version: "2.0.1",
description:
"Checkmate is an open-source, self-hosted tool designed to track and monitor server hardware, uptime, response times, and incidents in real-time with beautiful visualizations.",
logo: "checkmate.png",
links: {
github: "https://github.com/bluewave-labs/checkmate",
website: "https://bluewavelabs.ca",
docs: "https://bluewavelabs.gitbook.io/checkmate",
},
tags: ["self-hosted", "monitoring", "uptime"],
load: () => import("./checkmate/index").then((m) => m.generate),
}, },
]; ];

View File

@@ -360,7 +360,7 @@ const installUtilities = () => `
;; ;;
ubuntu | debian | raspbian) ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git jq openssl >/dev/null DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
;; ;;
centos | fedora | rhel | ol | rocky | almalinux | amzn) centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then if [ "$OS_TYPE" = "amzn" ]; then

View File

@@ -35,7 +35,6 @@ export async function checkGPUStatus(serverId?: string): Promise<GPUInfo> {
...cudaInfo, ...cudaInfo,
}; };
} catch (error) { } catch (error) {
console.error("Error in checkGPUStatus:", error);
return { return {
driverInstalled: false, driverInstalled: false,
driverVersion: undefined, driverVersion: undefined,
@@ -317,7 +316,6 @@ const setupLocalServer = async (daemonConfig: any) => {
try { try {
await execAsync(setupCommands); await execAsync(setupCommands);
} catch (error) { } catch (error) {
console.error("Setup failed:", error);
throw new Error( throw new Error(
"Failed to configure GPU support. Please ensure you have sudo privileges and try again.", "Failed to configure GPU support. Please ensure you have sudo privileges and try again.",
); );
@@ -344,11 +342,10 @@ const verifySetup = async (nodeId: string, serverId?: string) => {
"cat /etc/nvidia-container-runtime/config.toml", "cat /etc/nvidia-container-runtime/config.toml",
].join(" && "); ].join(" && ");
const { stdout: diagnostics } = serverId await (serverId
? await execAsyncRemote(serverId, diagnosticCommands) ? execAsyncRemote(serverId, diagnosticCommands)
: await execAsync(diagnosticCommands); : execAsync(diagnosticCommands));
console.error("Diagnostic Information:", diagnostics);
throw new Error("GPU support not detected in swarm after setup"); throw new Error("GPU support not detected in swarm after setup");
} }