mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
1 Commits
1365-creat
...
fix/adjust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6de744f91 |
@@ -47,7 +47,7 @@ const baseAdmin: User = {
|
|||||||
letsEncryptEmail: null,
|
letsEncryptEmail: null,
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
logCleanupCron: null,
|
enableLogRotation: false,
|
||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
import {
|
||||||
type ChartConfig,
|
type ChartConfig,
|
||||||
ChartContainer,
|
ChartContainer,
|
||||||
ChartTooltip,
|
ChartTooltip,
|
||||||
ChartTooltipContent,
|
ChartTooltipContent,
|
||||||
} from "@/components/ui/chart";
|
} from "@/components/ui/chart";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -14,13 +14,6 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
export interface RequestDistributionChartProps {
|
|
||||||
dateRange?: {
|
|
||||||
from: Date | undefined;
|
|
||||||
to: Date | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
views: {
|
views: {
|
||||||
label: "Page Views",
|
label: "Page Views",
|
||||||
@@ -31,22 +24,10 @@ const chartConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
export const RequestDistributionChart = ({
|
export const RequestDistributionChart = () => {
|
||||||
dateRange,
|
const { data: stats } = api.settings.readStats.useQuery(undefined, {
|
||||||
}: RequestDistributionChartProps) => {
|
|
||||||
const { data: stats } = api.settings.readStats.useQuery(
|
|
||||||
{
|
|
||||||
dateRange: dateRange
|
|
||||||
? {
|
|
||||||
start: dateRange.from?.toISOString(),
|
|
||||||
end: dateRange.to?.toISOString(),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refetchInterval: 1333,
|
refetchInterval: 1333,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
|||||||
@@ -79,15 +79,7 @@ export const priorities = [
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
export const RequestsTable = () => {
|
||||||
export interface RequestsTableProps {
|
|
||||||
dateRange?: {
|
|
||||||
from: Date | undefined;
|
|
||||||
to: Date | undefined;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
const [statusFilter, setStatusFilter] = useState<string[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
const [selectedRow, setSelectedRow] = useState<LogEntry>();
|
||||||
@@ -106,12 +98,6 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
|||||||
page: pagination,
|
page: pagination,
|
||||||
search,
|
search,
|
||||||
status: statusFilter,
|
status: statusFilter,
|
||||||
dateRange: dateRange
|
|
||||||
? {
|
|
||||||
start: dateRange.from?.toISOString(),
|
|
||||||
end: dateRange.to?.toISOString(),
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchInterval: 1333,
|
refetchInterval: 1333,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -9,29 +8,9 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { format } from "date-fns";
|
import { ArrowDownUp } from "lucide-react";
|
||||||
import {
|
|
||||||
ArrowDownUp,
|
|
||||||
AlertCircle,
|
|
||||||
InfoIcon,
|
|
||||||
Calendar as CalendarIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
@@ -41,30 +20,17 @@ export type LogEntry = NonNullable<
|
|||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
export const ShowRequests = () => {
|
export const ShowRequests = () => {
|
||||||
|
const { data: isLogRotateActive, refetch: refetchLogRotate } =
|
||||||
|
api.settings.getLogRotateStatus.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: toggleLogRotate } =
|
||||||
|
api.settings.toggleLogRotate.useMutation();
|
||||||
|
|
||||||
const { data: isActive, refetch } =
|
const { data: isActive, refetch } =
|
||||||
api.settings.haveActivateRequests.useQuery();
|
api.settings.haveActivateRequests.useQuery();
|
||||||
const { mutateAsync: toggleRequests } =
|
const { mutateAsync: toggleRequests } =
|
||||||
api.settings.toggleRequests.useMutation();
|
api.settings.toggleRequests.useMutation();
|
||||||
|
|
||||||
const { data: logCleanupStatus } =
|
|
||||||
api.settings.getLogCleanupStatus.useQuery();
|
|
||||||
const { mutateAsync: updateLogCleanup } =
|
|
||||||
api.settings.updateLogCleanup.useMutation();
|
|
||||||
const [cronExpression, setCronExpression] = useState<string | null>(null);
|
|
||||||
const [dateRange, setDateRange] = useState<{
|
|
||||||
from: Date | undefined;
|
|
||||||
to: Date | undefined;
|
|
||||||
}>({
|
|
||||||
from: undefined,
|
|
||||||
to: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (logCleanupStatus) {
|
|
||||||
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
|
||||||
}
|
|
||||||
}, [logCleanupStatus]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -91,60 +57,7 @@ export const ShowRequests = () => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<div className="flex w-full gap-4 justify-end items-center">
|
<div className="flex w-full gap-4 justify-end">
|
||||||
<div className="flex-1 flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor="cron" className="min-w-32">
|
|
||||||
Log Cleanup Schedule
|
|
||||||
</Label>
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<InfoIcon className="size-4 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p className="max-w-80">
|
|
||||||
At the scheduled time, the cleanup job will keep
|
|
||||||
only the last 1000 entries in the access log file
|
|
||||||
and signal Traefik to reopen its log files. The
|
|
||||||
default schedule is daily at midnight (0 0 * * *).
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 flex gap-4">
|
|
||||||
<Input
|
|
||||||
id="cron"
|
|
||||||
placeholder="0 0 * * *"
|
|
||||||
value={cronExpression || ""}
|
|
||||||
onChange={(e) => setCronExpression(e.target.value)}
|
|
||||||
className="max-w-60"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={async () => {
|
|
||||||
if (!cronExpression?.trim()) {
|
|
||||||
toast.error("Please enter a valid cron expression");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await updateLogCleanup({
|
|
||||||
cronExpression: cronExpression,
|
|
||||||
});
|
|
||||||
toast.success("Log cleanup schedule updated");
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(
|
|
||||||
`Failed to update log cleanup schedule: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update Schedule
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
title={isActive ? "Deactivate Requests" : "Activate Requests"}
|
||||||
description="You will also need to restart Traefik to apply the changes"
|
description="You will also need to restart Traefik to apply the changes"
|
||||||
@@ -164,81 +77,53 @@ export const ShowRequests = () => {
|
|||||||
>
|
>
|
||||||
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
<Button>{isActive ? "Deactivate" : "Activate"}</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
</div>
|
|
||||||
|
|
||||||
{isActive ? (
|
<DialogAction
|
||||||
<>
|
title={
|
||||||
<div className="flex justify-end mb-4 gap-2">
|
isLogRotateActive
|
||||||
{(dateRange.from || dateRange.to) && (
|
? "Activate Log Rotate"
|
||||||
<Button
|
: "Deactivate Log Rotate"
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
setDateRange({ from: undefined, to: undefined })
|
|
||||||
}
|
}
|
||||||
className="px-3"
|
description={
|
||||||
>
|
isLogRotateActive
|
||||||
Clear dates
|
? "This will make the logs rotate on interval 1 day and maximum size of 100 MB and maximum 6 logs"
|
||||||
</Button>
|
: "The log rotation will be disabled"
|
||||||
)}
|
}
|
||||||
<Popover>
|
onClick={() => {
|
||||||
<PopoverTrigger asChild>
|
toggleLogRotate({
|
||||||
<Button
|
enable: !isLogRotateActive,
|
||||||
variant="outline"
|
})
|
||||||
className="w-[300px] justify-start text-left font-normal"
|
.then(() => {
|
||||||
>
|
toast.success(
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
`Log rotate ${isLogRotateActive ? "activated" : "deactivated"}`,
|
||||||
{dateRange.from ? (
|
);
|
||||||
dateRange.to ? (
|
refetchLogRotate();
|
||||||
<>
|
})
|
||||||
{format(dateRange.from, "LLL dd, y")} -{" "}
|
.catch((err) => {
|
||||||
{format(dateRange.to, "LLL dd, y")}
|
toast.error(err.message);
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
format(dateRange.from, "LLL dd, y")
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>Pick a date range</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="end">
|
|
||||||
<Calendar
|
|
||||||
initialFocus
|
|
||||||
mode="range"
|
|
||||||
defaultMonth={dateRange.from}
|
|
||||||
selected={{
|
|
||||||
from: dateRange.from,
|
|
||||||
to: dateRange.to,
|
|
||||||
}}
|
|
||||||
onSelect={(range) => {
|
|
||||||
setDateRange({
|
|
||||||
from: range?.from,
|
|
||||||
to: range?.to,
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
numberOfMonths={2}
|
>
|
||||||
/>
|
<Button variant="secondary">
|
||||||
</PopoverContent>
|
{isLogRotateActive
|
||||||
</Popover>
|
? "Activate Log Rotate"
|
||||||
|
: "Deactivate Log Rotate"}
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
<RequestDistributionChart dateRange={dateRange} />
|
|
||||||
<RequestsTable dateRange={dateRange} />
|
<div>
|
||||||
</>
|
{isActive ? (
|
||||||
|
<RequestDistributionChart />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-12 gap-4 text-muted-foreground">
|
<div className="flex items-center justify-center min-h-[25vh]">
|
||||||
<AlertCircle className="size-12 text-muted-foreground/50" />
|
<span className="text-muted-foreground py-6">
|
||||||
<div className="text-center space-y-2">
|
You need to activate requests
|
||||||
<h3 className="text-lg font-medium">
|
</span>
|
||||||
Requests are not activated
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm max-w-md">
|
|
||||||
Activate requests to see incoming traffic statistics and
|
|
||||||
monitor your application's usage. After activation, you'll
|
|
||||||
need to reload Traefik for the changes to take effect.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isActive && <RequestsTable />}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -64,12 +64,12 @@ export const Enable2FA = () => {
|
|||||||
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
setIsPasswordLoading(true);
|
setIsPasswordLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data: enableData } = await authClient.twoFactor.enable({
|
const { data: enableData, error } = await authClient.twoFactor.enable({
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!enableData) {
|
if (!enableData) {
|
||||||
throw new Error("No data received from server");
|
throw new Error(error?.message || "Unknown error");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableData.backupCodes) {
|
if (enableData.backupCodes) {
|
||||||
@@ -95,7 +95,7 @@ export const Enable2FA = () => {
|
|||||||
error instanceof Error ? error.message : "Error setting up 2FA",
|
error instanceof Error ? error.message : "Error setting up 2FA",
|
||||||
);
|
);
|
||||||
passwordForm.setError("password", {
|
passwordForm.setError("password", {
|
||||||
message: "Error verifying password",
|
message: `Error verifying password: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsPasswordLoading(false);
|
setIsPasswordLoading(false);
|
||||||
|
|||||||
@@ -5,12 +5,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { IUpdateData } from "@dokploy/server/index";
|
import type { IUpdateData } from "@dokploy/server/index";
|
||||||
import {
|
import {
|
||||||
@@ -30,17 +24,9 @@ import { UpdateWebServer } from "./update-webserver";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
updateData?: IUpdateData;
|
updateData?: IUpdateData;
|
||||||
children?: React.ReactNode;
|
|
||||||
isOpen?: boolean;
|
|
||||||
onOpenChange?: (open: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UpdateServer = ({
|
export const UpdateServer = ({ updateData }: Props) => {
|
||||||
updateData,
|
|
||||||
children,
|
|
||||||
isOpen: isOpenProp,
|
|
||||||
onOpenChange: onOpenChangeProp,
|
|
||||||
}: Props) => {
|
|
||||||
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
const [hasCheckedUpdate, setHasCheckedUpdate] = useState(!!updateData);
|
||||||
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
|
||||||
!!updateData?.updateAvailable,
|
!!updateData?.updateAvailable,
|
||||||
@@ -49,10 +35,10 @@ export const UpdateServer = ({
|
|||||||
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 [latestVersion, setLatestVersion] = useState(
|
const [latestVersion, setLatestVersion] = useState(
|
||||||
updateData?.latestVersion ?? "",
|
updateData?.latestVersion ?? "",
|
||||||
);
|
);
|
||||||
const [isOpenInternal, setIsOpenInternal] = useState(false);
|
|
||||||
|
|
||||||
const handleCheckUpdates = async () => {
|
const handleCheckUpdates = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -79,52 +65,28 @@ export const UpdateServer = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isOpen = isOpenInternal || isOpenProp;
|
|
||||||
const onOpenChange = (open: boolean) => {
|
|
||||||
setIsOpenInternal(open);
|
|
||||||
onOpenChangeProp?.(open);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
{children ? (
|
|
||||||
children
|
|
||||||
) : (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
<Button
|
||||||
variant={updateData ? "outline" : "secondary"}
|
variant={updateData ? "outline" : "secondary"}
|
||||||
size="sm"
|
className="gap-2"
|
||||||
onClick={() => onOpenChange?.(true)}
|
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4 flex-shrink-0" />
|
|
||||||
{updateData ? (
|
{updateData ? (
|
||||||
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
<>
|
||||||
Update Available
|
<span className="flex h-2 w-2">
|
||||||
</span>
|
<span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-emerald-400 opacity-75" />
|
||||||
) : (
|
|
||||||
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
|
||||||
Check for updates
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{updateData && (
|
|
||||||
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
||||||
</span>
|
</span>
|
||||||
|
Update available
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
Updates
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
|
||||||
{updateData && (
|
|
||||||
<TooltipContent side="right" sideOffset={10}>
|
|
||||||
<p>Update Available</p>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-lg p-6">
|
<DialogContent className="max-w-lg p-6">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
@@ -255,7 +217,7 @@ export const UpdateServer = ({
|
|||||||
|
|
||||||
<div className="space-y-4 flex items-center justify-end">
|
<div className="space-y-4 flex items-center justify-end">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
|
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
{isUpdateAvailable ? (
|
{isUpdateAvailable ? (
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@@ -1015,16 +1017,18 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
|
{!isCloud && auth?.role === "owner" && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<UpdateServerButton />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu className="flex flex-col gap-2">
|
<SidebarMenu>
|
||||||
{!isCloud && auth?.role === "owner" && (
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<UpdateServerButton />
|
|
||||||
</SidebarMenuItem>
|
|
||||||
)}
|
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import type { IUpdateData } from "@dokploy/server/index";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
import UpdateServer from "../dashboard/settings/web-server/update-server";
|
||||||
import { Button } from "../ui/button";
|
|
||||||
import { Download } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "../ui/tooltip";
|
|
||||||
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
||||||
|
|
||||||
export const UpdateServerButton = () => {
|
export const UpdateServerButton = () => {
|
||||||
@@ -22,7 +15,6 @@ export const UpdateServerButton = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: getUpdateData } =
|
const { mutateAsync: getUpdateData } =
|
||||||
api.settings.getUpdateData.useMutation();
|
api.settings.getUpdateData.useMutation();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
const checkUpdatesIntervalRef = useRef<null | NodeJS.Timeout>(null);
|
||||||
|
|
||||||
@@ -77,47 +69,11 @@ export const UpdateServerButton = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return updateData.updateAvailable ? (
|
return (
|
||||||
<div className="border-t pt-4">
|
updateData.updateAvailable && (
|
||||||
<UpdateServer
|
<div>
|
||||||
updateData={updateData}
|
<UpdateServer updateData={updateData} />
|
||||||
isOpen={isOpen}
|
|
||||||
onOpenChange={setIsOpen}
|
|
||||||
>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant={updateData ? "outline" : "secondary"}
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => setIsOpen(true)}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 flex-shrink-0" />
|
|
||||||
{updateData ? (
|
|
||||||
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
|
||||||
Update Available
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="font-medium truncate group-data-[collapsible=icon]:hidden">
|
|
||||||
Check for updates
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{updateData && (
|
|
||||||
<span className="absolute right-2 flex h-2 w-2 group-data-[collapsible=icon]:hidden">
|
|
||||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
|
|
||||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{updateData && (
|
|
||||||
<TooltipContent side="right" sideOffset={10}>
|
|
||||||
<p>Update Available</p>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</UpdateServer>
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
import type * as React from "react";
|
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import { DayPicker } from "react-day-picker";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
|
||||||
|
|
||||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
|
||||||
|
|
||||||
function Calendar({
|
|
||||||
className,
|
|
||||||
classNames,
|
|
||||||
showOutsideDays = true,
|
|
||||||
...props
|
|
||||||
}: CalendarProps) {
|
|
||||||
return (
|
|
||||||
<DayPicker
|
|
||||||
showOutsideDays={showOutsideDays}
|
|
||||||
className={cn("p-3", className)}
|
|
||||||
classNames={{
|
|
||||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
|
||||||
month: "space-y-4",
|
|
||||||
caption: "flex justify-center pt-1 relative items-center",
|
|
||||||
caption_label: "text-sm font-medium",
|
|
||||||
nav: "space-x-1 flex items-center",
|
|
||||||
nav_button: cn(
|
|
||||||
buttonVariants({ variant: "outline" }),
|
|
||||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
||||||
),
|
|
||||||
nav_button_previous: "absolute left-1",
|
|
||||||
nav_button_next: "absolute right-1",
|
|
||||||
table: "w-full border-collapse space-y-1",
|
|
||||||
head_row: "flex",
|
|
||||||
head_cell:
|
|
||||||
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
|
|
||||||
row: "flex w-full mt-2",
|
|
||||||
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
|
|
||||||
day: cn(
|
|
||||||
buttonVariants({ variant: "ghost" }),
|
|
||||||
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
|
|
||||||
),
|
|
||||||
day_range_end: "day-range-end",
|
|
||||||
day_selected:
|
|
||||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
||||||
day_today: "bg-accent text-accent-foreground",
|
|
||||||
day_outside:
|
|
||||||
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
|
|
||||||
day_disabled: "text-muted-foreground opacity-50",
|
|
||||||
day_range_middle:
|
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
||||||
day_hidden: "invisible",
|
|
||||||
...classNames,
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
IconLeft: ({ className, ...props }) => (
|
|
||||||
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
|
|
||||||
),
|
|
||||||
IconRight: ({ className, ...props }) => (
|
|
||||||
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Calendar.displayName = "Calendar";
|
|
||||||
|
|
||||||
export { Calendar };
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "logCleanupCron" text;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -498,13 +498,6 @@
|
|||||||
"when": 1741322697251,
|
"when": 1741322697251,
|
||||||
"tag": "0070_useful_serpent_society",
|
"tag": "0070_useful_serpent_society",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 71,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1741460060541,
|
|
||||||
"tag": "0071_flaky_black_queen",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,13 @@
|
|||||||
import { db } from "@/server/db";
|
import { apiFindAllByApplication } from "@/server/db/schema";
|
||||||
import { apiFindAllByApplication, applications } from "@/server/db/schema";
|
|
||||||
import {
|
import {
|
||||||
createPreviewDeployment,
|
|
||||||
findApplicationById,
|
findApplicationById,
|
||||||
findPreviewDeploymentByApplicationId,
|
|
||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findPreviewDeploymentsByApplicationId,
|
findPreviewDeploymentsByApplicationId,
|
||||||
findPreviewDeploymentsByPullRequestId,
|
|
||||||
IS_CLOUD,
|
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { and } from "drizzle-orm";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { deploy } from "@/server/utils/deploy";
|
|
||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
|
||||||
|
|
||||||
export const previewDeploymentRouter = createTRPCRouter({
|
export const previewDeploymentRouter = createTRPCRouter({
|
||||||
all: protectedProcedure
|
all: protectedProcedure
|
||||||
@@ -69,142 +59,4 @@ export const previewDeploymentRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return previewDeployment;
|
return previewDeployment;
|
||||||
}),
|
}),
|
||||||
create: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
action: z.enum(["opened", "synchronize", "reopened", "closed"]),
|
|
||||||
pullRequestId: z.string(),
|
|
||||||
repository: z.string(),
|
|
||||||
owner: z.string(),
|
|
||||||
branch: z.string(),
|
|
||||||
deploymentHash: z.string(),
|
|
||||||
prBranch: z.string(),
|
|
||||||
prNumber: z.any(),
|
|
||||||
prTitle: z.string(),
|
|
||||||
prURL: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const organizationId = ctx.session.activeOrganizationId;
|
|
||||||
const action = input.action;
|
|
||||||
const prId = input.pullRequestId;
|
|
||||||
|
|
||||||
if (action === "closed") {
|
|
||||||
const previewDeploymentResult =
|
|
||||||
await findPreviewDeploymentsByPullRequestId(prId);
|
|
||||||
|
|
||||||
const filteredPreviewDeploymentResult = previewDeploymentResult.filter(
|
|
||||||
(previewDeployment) =>
|
|
||||||
previewDeployment.application.project.organizationId ===
|
|
||||||
organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filteredPreviewDeploymentResult.length > 0) {
|
|
||||||
for (const previewDeployment of filteredPreviewDeploymentResult) {
|
|
||||||
try {
|
|
||||||
await removePreviewDeployment(
|
|
||||||
previewDeployment.previewDeploymentId,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: "Preview Deployments Closed",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
action === "opened" ||
|
|
||||||
action === "synchronize" ||
|
|
||||||
action === "reopened"
|
|
||||||
) {
|
|
||||||
const deploymentHash = input.deploymentHash;
|
|
||||||
|
|
||||||
const prBranch = input.prBranch;
|
|
||||||
const prNumber = input.prNumber;
|
|
||||||
const prTitle = input.prTitle;
|
|
||||||
const prURL = input.prURL;
|
|
||||||
const apps = await db.query.applications.findMany({
|
|
||||||
where: and(
|
|
||||||
eq(applications.sourceType, "github"),
|
|
||||||
eq(applications.repository, input.repository),
|
|
||||||
eq(applications.branch, input.branch),
|
|
||||||
eq(applications.isPreviewDeploymentsActive, true),
|
|
||||||
eq(applications.owner, input.owner),
|
|
||||||
),
|
|
||||||
with: {
|
|
||||||
previewDeployments: true,
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredApps = apps.filter(
|
|
||||||
(app) => app.project.organizationId === organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(filteredApps);
|
|
||||||
|
|
||||||
for (const app of filteredApps) {
|
|
||||||
const previewLimit = app?.previewLimit || 0;
|
|
||||||
if (app?.previewDeployments?.length > previewLimit) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const previewDeploymentResult =
|
|
||||||
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
|
|
||||||
|
|
||||||
let previewDeploymentId =
|
|
||||||
previewDeploymentResult?.previewDeploymentId || "";
|
|
||||||
|
|
||||||
if (!previewDeploymentResult) {
|
|
||||||
try {
|
|
||||||
const previewDeployment = await createPreviewDeployment({
|
|
||||||
applicationId: app.applicationId as string,
|
|
||||||
branch: prBranch,
|
|
||||||
pullRequestId: prId,
|
|
||||||
pullRequestNumber: prNumber,
|
|
||||||
pullRequestTitle: prTitle,
|
|
||||||
pullRequestURL: prURL,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(previewDeployment);
|
|
||||||
previewDeploymentId = previewDeployment.previewDeploymentId;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobData: DeploymentJob = {
|
|
||||||
applicationId: app.applicationId as string,
|
|
||||||
titleLog: "Preview Deployment",
|
|
||||||
descriptionLog: `Hash: ${deploymentHash}`,
|
|
||||||
type: "deploy",
|
|
||||||
applicationType: "application-preview",
|
|
||||||
server: !!app.serverId,
|
|
||||||
previewDeploymentId,
|
|
||||||
isExternal: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (IS_CLOUD && app.serverId) {
|
|
||||||
jobData.serverId = app.serverId;
|
|
||||||
await deploy(jobData);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await myQueue.add(
|
|
||||||
"deployments",
|
|
||||||
{ ...jobData },
|
|
||||||
{
|
|
||||||
removeOnComplete: true,
|
|
||||||
removeOnFail: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: "Preview Deployments Created",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
|
logRotationManager,
|
||||||
parseRawConfig,
|
parseRawConfig,
|
||||||
paths,
|
paths,
|
||||||
prepareEnvironmentVariables,
|
prepareEnvironmentVariables,
|
||||||
@@ -52,9 +53,6 @@ import {
|
|||||||
writeConfig,
|
writeConfig,
|
||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
startLogCleanup,
|
|
||||||
stopLogCleanup,
|
|
||||||
getLogCleanupStatus,
|
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
import { checkGPUStatus, setupGPUSupport } from "@dokploy/server";
|
||||||
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
import { generateOpenApiDocument } from "@dokploy/trpc-openapi";
|
||||||
@@ -579,36 +577,49 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const rawConfig = readMonitoringConfig(
|
const rawConfig = readMonitoringConfig();
|
||||||
!!input.dateRange?.start && !!input.dateRange?.end,
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedConfig = parseRawConfig(
|
const parsedConfig = parseRawConfig(
|
||||||
rawConfig as string,
|
rawConfig as string,
|
||||||
input.page,
|
input.page,
|
||||||
input.sort,
|
input.sort,
|
||||||
input.search,
|
input.search,
|
||||||
input.status,
|
input.status,
|
||||||
input.dateRange,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedConfig;
|
return parsedConfig;
|
||||||
}),
|
}),
|
||||||
readStats: adminProcedure
|
readStats: adminProcedure.query(() => {
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
start: z.string().optional(),
|
|
||||||
end: z.string().optional(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(({ input }) => {
|
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const rawConfig = readMonitoringConfig(!!input?.start || !!input?.end);
|
const rawConfig = readMonitoringConfig();
|
||||||
const processedLogs = processLogs(rawConfig as string, input);
|
const processedLogs = processLogs(rawConfig as string);
|
||||||
return processedLogs || [];
|
return processedLogs || [];
|
||||||
}),
|
}),
|
||||||
|
getLogRotateStatus: adminProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return await logRotationManager.getStatus();
|
||||||
|
}),
|
||||||
|
toggleLogRotate: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
enable: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (input.enable) {
|
||||||
|
await logRotationManager.activate();
|
||||||
|
} else {
|
||||||
|
await logRotationManager.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
haveActivateRequests: adminProcedure.query(async () => {
|
haveActivateRequests: adminProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
@@ -809,20 +820,10 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateLogCleanup: adminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
cronExpression: z.string().nullable(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
if (input.cronExpression) {
|
|
||||||
return startLogCleanup(input.cronExpression);
|
|
||||||
}
|
|
||||||
return stopLogCleanup();
|
|
||||||
}),
|
|
||||||
|
|
||||||
getLogCleanupStatus: adminProcedure.query(async () => {
|
|
||||||
return getLogCleanupStatus();
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
// {
|
||||||
|
// "Parallelism": 1,
|
||||||
|
// "Delay": 10000000000,
|
||||||
|
// "FailureAction": "rollback",
|
||||||
|
// "Order": "start-first"
|
||||||
|
// }
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
isExternal: job.data.isExternal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +107,6 @@ export const deploymentWorker = new Worker(
|
|||||||
titleLog: job.data.titleLog,
|
titleLog: job.data.titleLog,
|
||||||
descriptionLog: job.data.descriptionLog,
|
descriptionLog: job.data.descriptionLog,
|
||||||
previewDeploymentId: job.data.previewDeploymentId,
|
previewDeploymentId: job.data.previewDeploymentId,
|
||||||
isExternal: job.data.isExternal,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type DeployJob =
|
|||||||
applicationType: "application-preview";
|
applicationType: "application-preview";
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
isExternal?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
export type DeploymentJob = DeployJob;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||||
logCleanupCron: text("logCleanupCron"),
|
enableLogRotation: boolean("enableLogRotation").notNull().default(false),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
metricsConfig: jsonb("metricsConfig")
|
||||||
@@ -250,12 +250,6 @@ export const apiReadStatsLogs = z.object({
|
|||||||
status: z.string().array().optional(),
|
status: z.string().array().optional(),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
|
sort: z.object({ id: z.string(), desc: z.boolean() }).optional(),
|
||||||
dateRange: z
|
|
||||||
.object({
|
|
||||||
start: z.string().optional(),
|
|
||||||
end: z.string().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateWebServerMonitoring = z.object({
|
export const apiUpdateWebServerMonitoring = z.object({
|
||||||
@@ -311,5 +305,4 @@ export const apiUpdateUser = createSchema.partial().extend({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
logCleanupCron: z.string().optional().nullable(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,9 +116,3 @@ export * from "./db/validations/index";
|
|||||||
export * from "./utils/gpu-setup";
|
export * from "./utils/gpu-setup";
|
||||||
|
|
||||||
export * from "./lib/auth";
|
export * from "./lib/auth";
|
||||||
|
|
||||||
export {
|
|
||||||
startLogCleanup,
|
|
||||||
stopLogCleanup,
|
|
||||||
getLogCleanupStatus,
|
|
||||||
} from "./utils/access-log/handler";
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const { handler, api } = betterAuth({
|
|||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema: schema,
|
schema: schema,
|
||||||
}),
|
}),
|
||||||
|
...(!IS_CLOUD && {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
}),
|
||||||
logger: {
|
logger: {
|
||||||
disabled: process.env.NODE_ENV === "production",
|
disabled: process.env.NODE_ENV === "production",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -387,13 +387,11 @@ export const deployPreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
isExternal = false,
|
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
isExternal?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -419,7 +417,6 @@ export const deployPreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (!isExternal) {
|
|
||||||
const commentExists = await issueCommentExists({
|
const commentExists = await issueCommentExists({
|
||||||
...issueParams,
|
...issueParams,
|
||||||
});
|
});
|
||||||
@@ -450,12 +447,16 @@ export const deployPreviewApplication = async ({
|
|||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
|
|
||||||
|
// const admin = await findUserById(application.project.userId);
|
||||||
|
|
||||||
|
// if (admin.cleanupCacheOnPreviews) {
|
||||||
|
// await cleanupFullDocker(application?.serverId);
|
||||||
|
// }
|
||||||
|
|
||||||
if (application.sourceType === "github") {
|
if (application.sourceType === "github") {
|
||||||
await cloneGithubRepository({
|
await cloneGithubRepository({
|
||||||
...application,
|
...application,
|
||||||
@@ -465,8 +466,6 @@ export const deployPreviewApplication = async ({
|
|||||||
});
|
});
|
||||||
await buildApplication(application, deployment.logPath);
|
await buildApplication(application, deployment.logPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isExternal) {
|
|
||||||
const successComment = getIssueComment(
|
const successComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
"success",
|
"success",
|
||||||
@@ -476,20 +475,16 @@ export const deployPreviewApplication = async ({
|
|||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isExternal) {
|
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
await updateIssueComment({
|
await updateIssueComment({
|
||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
@@ -505,13 +500,11 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
titleLog = "Preview Deployment",
|
titleLog = "Preview Deployment",
|
||||||
descriptionLog = "",
|
descriptionLog = "",
|
||||||
previewDeploymentId,
|
previewDeploymentId,
|
||||||
isExternal = false,
|
|
||||||
}: {
|
}: {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
titleLog: string;
|
titleLog: string;
|
||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
previewDeploymentId: string;
|
previewDeploymentId: string;
|
||||||
isExternal?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
const application = await findApplicationById(applicationId);
|
const application = await findApplicationById(applicationId);
|
||||||
|
|
||||||
@@ -537,7 +530,6 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
githubId: application?.githubId || "",
|
githubId: application?.githubId || "",
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
if (!isExternal) {
|
|
||||||
const commentExists = await issueCommentExists({
|
const commentExists = await issueCommentExists({
|
||||||
...issueParams,
|
...issueParams,
|
||||||
});
|
});
|
||||||
@@ -568,8 +560,6 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
application.appName = previewDeployment.appName;
|
application.appName = previewDeployment.appName;
|
||||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain}`;
|
||||||
application.buildArgs = application.previewBuildArgs;
|
application.buildArgs = application.previewBuildArgs;
|
||||||
@@ -596,7 +586,6 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
await mechanizeDockerContainer(application);
|
await mechanizeDockerContainer(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isExternal) {
|
|
||||||
const successComment = getIssueComment(
|
const successComment = getIssueComment(
|
||||||
application.name,
|
application.name,
|
||||||
"success",
|
"success",
|
||||||
@@ -606,19 +595,16 @@ export const deployRemotePreviewApplication = async ({
|
|||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
body: `### Dokploy Preview Deployment\n\n${successComment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "done",
|
previewStatus: "done",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isExternal) {
|
|
||||||
const comment = getIssueComment(application.name, "error", previewDomain);
|
const comment = getIssueComment(application.name, "error", previewDomain);
|
||||||
await updateIssueComment({
|
await updateIssueComment({
|
||||||
...issueParams,
|
...issueParams,
|
||||||
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
body: `### Dokploy Preview Deployment\n\n${comment}`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
await updatePreviewDeployment(previewDeploymentId, {
|
await updatePreviewDeployment(previewDeploymentId, {
|
||||||
previewStatus: "error",
|
previewStatus: "error",
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ export const findPreviewDeploymentsByApplicationId = async (
|
|||||||
|
|
||||||
export const createPreviewDeployment = async (
|
export const createPreviewDeployment = async (
|
||||||
schema: typeof apiCreatePreviewDeployment._type,
|
schema: typeof apiCreatePreviewDeployment._type,
|
||||||
isExternal = false,
|
|
||||||
) => {
|
) => {
|
||||||
const application = await findApplicationById(schema.applicationId);
|
const application = await findApplicationById(schema.applicationId);
|
||||||
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
const appName = `preview-${application.appName}-${generatePassword(6)}`;
|
||||||
@@ -166,8 +165,6 @@ export const createPreviewDeployment = async (
|
|||||||
org?.ownerId || "",
|
org?.ownerId || "",
|
||||||
);
|
);
|
||||||
|
|
||||||
let issueId = "";
|
|
||||||
if (!isExternal) {
|
|
||||||
const octokit = authGithub(application?.github as Github);
|
const octokit = authGithub(application?.github as Github);
|
||||||
|
|
||||||
const runningComment = getIssueComment(
|
const runningComment = getIssueComment(
|
||||||
@@ -183,15 +180,12 @@ export const createPreviewDeployment = async (
|
|||||||
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
body: `### Dokploy Preview Deployment\n\n${runningComment}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
issueId = `${issue.data.id}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previewDeployment = await db
|
const previewDeployment = await db
|
||||||
.insert(previewDeployments)
|
.insert(previewDeployments)
|
||||||
.values({
|
.values({
|
||||||
...schema,
|
...schema,
|
||||||
appName: appName,
|
appName: appName,
|
||||||
pullRequestCommentId: issueId,
|
pullRequestCommentId: `${issue.data.id}`,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -237,13 +231,6 @@ export const findPreviewDeploymentsByPullRequestId = async (
|
|||||||
) => {
|
) => {
|
||||||
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
const previewDeploymentResult = await db.query.previewDeployments.findMany({
|
||||||
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
where: eq(previewDeployments.pullRequestId, pullRequestId),
|
||||||
with: {
|
|
||||||
application: {
|
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return previewDeploymentResult;
|
return previewDeploymentResult;
|
||||||
|
|||||||
@@ -1,77 +1,121 @@
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||||
|
import { type RotatingFileStream, createStream } from "rotating-file-stream";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
import { findAdmin } from "@dokploy/server/services/admin";
|
import { findAdmin } from "@dokploy/server/services/admin";
|
||||||
import { updateUser } from "@dokploy/server/services/user";
|
import { updateUser } from "@dokploy/server/services/user";
|
||||||
import { scheduleJob, scheduledJobs } from "node-schedule";
|
|
||||||
|
|
||||||
const LOG_CLEANUP_JOB_NAME = "access-log-cleanup";
|
class LogRotationManager {
|
||||||
|
private static instance: LogRotationManager;
|
||||||
|
private stream: RotatingFileStream | null = null;
|
||||||
|
|
||||||
export const startLogCleanup = async (
|
private constructor() {
|
||||||
cronExpression = "0 0 * * *",
|
if (IS_CLOUD) {
|
||||||
): Promise<boolean> => {
|
return;
|
||||||
try {
|
}
|
||||||
|
this.initialize().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): LogRotationManager {
|
||||||
|
if (!LogRotationManager.instance) {
|
||||||
|
LogRotationManager.instance = new LogRotationManager();
|
||||||
|
}
|
||||||
|
return LogRotationManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
const isActive = await this.getStateFromDB();
|
||||||
|
if (isActive) {
|
||||||
|
await this.activateStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStateFromDB(): Promise<boolean> {
|
||||||
|
const admin = await findAdmin();
|
||||||
|
return admin?.user.enableLogRotation ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setStateInDB(active: boolean): Promise<void> {
|
||||||
|
const admin = await findAdmin();
|
||||||
|
if (!admin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateUser(admin.user.id, {
|
||||||
|
enableLogRotation: active,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async activateStream(): Promise<void> {
|
||||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||||
|
if (this.stream) {
|
||||||
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
await this.deactivateStream();
|
||||||
if (existingJob) {
|
|
||||||
existingJob.cancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduleJob(LOG_CLEANUP_JOB_NAME, cronExpression, async () => {
|
this.stream = createStream("access.log", {
|
||||||
|
size: "100M",
|
||||||
|
interval: "1d",
|
||||||
|
path: DYNAMIC_TRAEFIK_PATH,
|
||||||
|
rotate: 6,
|
||||||
|
compress: "gzip",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.stream.on("rotation", this.handleRotation.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deactivateStream(): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (this.stream) {
|
||||||
|
this.stream.end(() => {
|
||||||
|
this.stream = null;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async activate(): Promise<boolean> {
|
||||||
|
const currentState = await this.getStateFromDB();
|
||||||
|
if (currentState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setStateInDB(true);
|
||||||
|
await this.activateStream();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deactivate(): Promise<boolean> {
|
||||||
|
console.log("Deactivating log rotation...");
|
||||||
|
const currentState = await this.getStateFromDB();
|
||||||
|
if (!currentState) {
|
||||||
|
console.log("Log rotation is already inactive in DB");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.setStateInDB(false);
|
||||||
|
await this.deactivateStream();
|
||||||
|
console.log("Log rotation deactivated successfully");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleRotation() {
|
||||||
try {
|
try {
|
||||||
|
const status = await this.getStatus();
|
||||||
|
if (!status) {
|
||||||
|
await this.deactivateStream();
|
||||||
|
}
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`tail -n 1000 ${DYNAMIC_TRAEFIK_PATH}/access.log > ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp && mv ${DYNAMIC_TRAEFIK_PATH}/access.log.tmp ${DYNAMIC_TRAEFIK_PATH}/access.log`,
|
"docker kill -s USR1 $(docker ps -q --filter name=dokploy-traefik)",
|
||||||
);
|
);
|
||||||
|
console.log("USR1 Signal send to Traefik");
|
||||||
await execAsync("docker exec dokploy-traefik kill -USR1 1");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error during log cleanup:", error);
|
console.error("Error sending USR1 Signal to Traefik:", error);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const admin = await findAdmin();
|
|
||||||
if (admin) {
|
|
||||||
await updateUser(admin.user.id, {
|
|
||||||
logCleanupCron: cronExpression,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
public async getStatus(): Promise<boolean> {
|
||||||
return true;
|
const dbState = await this.getStateFromDB();
|
||||||
} catch (_) {
|
return dbState;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const stopLogCleanup = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const existingJob = scheduledJobs[LOG_CLEANUP_JOB_NAME];
|
|
||||||
if (existingJob) {
|
|
||||||
existingJob.cancel();
|
|
||||||
}
|
}
|
||||||
|
export const logRotationManager = LogRotationManager.getInstance();
|
||||||
// Update database
|
|
||||||
const admin = await findAdmin();
|
|
||||||
if (admin) {
|
|
||||||
await updateUser(admin.user.id, {
|
|
||||||
logCleanupCron: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error stopping log cleanup:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getLogCleanupStatus = async (): Promise<{
|
|
||||||
enabled: boolean;
|
|
||||||
cronExpression: string | null;
|
|
||||||
}> => {
|
|
||||||
const admin = await findAdmin();
|
|
||||||
const cronExpression = admin?.user.logCleanupCron ?? null;
|
|
||||||
return {
|
|
||||||
enabled: cronExpression !== null,
|
|
||||||
cronExpression,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -6,21 +6,14 @@ interface HourlyData {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function processLogs(
|
export function processLogs(logString: string): HourlyData[] {
|
||||||
logString: string,
|
|
||||||
dateRange?: { start?: string; end?: string },
|
|
||||||
): HourlyData[] {
|
|
||||||
if (_.isEmpty(logString)) {
|
if (_.isEmpty(logString)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const hourlyData = _(logString)
|
const hourlyData = _(logString)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => {
|
.compact()
|
||||||
const trimmed = line.trim();
|
|
||||||
// Check if the line starts with { and ends with } to ensure it's a potential JSON object
|
|
||||||
return trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}");
|
|
||||||
})
|
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
try {
|
try {
|
||||||
const log: LogEntry = JSON.parse(entry);
|
const log: LogEntry = JSON.parse(entry);
|
||||||
@@ -28,20 +21,6 @@ export function processLogs(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const date = new Date(log.StartUTC);
|
const date = new Date(log.StartUTC);
|
||||||
|
|
||||||
if (dateRange?.start || dateRange?.end) {
|
|
||||||
const logDate = date.getTime();
|
|
||||||
const start = dateRange?.start
|
|
||||||
? new Date(dateRange.start).getTime()
|
|
||||||
: 0;
|
|
||||||
const end = dateRange?.end
|
|
||||||
? new Date(dateRange.end).getTime()
|
|
||||||
: Number.POSITIVE_INFINITY;
|
|
||||||
if (logDate < start || logDate > end) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${date.toISOString().slice(0, 13)}:00:00Z`;
|
return `${date.toISOString().slice(0, 13)}:00:00Z`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing log entry:", error);
|
console.error("Error parsing log entry:", error);
|
||||||
@@ -72,46 +51,21 @@ export function parseRawConfig(
|
|||||||
sort?: SortInfo,
|
sort?: SortInfo,
|
||||||
search?: string,
|
search?: string,
|
||||||
status?: string[],
|
status?: string[],
|
||||||
dateRange?: { start?: string; end?: string },
|
|
||||||
): { data: LogEntry[]; totalCount: number } {
|
): { data: LogEntry[]; totalCount: number } {
|
||||||
try {
|
try {
|
||||||
if (_.isEmpty(rawConfig)) {
|
if (_.isEmpty(rawConfig)) {
|
||||||
return { data: [], totalCount: 0 };
|
return { data: [], totalCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Split logs into chunks to avoid memory issues
|
|
||||||
let parsedLogs = _(rawConfig)
|
let parsedLogs = _(rawConfig)
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
return (
|
|
||||||
trimmed !== "" && trimmed.startsWith("{") && trimmed.endsWith("}")
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.map((line) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(line) as LogEntry;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing log line:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.compact()
|
.compact()
|
||||||
|
.map((line) => JSON.parse(line) as LogEntry)
|
||||||
.value();
|
.value();
|
||||||
|
|
||||||
// Apply date range filter if provided
|
parsedLogs = parsedLogs.filter(
|
||||||
if (dateRange?.start || dateRange?.end) {
|
(log) => log.ServiceName !== "dokploy-service-app@file",
|
||||||
parsedLogs = parsedLogs.filter((log) => {
|
);
|
||||||
const logDate = new Date(log.StartUTC).getTime();
|
|
||||||
const start = dateRange?.start
|
|
||||||
? new Date(dateRange.start).getTime()
|
|
||||||
: 0;
|
|
||||||
const end = dateRange?.end
|
|
||||||
? new Date(dateRange.end).getTime()
|
|
||||||
: Number.POSITIVE_INFINITY;
|
|
||||||
return logDate >= start && logDate <= end;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
parsedLogs = parsedLogs.filter((log) =>
|
parsedLogs = parsedLogs.filter((log) =>
|
||||||
@@ -124,7 +78,6 @@ export function parseRawConfig(
|
|||||||
status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
|
status.some((range) => isStatusInRange(log.DownstreamStatus, range)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCount = parsedLogs.length;
|
const totalCount = parsedLogs.length;
|
||||||
|
|
||||||
if (sort) {
|
if (sort) {
|
||||||
@@ -148,7 +101,6 @@ export function parseRawConfig(
|
|||||||
throw new Error("Failed to parse rawConfig");
|
throw new Error("Failed to parse rawConfig");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStatusInRange = (status: number, range: string) => {
|
const isStatusInRange = (status: number, range: string) => {
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case "info":
|
case "info":
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { runMongoBackup } from "./mongo";
|
|||||||
import { runMySqlBackup } from "./mysql";
|
import { runMySqlBackup } from "./mysql";
|
||||||
import { runPostgresBackup } from "./postgres";
|
import { runPostgresBackup } from "./postgres";
|
||||||
import { findAdmin } from "../../services/admin";
|
import { findAdmin } from "../../services/admin";
|
||||||
import { startLogCleanup } from "../access-log/handler";
|
|
||||||
|
|
||||||
export const initCronJobs = async () => {
|
export const initCronJobs = async () => {
|
||||||
console.log("Setting up cron jobs....");
|
console.log("Setting up cron jobs....");
|
||||||
@@ -169,8 +168,4 @@ export const initCronJobs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin?.user.logCleanupCron) {
|
|
||||||
await startLogCleanup(admin.user.logCleanupCron);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,44 +137,12 @@ export const readRemoteConfig = async (serverId: string, appName: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readMonitoringConfig = (readAll = false) => {
|
export const readMonitoringConfig = () => {
|
||||||
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
const { DYNAMIC_TRAEFIK_PATH } = paths();
|
||||||
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
const configPath = path.join(DYNAMIC_TRAEFIK_PATH, "access.log");
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
if (!readAll) {
|
const yamlStr = fs.readFileSync(configPath, "utf8");
|
||||||
// Read first 500 lines
|
return yamlStr;
|
||||||
let content = "";
|
|
||||||
let chunk = "";
|
|
||||||
let validCount = 0;
|
|
||||||
|
|
||||||
for (const char of fs.readFileSync(configPath, "utf8")) {
|
|
||||||
chunk += char;
|
|
||||||
if (char === "\n") {
|
|
||||||
try {
|
|
||||||
const trimmed = chunk.trim();
|
|
||||||
if (
|
|
||||||
trimmed !== "" &&
|
|
||||||
trimmed.startsWith("{") &&
|
|
||||||
trimmed.endsWith("}")
|
|
||||||
) {
|
|
||||||
const log = JSON.parse(trimmed);
|
|
||||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
|
||||||
content += chunk;
|
|
||||||
validCount++;
|
|
||||||
if (validCount >= 500) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore invalid JSON
|
|
||||||
}
|
|
||||||
chunk = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
return fs.readFileSync(configPath, "utf8");
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
1
pnpm-lock.yaml
generated
1
pnpm-lock.yaml
generated
@@ -6282,7 +6282,6 @@ packages:
|
|||||||
|
|
||||||
oslo@1.2.0:
|
oslo@1.2.0:
|
||||||
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
resolution: {integrity: sha512-OoFX6rDsNcOQVAD2gQD/z03u4vEjWZLzJtwkmgfRF+KpQUXwdgEXErD7zNhyowmHwHefP+PM9Pw13pgpHMRlzw==}
|
||||||
deprecated: Package is no longer supported. Please see https://oslojs.dev for the successor project.
|
|
||||||
|
|
||||||
otpauth@9.3.4:
|
otpauth@9.3.4:
|
||||||
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
|
||||||
|
|||||||
Reference in New Issue
Block a user