Compare commits

...

50 Commits

Author SHA1 Message Date
Mauricio Siu
9498fbeff3 Update package.json 2025-12-31 00:28:03 -06:00
Mauricio Siu
d2aa60ddf7 Update package.json 2025-12-30 23:53:30 -06:00
Mauricio Siu
58b75205af Merge pull request #3327 from Dokploy/refactor/separate-settings-from-users-table
refactor(settings): migrate user settings to webServerSettings schema…
2025-12-28 13:21:55 -06:00
Mauricio Siu
9e03625586 refactor(auth): simplify trustedOrigins logic by removing redundant admin check and using optional chaining for settings access 2025-12-28 13:18:20 -06:00
Mauricio Siu
260efdc2bb Merge pull request #3353 from bdkopen/remove-rotating-file-stream
chore: uninstall `rotating-file-stream`
2025-12-28 13:09:34 -06:00
bdkopen
1b5bfe051d chore: uninstall rotating-file-stream 2025-12-27 12:33:39 -05:00
Mauricio Siu
e4384075f2 Merge pull request #3341 from dpulpeiro/fix/stack-registry-auth
fix: pass registry auth to stack deploy
2025-12-25 03:29:33 -06:00
Mauricio Siu
b355d44605 fix(web-server-settings): use optional chaining for safer ID access in update function 2025-12-24 12:24:27 -06:00
Daniel García Pulpeiro
f39aa23803 fix: pass registry auth to stack deploy 2025-12-23 22:37:00 +01:00
Mauricio Siu
3abc4cdc3b refactor(access-log): consolidate web server settings imports and enhance log cleanup status retrieval 2025-12-21 01:46:27 -06:00
Mauricio Siu
ec56062f17 fix(settings): update getIp function to return an empty string for cloud environments 2025-12-21 01:45:49 -06:00
Mauricio Siu
10c4f882a5 Update packages/server/src/services/web-server-settings.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-21 01:44:46 -06:00
Mauricio Siu
f1dfa9c6a2 refactor(preview-deployment): remove dynamic import of getWebServerSettings and streamline IP retrieval logic 2025-12-21 01:43:09 -06:00
Mauricio Siu
6010643d9e refactor(server): update server configuration handling to utilize webServerSettings schema and improve code clarity 2025-12-21 01:41:33 -06:00
Mauricio Siu
1ccb205495 fix(admin): add optional chaining to safely access settings properties 2025-12-21 01:35:21 -06:00
autofix-ci[bot]
b2be5bc09f [autofix.ci] apply automated fixes 2025-12-21 07:33:59 +00:00
Mauricio Siu
babd30a110 refactor(settings): migrate user settings to webServerSettings schema and update related components 2025-12-21 01:33:18 -06:00
Mauricio Siu
e77f276785 refactor(issue-template): remove unnecessary dropdowns for git providers in bug report 2025-12-21 01:06:12 -06:00
Mauricio Siu
78c9a047b0 feat(issue-template): add dropdowns for affected areas and git providers in bug report 2025-12-21 01:05:33 -06:00
Mauricio Siu
84e0f5856b Merge pull request #3164 from difagume/fix/log-warning-detection
fix(docker-logs): fix warning symbol detection
2025-12-20 23:02:00 -06:00
Mauricio Siu
2bfa4643fc Merge pull request #3186 from divaltor/slider-resources
feat(resources): Add number component to have better UX control over Docker resources
2025-12-20 22:56:54 -06:00
Mauricio Siu
8c7bc82712 Merge pull request #3323 from Dokploy/copilot/fix-shell-command-issue
fix: quote registry username in docker login to prevent shell variable expansion
2025-12-20 21:24:57 -06:00
copilot-swe-agent[bot]
44645a6fbe fix: properly quote registry username in docker login to handle special characters like $
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 19:41:56 +00:00
copilot-swe-agent[bot]
771d0dd8ab Initial plan 2025-12-20 19:35:55 +00:00
Mauricio Siu
67725759e6 Merge pull request #3318 from Dokploy/copilot/fix-perplexity-ai-models-endpoint
Fix Perplexity AI provider models endpoint by returning hardcoded model list
2025-12-20 13:34:55 -06:00
Mauricio Siu
2065372d4f fix: update test command in package.json to remove specific test target 2025-12-20 13:34:32 -06:00
copilot-swe-agent[bot]
69d5c6f0cb Fix Perplexity AI provider by adding hardcoded model list
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2025-12-20 06:43:11 +00:00
copilot-swe-agent[bot]
53f67c6eb2 Initial plan 2025-12-20 06:38:09 +00:00
Mauricio Siu
7c53a3ef75 Merge pull request #3316 from Dokploy/feat/add-admin-creation-projects
fix: update project handling permissions to include admin role
2025-12-19 23:26:44 -06:00
Mauricio Siu
c065c85ee6 fix: update project handling permissions to include admin role 2025-12-19 23:26:12 -06:00
Mauricio Siu
db97de2a39 Merge pull request #3255 from odedd/feat/all-timezones-support
feat(schedules): add support for all IANA timezones
2025-12-19 23:23:27 -06:00
Mauricio Siu
dc7af1b840 Merge pull request #3269 from gosangam/canary
fix: return database instance as response on db creation (mongo, mysq…
2025-12-19 23:21:57 -06:00
Mauricio Siu
97362da2ae Merge pull request #3303 from draconisNoctis/feature/Fix-Disabling-of-Require-Collaborator-Permissions-Form
fix: disabling of previewRequireCollaboratorPermissions
2025-12-19 23:19:25 -06:00
Mauricio Siu
b476e50ff1 Merge pull request #3229 from fir4tozden/fix/some-fixes-in-dockerSafeExec
fix: some fixes in dockerSafeExec()
2025-12-19 23:10:59 -06:00
Mauricio Siu
1b22384315 Merge pull request #3267 from fir4tozden/bug-fix/volume-cleaning-should-not-be-performed
[CRITICAL] fix: volume cleaning should not be performed
2025-12-19 23:10:31 -06:00
Mark Wecke
e607220bfa fix: disabling of previewRequireCollaboratorPermissions 2025-12-18 15:37:51 +01:00
gosangam
b65f53d141 fix: return database instance as response on db creation (mongo, mysql, mariadb & postgres) 2025-12-14 20:31:05 +05:30
autofix-ci[bot]
2b1a3db7b8 [autofix.ci] apply automated fixes 2025-12-14 05:20:20 +00:00
фырат ёздэн
b66156956a fix: typing 2025-12-14 08:20:00 +03:00
autofix-ci[bot]
669de0f95f [autofix.ci] apply automated fixes 2025-12-14 05:16:30 +00:00
фырат ёздэн
371cf83e52 fix: typing 2025-12-14 08:16:09 +03:00
фырат ёздэн
51abf49458 chore: update pr id 2025-12-14 08:13:02 +03:00
autofix-ci[bot]
ba5283039c [autofix.ci] apply automated fixes 2025-12-14 05:11:51 +00:00
фырат ёздэн
19a7a80d43 [BUG] fix: volume cleaning should not be performed 2025-12-14 08:06:55 +03:00
фырат ёздэн
5d42737943 cepte 2025-12-14 07:32:28 +03:00
фырат ёздэн
4c10056394 chore 2025-12-14 07:24:27 +03:00
Oded Davidov
c045c5328f feat(schedules): add support for all IANA timezones
- Replace limited 15-timezone list with comprehensive 421 IANA timezones
- Add searchable timezone selector with region grouping for better UX
- Create dedicated timezones.ts file following project conventions
- Support all timezone offsets including 30-min and 45-min offsets

Closes #2935

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 23:07:03 +02:00
фырат ёздэн
8c889fc71e fix: some fixes in dockerSafeExec() 2025-12-10 22:06:55 +03:00
Vlad Vladov
d465fb4da1 feat(resources): Add number component to have better UX control over Docker resources 2025-12-07 13:45:14 +02:00
diego fabricio
698104e7b7 fix(docker-logs): fix warning symbol detection
- Added support for detecting warning symbols (⚠, ⚠️) in log messages
2025-12-04 11:33:29 -05:00
49 changed files with 8297 additions and 417 deletions

View File

@@ -206,4 +206,38 @@ describe("getRegistryTag", () => {
expect(result).toBe("docker.io/myuser/repo");
});
});
describe("special characters in username", () => {
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
const registry = createMockRegistry({
username: "robot$library+dokploy",
});
const result = getRegistryTag(registry, "nginx");
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
});
it("should handle username with $ and other special characters", () => {
const registry = createMockRegistry({
username: "robot$test+app",
});
const result = getRegistryTag(registry, "myapp:latest");
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
});
it("should handle username with multiple $ symbols", () => {
const registry = createMockRegistry({
username: "user$name$test",
});
const result = getRegistryTag(registry, "app");
expect(result).toBe("docker.io/user$name$test/app");
});
it("should handle username with + and - symbols", () => {
const registry = createMockRegistry({
username: "robot+test-user",
});
const result = getRegistryTag(registry, "nginx:latest");
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
});
});
});

View File

@@ -1,7 +1,7 @@
import type { Domain } from "@dokploy/server";
import { createDomainLabels } from "@dokploy/server";
import { parse, stringify } from "yaml";
import { describe, expect, it } from "vitest";
import { parse, stringify } from "yaml";
/**
* Regression tests for Traefik Host rule label format.

View File

@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
default: fs,
}));
import type { FileConfig, User } from "@dokploy/server";
import type { FileConfig } from "@dokploy/server";
import {
createDefaultServerTraefikConfig,
loadOrCreateConfig,
updateServerTraefik,
} from "@dokploy/server";
import type { webServerSettings } from "@dokploy/server/db/schema";
import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = {
type WebServerSettings = typeof webServerSettings.$inferSelect;
const baseSettings: WebServerSettings = {
id: "",
https: false,
enablePaidFeatures: false,
allowImpersonation: false,
role: "user",
firstName: "",
lastName: "",
certificateType: "none",
host: null,
serverIp: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
metricsConfig: {
containers: {
refreshRate: 20,
@@ -45,29 +51,8 @@ const baseAdmin: User = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
createdAt: new Date(),
serverIp: null,
certificateType: "none",
host: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
banExpires: new Date(),
banned: true,
banReason: "",
email: "",
expirationDate: "",
id: "",
isRegistered: false,
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
createdAt: null,
updatedAt: new Date(),
twoFactorEnabled: false,
};
beforeEach(() => {
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
test("Should apply redirect-to-https", () => {
updateServerTraefik(
{
...baseAdmin,
...baseSettings,
https: true,
certificateType: "letsencrypt",
},
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
});
test("Should change only host when no certificate", () => {
updateServerTraefik(baseAdmin, "example.com");
updateServerTraefik(baseSettings, "example.com");
const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
test("Should not touch config without host", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(baseAdmin, null);
updateServerTraefik(baseSettings, null);
const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
test("Should remove websecure if https rollback to http", () => {
updateServerTraefik(
{ ...baseAdmin, certificateType: "letsencrypt" },
{ ...baseSettings, certificateType: "letsencrypt" },
"example.com",
);
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
updateServerTraefik(
{ ...baseSettings, certificateType: "none" },
"example.com",
);
const config: FileConfig = loadOrCreateConfig("dokploy");

View File

@@ -21,7 +21,10 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
createConverter,
NumberInputWithSteps,
} from "@/components/ui/number-input";
import {
Tooltip,
TooltipContent,
@@ -30,6 +33,23 @@ import {
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
const CPU_STEP = 0.25;
const MEMORY_STEP_MB = 256;
const formatNumber = (value: number, decimals = 2): string =>
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
);
const memoryConverter = createConverter(1024 * 1024, (mb) => {
if (mb <= 0) return "";
return mb >= 1024
? `${formatNumber(mb / 1024)} GB`
: `${formatNumber(mb)} MB`;
});
const addResourcesSchema = z.object({
memoryReservation: z.string().optional(),
cpuLimit: z.string().optional(),
@@ -51,6 +71,7 @@ interface Props {
}
type AddResources = z.infer<typeof addResourcesSchema>;
export const ShowResources = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory hard limit in bytes. Example: 1GB =
1073741824 bytes
1073741824 bytes. Use +/- buttons to adjust by
256 MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1073741824 (1GB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
Memory soft limit in bytes. Example: 256MB =
268435456 bytes
268435456 bytes. Use +/- buttons to adjust by 256
MB.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="268435456 (256MB in bytes)"
{...field}
step={MEMORY_STEP_MB}
converter={memoryConverter}
/>
</FormControl>
<FormMessage />
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU quota in units of 10^-9 CPUs. Example: 2
CPUs = 2000000000
CPUs = 2000000000. Use +/- buttons to adjust by
0.25 CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="2000000000 (2 CPUs)"
{...field}
value={field.value?.toString() || ""}
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
<TooltipContent>
<p>
CPU shares (relative weight). Example: 1 CPU =
1000000000
1000000000. Use +/- buttons to adjust by 0.25
CPU.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input placeholder="1000000000 (1 CPU)" {...field} />
<NumberInputWithSteps
value={field.value}
onChange={field.onChange}
placeholder="1000000000 (1 CPU)"
step={CPU_STEP}
converter={cpuConverter}
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -123,7 +123,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions || true,
data.previewRequireCollaboratorPermissions ?? true,
});
}
}, [data]);

View File

@@ -1,5 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import {
CheckIcon,
ChevronsUpDown,
DatabaseZap,
Info,
PenBoxIcon,
@@ -13,6 +15,14 @@ import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
@@ -31,6 +41,12 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@@ -48,6 +64,7 @@ import {
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain";
import { getTimezoneLabel, TIMEZONES } from "./timezones";
export const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" },
@@ -60,30 +77,6 @@ export const commonCronExpressions = [
{ label: "Custom", value: "custom" },
];
export const commonTimezones = [
{ label: "UTC (Coordinated Universal Time)", value: "UTC" },
{ label: "America/New_York (Eastern Time)", value: "America/New_York" },
{ label: "America/Chicago (Central Time)", value: "America/Chicago" },
{ label: "America/Denver (Mountain Time)", value: "America/Denver" },
{ label: "America/Los_Angeles (Pacific Time)", value: "America/Los_Angeles" },
{
label: "America/Mexico_City (Central Mexico)",
value: "America/Mexico_City",
},
{ label: "America/Sao_Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
{ label: "Europe/London (Greenwich Mean Time)", value: "Europe/London" },
{ label: "Europe/Paris (Central European Time)", value: "Europe/Paris" },
{ label: "Europe/Berlin (Central European Time)", value: "Europe/Berlin" },
{ label: "Asia/Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
{ label: "Asia/Shanghai (China Standard Time)", value: "Asia/Shanghai" },
{ label: "Asia/Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
{ label: "Asia/Kolkata (India Standard Time)", value: "Asia/Kolkata" },
{
label: "Australia/Sydney (Australian Eastern Time)",
value: "Australia/Sydney",
},
];
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
@@ -512,25 +505,60 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
</Tooltip>
</TooltipProvider>
</FormLabel>
<Select
onValueChange={(value) => {
field.onChange(value);
}}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="UTC (default)" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonTimezones.map((tz) => (
<SelectItem key={tz.value} value={tz.value}>
{tz.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
)}
>
{getTimezoneLabel(field.value)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0" align="start">
<Command>
<CommandInput
placeholder="Search timezone..."
className="h-9"
/>
<CommandList>
<CommandEmpty>No timezone found.</CommandEmpty>
<ScrollArea className="h-72">
{Object.entries(TIMEZONES).map(
([region, zones]) => (
<CommandGroup key={region} heading={region}>
{zones.map((tz) => (
<CommandItem
key={tz.value}
value={`${region} ${tz.label} ${tz.value}`}
onSelect={() => {
field.onChange(tz.value);
}}
>
{tz.value}
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
field.value === tz.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
),
)}
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormDescription>
Optional: Choose a timezone for the schedule execution time
</FormDescription>

View File

@@ -0,0 +1,458 @@
// Complete list of IANA timezones grouped by region
export const TIMEZONES: Record<
string,
Array<{ label: string; value: string }>
> = {
Common: [{ label: "UTC (Coordinated Universal Time)", value: "UTC" }],
Africa: [
{ label: "Abidjan", value: "Africa/Abidjan" },
{ label: "Accra", value: "Africa/Accra" },
{ label: "Addis Ababa", value: "Africa/Addis_Ababa" },
{ label: "Algiers", value: "Africa/Algiers" },
{ label: "Asmara", value: "Africa/Asmara" },
{ label: "Bamako", value: "Africa/Bamako" },
{ label: "Bangui", value: "Africa/Bangui" },
{ label: "Banjul", value: "Africa/Banjul" },
{ label: "Bissau", value: "Africa/Bissau" },
{ label: "Blantyre", value: "Africa/Blantyre" },
{ label: "Brazzaville", value: "Africa/Brazzaville" },
{ label: "Bujumbura", value: "Africa/Bujumbura" },
{ label: "Cairo", value: "Africa/Cairo" },
{ label: "Casablanca", value: "Africa/Casablanca" },
{ label: "Ceuta", value: "Africa/Ceuta" },
{ label: "Conakry", value: "Africa/Conakry" },
{ label: "Dakar", value: "Africa/Dakar" },
{ label: "Dar es Salaam", value: "Africa/Dar_es_Salaam" },
{ label: "Djibouti", value: "Africa/Djibouti" },
{ label: "Douala", value: "Africa/Douala" },
{ label: "El Aaiun", value: "Africa/El_Aaiun" },
{ label: "Freetown", value: "Africa/Freetown" },
{ label: "Gaborone", value: "Africa/Gaborone" },
{ label: "Harare", value: "Africa/Harare" },
{ label: "Johannesburg", value: "Africa/Johannesburg" },
{ label: "Juba", value: "Africa/Juba" },
{ label: "Kampala", value: "Africa/Kampala" },
{ label: "Khartoum", value: "Africa/Khartoum" },
{ label: "Kigali", value: "Africa/Kigali" },
{ label: "Kinshasa", value: "Africa/Kinshasa" },
{ label: "Lagos", value: "Africa/Lagos" },
{ label: "Libreville", value: "Africa/Libreville" },
{ label: "Lome", value: "Africa/Lome" },
{ label: "Luanda", value: "Africa/Luanda" },
{ label: "Lubumbashi", value: "Africa/Lubumbashi" },
{ label: "Lusaka", value: "Africa/Lusaka" },
{ label: "Malabo", value: "Africa/Malabo" },
{ label: "Maputo", value: "Africa/Maputo" },
{ label: "Maseru", value: "Africa/Maseru" },
{ label: "Mbabane", value: "Africa/Mbabane" },
{ label: "Mogadishu", value: "Africa/Mogadishu" },
{ label: "Monrovia", value: "Africa/Monrovia" },
{ label: "Nairobi", value: "Africa/Nairobi" },
{ label: "Ndjamena", value: "Africa/Ndjamena" },
{ label: "Niamey", value: "Africa/Niamey" },
{ label: "Nouakchott", value: "Africa/Nouakchott" },
{ label: "Ouagadougou", value: "Africa/Ouagadougou" },
{ label: "Porto-Novo", value: "Africa/Porto-Novo" },
{ label: "Sao Tome", value: "Africa/Sao_Tome" },
{ label: "Tripoli", value: "Africa/Tripoli" },
{ label: "Tunis", value: "Africa/Tunis" },
{ label: "Windhoek", value: "Africa/Windhoek" },
],
America: [
{ label: "Adak", value: "America/Adak" },
{ label: "Anchorage", value: "America/Anchorage" },
{ label: "Anguilla", value: "America/Anguilla" },
{ label: "Antigua", value: "America/Antigua" },
{ label: "Araguaina", value: "America/Araguaina" },
{
label: "Argentina/Buenos Aires",
value: "America/Argentina/Buenos_Aires",
},
{ label: "Argentina/Catamarca", value: "America/Argentina/Catamarca" },
{ label: "Argentina/Cordoba", value: "America/Argentina/Cordoba" },
{ label: "Argentina/Jujuy", value: "America/Argentina/Jujuy" },
{ label: "Argentina/La Rioja", value: "America/Argentina/La_Rioja" },
{ label: "Argentina/Mendoza", value: "America/Argentina/Mendoza" },
{
label: "Argentina/Rio Gallegos",
value: "America/Argentina/Rio_Gallegos",
},
{ label: "Argentina/Salta", value: "America/Argentina/Salta" },
{ label: "Argentina/San Juan", value: "America/Argentina/San_Juan" },
{ label: "Argentina/San Luis", value: "America/Argentina/San_Luis" },
{ label: "Argentina/Tucuman", value: "America/Argentina/Tucuman" },
{ label: "Argentina/Ushuaia", value: "America/Argentina/Ushuaia" },
{ label: "Aruba", value: "America/Aruba" },
{ label: "Asuncion", value: "America/Asuncion" },
{ label: "Atikokan", value: "America/Atikokan" },
{ label: "Bahia", value: "America/Bahia" },
{ label: "Bahia Banderas", value: "America/Bahia_Banderas" },
{ label: "Barbados", value: "America/Barbados" },
{ label: "Belem", value: "America/Belem" },
{ label: "Belize", value: "America/Belize" },
{ label: "Blanc-Sablon", value: "America/Blanc-Sablon" },
{ label: "Boa Vista", value: "America/Boa_Vista" },
{ label: "Bogota", value: "America/Bogota" },
{ label: "Boise", value: "America/Boise" },
{ label: "Cambridge Bay", value: "America/Cambridge_Bay" },
{ label: "Campo Grande", value: "America/Campo_Grande" },
{ label: "Cancun", value: "America/Cancun" },
{ label: "Caracas", value: "America/Caracas" },
{ label: "Cayenne", value: "America/Cayenne" },
{ label: "Cayman", value: "America/Cayman" },
{ label: "Chicago (Central Time)", value: "America/Chicago" },
{ label: "Chihuahua", value: "America/Chihuahua" },
{ label: "Ciudad Juarez", value: "America/Ciudad_Juarez" },
{ label: "Costa Rica", value: "America/Costa_Rica" },
{ label: "Creston", value: "America/Creston" },
{ label: "Cuiaba", value: "America/Cuiaba" },
{ label: "Curacao", value: "America/Curacao" },
{ label: "Danmarkshavn", value: "America/Danmarkshavn" },
{ label: "Dawson", value: "America/Dawson" },
{ label: "Dawson Creek", value: "America/Dawson_Creek" },
{ label: "Denver (Mountain Time)", value: "America/Denver" },
{ label: "Detroit", value: "America/Detroit" },
{ label: "Dominica", value: "America/Dominica" },
{ label: "Edmonton", value: "America/Edmonton" },
{ label: "Eirunepe", value: "America/Eirunepe" },
{ label: "El Salvador", value: "America/El_Salvador" },
{ label: "Fort Nelson", value: "America/Fort_Nelson" },
{ label: "Fortaleza", value: "America/Fortaleza" },
{ label: "Glace Bay", value: "America/Glace_Bay" },
{ label: "Goose Bay", value: "America/Goose_Bay" },
{ label: "Grand Turk", value: "America/Grand_Turk" },
{ label: "Grenada", value: "America/Grenada" },
{ label: "Guadeloupe", value: "America/Guadeloupe" },
{ label: "Guatemala", value: "America/Guatemala" },
{ label: "Guayaquil", value: "America/Guayaquil" },
{ label: "Guyana", value: "America/Guyana" },
{ label: "Halifax", value: "America/Halifax" },
{ label: "Havana", value: "America/Havana" },
{ label: "Hermosillo", value: "America/Hermosillo" },
{ label: "Indiana/Indianapolis", value: "America/Indiana/Indianapolis" },
{ label: "Indiana/Knox", value: "America/Indiana/Knox" },
{ label: "Indiana/Marengo", value: "America/Indiana/Marengo" },
{ label: "Indiana/Petersburg", value: "America/Indiana/Petersburg" },
{ label: "Indiana/Tell City", value: "America/Indiana/Tell_City" },
{ label: "Indiana/Vevay", value: "America/Indiana/Vevay" },
{ label: "Indiana/Vincennes", value: "America/Indiana/Vincennes" },
{ label: "Indiana/Winamac", value: "America/Indiana/Winamac" },
{ label: "Inuvik", value: "America/Inuvik" },
{ label: "Iqaluit", value: "America/Iqaluit" },
{ label: "Jamaica", value: "America/Jamaica" },
{ label: "Juneau", value: "America/Juneau" },
{ label: "Kentucky/Louisville", value: "America/Kentucky/Louisville" },
{ label: "Kentucky/Monticello", value: "America/Kentucky/Monticello" },
{ label: "Kralendijk", value: "America/Kralendijk" },
{ label: "La Paz", value: "America/La_Paz" },
{ label: "Lima", value: "America/Lima" },
{ label: "Los Angeles (Pacific Time)", value: "America/Los_Angeles" },
{ label: "Lower Princes", value: "America/Lower_Princes" },
{ label: "Maceio", value: "America/Maceio" },
{ label: "Managua", value: "America/Managua" },
{ label: "Manaus", value: "America/Manaus" },
{ label: "Marigot", value: "America/Marigot" },
{ label: "Martinique", value: "America/Martinique" },
{ label: "Matamoros", value: "America/Matamoros" },
{ label: "Mazatlan", value: "America/Mazatlan" },
{ label: "Menominee", value: "America/Menominee" },
{ label: "Merida", value: "America/Merida" },
{ label: "Metlakatla", value: "America/Metlakatla" },
{ label: "Mexico City (Central Mexico)", value: "America/Mexico_City" },
{ label: "Miquelon", value: "America/Miquelon" },
{ label: "Moncton", value: "America/Moncton" },
{ label: "Monterrey", value: "America/Monterrey" },
{ label: "Montevideo", value: "America/Montevideo" },
{ label: "Montserrat", value: "America/Montserrat" },
{ label: "Nassau", value: "America/Nassau" },
{ label: "New York (Eastern Time)", value: "America/New_York" },
{ label: "Nome", value: "America/Nome" },
{ label: "Noronha", value: "America/Noronha" },
{ label: "North Dakota/Beulah", value: "America/North_Dakota/Beulah" },
{ label: "North Dakota/Center", value: "America/North_Dakota/Center" },
{
label: "North Dakota/New Salem",
value: "America/North_Dakota/New_Salem",
},
{ label: "Nuuk", value: "America/Nuuk" },
{ label: "Ojinaga", value: "America/Ojinaga" },
{ label: "Panama", value: "America/Panama" },
{ label: "Paramaribo", value: "America/Paramaribo" },
{ label: "Phoenix", value: "America/Phoenix" },
{ label: "Port-au-Prince", value: "America/Port-au-Prince" },
{ label: "Port of Spain", value: "America/Port_of_Spain" },
{ label: "Porto Velho", value: "America/Porto_Velho" },
{ label: "Puerto Rico", value: "America/Puerto_Rico" },
{ label: "Punta Arenas", value: "America/Punta_Arenas" },
{ label: "Rankin Inlet", value: "America/Rankin_Inlet" },
{ label: "Recife", value: "America/Recife" },
{ label: "Regina", value: "America/Regina" },
{ label: "Resolute", value: "America/Resolute" },
{ label: "Rio Branco", value: "America/Rio_Branco" },
{ label: "Santarem", value: "America/Santarem" },
{ label: "Santiago", value: "America/Santiago" },
{ label: "Santo Domingo", value: "America/Santo_Domingo" },
{ label: "Sao Paulo (Brasilia Time)", value: "America/Sao_Paulo" },
{ label: "Scoresbysund", value: "America/Scoresbysund" },
{ label: "Sitka", value: "America/Sitka" },
{ label: "St Barthelemy", value: "America/St_Barthelemy" },
{ label: "St Johns", value: "America/St_Johns" },
{ label: "St Kitts", value: "America/St_Kitts" },
{ label: "St Lucia", value: "America/St_Lucia" },
{ label: "St Thomas", value: "America/St_Thomas" },
{ label: "St Vincent", value: "America/St_Vincent" },
{ label: "Swift Current", value: "America/Swift_Current" },
{ label: "Tegucigalpa", value: "America/Tegucigalpa" },
{ label: "Thule", value: "America/Thule" },
{ label: "Tijuana", value: "America/Tijuana" },
{ label: "Toronto", value: "America/Toronto" },
{ label: "Tortola", value: "America/Tortola" },
{ label: "Vancouver", value: "America/Vancouver" },
{ label: "Whitehorse", value: "America/Whitehorse" },
{ label: "Winnipeg", value: "America/Winnipeg" },
{ label: "Yakutat", value: "America/Yakutat" },
],
Antarctica: [
{ label: "Casey", value: "Antarctica/Casey" },
{ label: "Davis", value: "Antarctica/Davis" },
{ label: "DumontDUrville", value: "Antarctica/DumontDUrville" },
{ label: "Macquarie", value: "Antarctica/Macquarie" },
{ label: "Mawson", value: "Antarctica/Mawson" },
{ label: "McMurdo", value: "Antarctica/McMurdo" },
{ label: "Palmer", value: "Antarctica/Palmer" },
{ label: "Rothera", value: "Antarctica/Rothera" },
{ label: "Syowa", value: "Antarctica/Syowa" },
{ label: "Troll", value: "Antarctica/Troll" },
{ label: "Vostok", value: "Antarctica/Vostok" },
],
Arctic: [{ label: "Longyearbyen", value: "Arctic/Longyearbyen" }],
Asia: [
{ label: "Aden", value: "Asia/Aden" },
{ label: "Almaty", value: "Asia/Almaty" },
{ label: "Amman", value: "Asia/Amman" },
{ label: "Anadyr", value: "Asia/Anadyr" },
{ label: "Aqtau", value: "Asia/Aqtau" },
{ label: "Aqtobe", value: "Asia/Aqtobe" },
{ label: "Ashgabat", value: "Asia/Ashgabat" },
{ label: "Atyrau", value: "Asia/Atyrau" },
{ label: "Baghdad", value: "Asia/Baghdad" },
{ label: "Bahrain", value: "Asia/Bahrain" },
{ label: "Baku", value: "Asia/Baku" },
{ label: "Bangkok", value: "Asia/Bangkok" },
{ label: "Barnaul", value: "Asia/Barnaul" },
{ label: "Beirut", value: "Asia/Beirut" },
{ label: "Bishkek", value: "Asia/Bishkek" },
{ label: "Brunei", value: "Asia/Brunei" },
{ label: "Chita", value: "Asia/Chita" },
{ label: "Choibalsan", value: "Asia/Choibalsan" },
{ label: "Colombo", value: "Asia/Colombo" },
{ label: "Damascus", value: "Asia/Damascus" },
{ label: "Dhaka", value: "Asia/Dhaka" },
{ label: "Dili", value: "Asia/Dili" },
{ label: "Dubai (Gulf Standard Time)", value: "Asia/Dubai" },
{ label: "Dushanbe", value: "Asia/Dushanbe" },
{ label: "Famagusta", value: "Asia/Famagusta" },
{ label: "Gaza", value: "Asia/Gaza" },
{ label: "Hebron", value: "Asia/Hebron" },
{ label: "Ho Chi Minh", value: "Asia/Ho_Chi_Minh" },
{ label: "Hong Kong", value: "Asia/Hong_Kong" },
{ label: "Hovd", value: "Asia/Hovd" },
{ label: "Irkutsk", value: "Asia/Irkutsk" },
{ label: "Jakarta", value: "Asia/Jakarta" },
{ label: "Jayapura", value: "Asia/Jayapura" },
{ label: "Jerusalem", value: "Asia/Jerusalem" },
{ label: "Kabul", value: "Asia/Kabul" },
{ label: "Kamchatka", value: "Asia/Kamchatka" },
{ label: "Karachi", value: "Asia/Karachi" },
{ label: "Kathmandu", value: "Asia/Kathmandu" },
{ label: "Khandyga", value: "Asia/Khandyga" },
{ label: "Kolkata (India Standard Time)", value: "Asia/Kolkata" },
{ label: "Krasnoyarsk", value: "Asia/Krasnoyarsk" },
{ label: "Kuala Lumpur", value: "Asia/Kuala_Lumpur" },
{ label: "Kuching", value: "Asia/Kuching" },
{ label: "Kuwait", value: "Asia/Kuwait" },
{ label: "Macau", value: "Asia/Macau" },
{ label: "Magadan", value: "Asia/Magadan" },
{ label: "Makassar", value: "Asia/Makassar" },
{ label: "Manila", value: "Asia/Manila" },
{ label: "Muscat", value: "Asia/Muscat" },
{ label: "Nicosia", value: "Asia/Nicosia" },
{ label: "Novokuznetsk", value: "Asia/Novokuznetsk" },
{ label: "Novosibirsk", value: "Asia/Novosibirsk" },
{ label: "Omsk", value: "Asia/Omsk" },
{ label: "Oral", value: "Asia/Oral" },
{ label: "Phnom Penh", value: "Asia/Phnom_Penh" },
{ label: "Pontianak", value: "Asia/Pontianak" },
{ label: "Pyongyang", value: "Asia/Pyongyang" },
{ label: "Qatar", value: "Asia/Qatar" },
{ label: "Qostanay", value: "Asia/Qostanay" },
{ label: "Qyzylorda", value: "Asia/Qyzylorda" },
{ label: "Riyadh", value: "Asia/Riyadh" },
{ label: "Sakhalin", value: "Asia/Sakhalin" },
{ label: "Samarkand", value: "Asia/Samarkand" },
{ label: "Seoul", value: "Asia/Seoul" },
{ label: "Shanghai (China Standard Time)", value: "Asia/Shanghai" },
{ label: "Singapore", value: "Asia/Singapore" },
{ label: "Srednekolymsk", value: "Asia/Srednekolymsk" },
{ label: "Taipei", value: "Asia/Taipei" },
{ label: "Tashkent", value: "Asia/Tashkent" },
{ label: "Tbilisi", value: "Asia/Tbilisi" },
{ label: "Tehran", value: "Asia/Tehran" },
{ label: "Thimphu", value: "Asia/Thimphu" },
{ label: "Tokyo (Japan Standard Time)", value: "Asia/Tokyo" },
{ label: "Tomsk", value: "Asia/Tomsk" },
{ label: "Ulaanbaatar", value: "Asia/Ulaanbaatar" },
{ label: "Urumqi", value: "Asia/Urumqi" },
{ label: "Ust-Nera", value: "Asia/Ust-Nera" },
{ label: "Vientiane", value: "Asia/Vientiane" },
{ label: "Vladivostok", value: "Asia/Vladivostok" },
{ label: "Yakutsk", value: "Asia/Yakutsk" },
{ label: "Yangon", value: "Asia/Yangon" },
{ label: "Yekaterinburg", value: "Asia/Yekaterinburg" },
{ label: "Yerevan", value: "Asia/Yerevan" },
],
Atlantic: [
{ label: "Azores", value: "Atlantic/Azores" },
{ label: "Bermuda", value: "Atlantic/Bermuda" },
{ label: "Canary", value: "Atlantic/Canary" },
{ label: "Cape Verde", value: "Atlantic/Cape_Verde" },
{ label: "Faroe", value: "Atlantic/Faroe" },
{ label: "Madeira", value: "Atlantic/Madeira" },
{ label: "Reykjavik", value: "Atlantic/Reykjavik" },
{ label: "South Georgia", value: "Atlantic/South_Georgia" },
{ label: "St Helena", value: "Atlantic/St_Helena" },
{ label: "Stanley", value: "Atlantic/Stanley" },
],
Australia: [
{ label: "Adelaide", value: "Australia/Adelaide" },
{ label: "Brisbane", value: "Australia/Brisbane" },
{ label: "Broken Hill", value: "Australia/Broken_Hill" },
{ label: "Darwin", value: "Australia/Darwin" },
{ label: "Eucla", value: "Australia/Eucla" },
{ label: "Hobart", value: "Australia/Hobart" },
{ label: "Lindeman", value: "Australia/Lindeman" },
{ label: "Lord Howe", value: "Australia/Lord_Howe" },
{ label: "Melbourne", value: "Australia/Melbourne" },
{ label: "Perth", value: "Australia/Perth" },
{ label: "Sydney (Australian Eastern Time)", value: "Australia/Sydney" },
],
Europe: [
{ label: "Amsterdam", value: "Europe/Amsterdam" },
{ label: "Andorra", value: "Europe/Andorra" },
{ label: "Astrakhan", value: "Europe/Astrakhan" },
{ label: "Athens", value: "Europe/Athens" },
{ label: "Belgrade", value: "Europe/Belgrade" },
{ label: "Berlin (Central European Time)", value: "Europe/Berlin" },
{ label: "Bratislava", value: "Europe/Bratislava" },
{ label: "Brussels", value: "Europe/Brussels" },
{ label: "Bucharest", value: "Europe/Bucharest" },
{ label: "Budapest", value: "Europe/Budapest" },
{ label: "Busingen", value: "Europe/Busingen" },
{ label: "Chisinau", value: "Europe/Chisinau" },
{ label: "Copenhagen", value: "Europe/Copenhagen" },
{ label: "Dublin", value: "Europe/Dublin" },
{ label: "Gibraltar", value: "Europe/Gibraltar" },
{ label: "Guernsey", value: "Europe/Guernsey" },
{ label: "Helsinki", value: "Europe/Helsinki" },
{ label: "Isle of Man", value: "Europe/Isle_of_Man" },
{ label: "Istanbul", value: "Europe/Istanbul" },
{ label: "Jersey", value: "Europe/Jersey" },
{ label: "Kaliningrad", value: "Europe/Kaliningrad" },
{ label: "Kirov", value: "Europe/Kirov" },
{ label: "Kyiv", value: "Europe/Kyiv" },
{ label: "Lisbon", value: "Europe/Lisbon" },
{ label: "Ljubljana", value: "Europe/Ljubljana" },
{ label: "London (Greenwich Mean Time)", value: "Europe/London" },
{ label: "Luxembourg", value: "Europe/Luxembourg" },
{ label: "Madrid", value: "Europe/Madrid" },
{ label: "Malta", value: "Europe/Malta" },
{ label: "Mariehamn", value: "Europe/Mariehamn" },
{ label: "Minsk", value: "Europe/Minsk" },
{ label: "Monaco", value: "Europe/Monaco" },
{ label: "Moscow", value: "Europe/Moscow" },
{ label: "Oslo", value: "Europe/Oslo" },
{ label: "Paris (Central European Time)", value: "Europe/Paris" },
{ label: "Podgorica", value: "Europe/Podgorica" },
{ label: "Prague", value: "Europe/Prague" },
{ label: "Riga", value: "Europe/Riga" },
{ label: "Rome", value: "Europe/Rome" },
{ label: "Samara", value: "Europe/Samara" },
{ label: "San Marino", value: "Europe/San_Marino" },
{ label: "Sarajevo", value: "Europe/Sarajevo" },
{ label: "Saratov", value: "Europe/Saratov" },
{ label: "Simferopol", value: "Europe/Simferopol" },
{ label: "Skopje", value: "Europe/Skopje" },
{ label: "Sofia", value: "Europe/Sofia" },
{ label: "Stockholm", value: "Europe/Stockholm" },
{ label: "Tallinn", value: "Europe/Tallinn" },
{ label: "Tirane", value: "Europe/Tirane" },
{ label: "Ulyanovsk", value: "Europe/Ulyanovsk" },
{ label: "Vaduz", value: "Europe/Vaduz" },
{ label: "Vatican", value: "Europe/Vatican" },
{ label: "Vienna", value: "Europe/Vienna" },
{ label: "Vilnius", value: "Europe/Vilnius" },
{ label: "Volgograd", value: "Europe/Volgograd" },
{ label: "Warsaw", value: "Europe/Warsaw" },
{ label: "Zagreb", value: "Europe/Zagreb" },
{ label: "Zurich", value: "Europe/Zurich" },
],
Indian: [
{ label: "Antananarivo", value: "Indian/Antananarivo" },
{ label: "Chagos", value: "Indian/Chagos" },
{ label: "Christmas", value: "Indian/Christmas" },
{ label: "Cocos", value: "Indian/Cocos" },
{ label: "Comoro", value: "Indian/Comoro" },
{ label: "Kerguelen", value: "Indian/Kerguelen" },
{ label: "Mahe", value: "Indian/Mahe" },
{ label: "Maldives", value: "Indian/Maldives" },
{ label: "Mauritius", value: "Indian/Mauritius" },
{ label: "Mayotte", value: "Indian/Mayotte" },
{ label: "Reunion", value: "Indian/Reunion" },
],
Pacific: [
{ label: "Apia", value: "Pacific/Apia" },
{ label: "Auckland", value: "Pacific/Auckland" },
{ label: "Bougainville", value: "Pacific/Bougainville" },
{ label: "Chatham", value: "Pacific/Chatham" },
{ label: "Chuuk", value: "Pacific/Chuuk" },
{ label: "Easter", value: "Pacific/Easter" },
{ label: "Efate", value: "Pacific/Efate" },
{ label: "Fakaofo", value: "Pacific/Fakaofo" },
{ label: "Fiji", value: "Pacific/Fiji" },
{ label: "Funafuti", value: "Pacific/Funafuti" },
{ label: "Galapagos", value: "Pacific/Galapagos" },
{ label: "Gambier", value: "Pacific/Gambier" },
{ label: "Guadalcanal", value: "Pacific/Guadalcanal" },
{ label: "Guam", value: "Pacific/Guam" },
{ label: "Honolulu", value: "Pacific/Honolulu" },
{ label: "Kanton", value: "Pacific/Kanton" },
{ label: "Kiritimati", value: "Pacific/Kiritimati" },
{ label: "Kosrae", value: "Pacific/Kosrae" },
{ label: "Kwajalein", value: "Pacific/Kwajalein" },
{ label: "Majuro", value: "Pacific/Majuro" },
{ label: "Marquesas", value: "Pacific/Marquesas" },
{ label: "Midway", value: "Pacific/Midway" },
{ label: "Nauru", value: "Pacific/Nauru" },
{ label: "Niue", value: "Pacific/Niue" },
{ label: "Norfolk", value: "Pacific/Norfolk" },
{ label: "Noumea", value: "Pacific/Noumea" },
{ label: "Pago Pago", value: "Pacific/Pago_Pago" },
{ label: "Palau", value: "Pacific/Palau" },
{ label: "Pitcairn", value: "Pacific/Pitcairn" },
{ label: "Pohnpei", value: "Pacific/Pohnpei" },
{ label: "Port Moresby", value: "Pacific/Port_Moresby" },
{ label: "Rarotonga", value: "Pacific/Rarotonga" },
{ label: "Saipan", value: "Pacific/Saipan" },
{ label: "Tahiti", value: "Pacific/Tahiti" },
{ label: "Tarawa", value: "Pacific/Tarawa" },
{ label: "Tongatapu", value: "Pacific/Tongatapu" },
{ label: "Wake", value: "Pacific/Wake" },
{ label: "Wallis", value: "Pacific/Wallis" },
],
};
// Helper to get display label for a timezone value
export function getTimezoneLabel(value: string | undefined): string {
if (!value) return "UTC (default)";
return value;
}

View File

@@ -108,7 +108,8 @@ export const getLogType = (message: string): LogStyle => {
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
/⚠|⚠️/i.test(lowerMessage)
) {
return LOG_STYLES.warning;
}

View File

@@ -190,7 +190,9 @@ export const ShowProjects = () => {
Create and manage your projects
</CardDescription>
</CardHeader>
{(auth?.role === "owner" || auth?.canCreateProjects) && (
{(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canCreateProjects) && (
<div className="">
<HandleProject />
</div>

View File

@@ -1,5 +1,6 @@
import { Activity } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -7,7 +8,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowStorageActions } from "./show-storage-actions";
import { ShowTraefikActions } from "./show-traefik-actions";

View File

@@ -7,9 +7,12 @@ interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.user.get.useQuery(undefined, {
enabled: !serverId,
});
const { data, refetch } = api.settings.getWebServerSettings.useQuery(
undefined,
{
enabled: !serverId,
},
);
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
const enabled = serverId
? server?.enableDockerCleanup
: data?.user.enableDockerCleanup;
: data?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
try {
await mutateAsync({
enableDockerCleanup: checked,
serverId: serverId,
...(serverId && { serverId }),
} as {
enableDockerCleanup: boolean;
serverId?: string;
});
if (serverId) {
await refetchServer();

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, Pencil } from "lucide-react";
import { Pencil, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";

View File

@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => {
const { data } = serverId
const { data: serverData } = serverId
? api.server.one.useQuery(
{
serverId: serverId || "",
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId,
},
)
: api.user.getServerMetrics.useQuery();
: { data: null };
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const data = serverId ? serverData : webServerSettings;
const url = useUrl();

View File

@@ -1,17 +1,17 @@
import { format } from "date-fns";
import {
Clock,
Key,
KeyIcon,
Loader2,
MoreHorizontal,
ServerIcon,
Clock,
User,
Key,
Network,
Terminal,
Settings,
Pencil,
ServerIcon,
Settings,
Terminal,
Trash2,
User,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";

View File

@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => {
const { t } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery();
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation();
@@ -82,15 +82,15 @@ export const WebDomain = () => {
});
const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.user?.host || "";
const host = data?.host || "";
const hasChanged = domain !== host;
useEffect(() => {
if (data) {
form.reset({
domain: data?.user?.host || "",
certificateType: data?.user?.certificateType,
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
https: data?.user?.https || false,
domain: data?.host || "",
certificateType: data?.certificateType || "none",
letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.https || false,
});
}
}, [form, form.reset, data]);

View File

@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => {
const { t } = useTranslation("settings");
const { data } = api.user.get.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -53,7 +54,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.user.serverIp}
Server IP: {webServerSettings?.serverIp}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.user.get.useQuery();
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.user.update.useMutation();
api.settings.updateServerIp.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.user.serverIp || "",
serverIp: data?.serverIp || "",
},
resolver: zodResolver(schema),
});
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => {
if (data) {
form.reset({
serverIp: data.user.serverIp || "",
serverIp: data.serverIp || "",
});
}
}, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => {
if (!ip) return;
form.setValue("serverIp", ip);
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
})
.then(async () => {
toast.success("Server IP Updated");
await utils.user.get.invalidate();
await refetch();
setIsOpen(false);
})
.catch(() => {

View File

@@ -1,13 +1,13 @@
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import { Fragment } from "react";
import { ChevronDown } from "lucide-react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
DropdownMenu,

View File

@@ -0,0 +1,84 @@
import { MinusIcon, PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
export interface UnitConverter {
toValue: (raw: string | undefined) => number;
fromValue: (value: number) => string;
formatDisplay: (value: number) => string;
}
export const createConverter = (
multiplier: number,
formatDisplay: (value: number) => string,
): UnitConverter => ({
toValue: (raw) => {
if (!raw) return 0;
const value = Number.parseInt(raw, 10);
return Number.isNaN(value) ? 0 : value / multiplier;
},
fromValue: (value) =>
value <= 0 ? "" : String(Math.round(value * multiplier)),
formatDisplay,
});
interface NumberInputWithStepsProps {
value: string | undefined;
onChange: (value: string) => void;
placeholder: string;
step: number;
converter: UnitConverter;
}
export const NumberInputWithSteps = ({
value,
onChange,
placeholder,
step,
converter,
}: NumberInputWithStepsProps) => {
const numericValue = converter.toValue(value);
const displayValue = converter.formatDisplay(numericValue);
const handleIncrement = () =>
onChange(converter.fromValue(numericValue + step));
const handleDecrement = () =>
onChange(converter.fromValue(Math.max(0, numericValue - step)));
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleDecrement}
disabled={numericValue <= 0}
>
<MinusIcon className="h-4 w-4" />
</Button>
<Input
placeholder={placeholder}
value={value || ""}
onChange={(e) => onChange(e.target.value)}
className="text-center"
/>
<Button
type="button"
variant="outline"
size="icon"
className="h-9 w-9 shrink-0"
onClick={handleIncrement}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
{displayValue && (
<span className="text-xs text-muted-foreground text-center">
{displayValue}
</span>
)}
</div>
);
};

View File

@@ -0,0 +1,114 @@
CREATE TABLE "webServerSettings" (
"id" text PRIMARY KEY NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"https" boolean DEFAULT false NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
"logCleanupCron" text DEFAULT '0 0 * * *',
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now() NOT NULL
);
-- Migrate data from user table to webServerSettings
-- Get the owner user's data and insert into webServerSettings
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
COALESCE(
u."metricsConfig",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
) as "metricsConfig",
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
FROM "user" u
INNER JOIN "member" m ON u."id" = m."user_id"
WHERE m."role" = 'owner'
ORDER BY m."created_at" ASC
LIMIT 1;
-- If no owner found, create a default entry
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
NULL as "serverIp",
'none' as "certificateType",
false as "https",
NULL as "host",
NULL as "letsEncryptEmail",
NULL as "sshPrivateKey",
true as "enableDockerCleanup",
'0 0 * * *' as "logCleanupCron",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
false as "cleanupCacheApplications",
false as "cleanupCacheOnPreviews",
false as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
WHERE NOT EXISTS (
SELECT 1 FROM "webServerSettings"
);
--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";

File diff suppressed because it is too large Load Diff

View File

@@ -932,6 +932,13 @@
"when": 1765346573500,
"tag": "0132_clean_layla_miller",
"breakpoints": true
},
{
"idx": 133,
"version": "7",
"when": 1766301478005,
"tag": "0133_striped_the_order",
"breakpoints": true
}
]
}

View File

@@ -33,7 +33,7 @@
"docker:build:canary": "./docker/build.sh canary",
"docker:push:canary": "./docker/push.sh canary",
"version": "echo $(node -p \"require('./package.json').version\")",
"test": "vitest --config __test__/vitest.config.ts volume-backups",
"test": "vitest --config __test__/vitest.config.ts",
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
},
"dependencies": {
@@ -140,7 +140,6 @@
"react-i18next": "^15.5.2",
"react-markdown": "^9.1.0",
"recharts": "^2.15.3",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6",
"sonner": "^1.7.4",
"ssh2": "1.15.0",

View File

@@ -1,8 +1,8 @@
import {
findUserById,
getWebServerSettings,
IS_CLOUD,
setupWebMonitoring,
updateUser,
updateWebServerSettings,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure
.input(apiUpdateWebServerMonitoring)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
try {
if (IS_CLOUD) {
throw new TRPCError({
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud",
});
}
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup the monitoring",
});
}
await updateUser(user.id, {
await updateWebServerSettings({
metricsConfig: {
server: {
type: "Dokploy",
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
},
});
const currentServer = await setupWebMonitoring(user.id);
return currentServer;
await setupWebMonitoring();
const settings = await getWebServerSettings();
return settings;
} catch (error) {
throw error;
}

View File

@@ -68,6 +68,40 @@ export const aiRouter = createTRPCRouter({
{ headers: {} },
);
break;
case "perplexity":
// Perplexity doesn't have a /models endpoint, return hardcoded list
return [
{
id: "sonar-deep-research",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-reasoning",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar-pro",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
{
id: "sonar",
object: "model",
created: Date.now(),
owned_by: "perplexity",
},
] as Model[];
default:
if (!input.apiKey)
throw new TRPCError({

View File

@@ -17,8 +17,8 @@ import {
findGitProviderById,
findProjectById,
findServerById,
findUserById,
getComposeContainer,
getWebServerSettings,
IS_CLOUD,
loadServices,
randomizeComposeFile,
@@ -569,8 +569,7 @@ export const composeRouter = createTRPCRouter({
const template = await fetchTemplateFiles(input.id, input.baseUrl);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
const project = await findProjectById(environment.projectId);
@@ -579,6 +578,9 @@ export const composeRouter = createTRPCRouter({
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const projectName = slugify(`${project.name} ${input.id}`);
@@ -803,14 +805,16 @@ export const composeRouter = createTRPCRouter({
const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8",
);
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const templateData = JSON.parse(decodedData);
const config = parse(templateData.config) as CompleteTemplate;
@@ -880,14 +884,16 @@ export const composeRouter = createTRPCRouter({
await removeDomainById(domain.domainId);
}
const admin = await findUserById(ctx.user.ownerId);
let serverIp = admin.serverIp || "127.0.0.1";
let serverIp = "127.0.0.1";
if (compose.serverId) {
const server = await findServerById(compose.serverId);
serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
}
const templateData = JSON.parse(decodedData);

View File

@@ -9,6 +9,7 @@ import {
findPreviewDeploymentById,
findServerById,
generateTraefikMeDomain,
getWebServerSettings,
manageDomain,
removeDomain,
removeDomainById,
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
}),
canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
.query(async ({ input }) => {
if (input.serverId) {
const server = await findServerById(input.serverId);
return server.ipAddress;
}
return organization?.owner.serverIp;
const settings = await getWebServerSettings();
return settings?.serverIp || "";
}),
update: protectedProcedure

View File

@@ -8,6 +8,7 @@ import {
createSlackNotification,
createTelegramNotification,
findNotificationById,
getWebServerSettings,
IS_CLOUD,
removeNotificationById,
sendCustomNotification,
@@ -66,7 +67,6 @@ import {
apiUpdateTelegram,
notifications,
server,
user,
} from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
let organizationId = "";
let ServerName = "";
if (input.ServerType === "Dokploy") {
const result = await db
.select()
.from(user)
.where(
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
);
if (!result?.[0]?.id) {
const settings = await getWebServerSettings();
if (
!settings?.metricsConfig?.server?.token ||
settings.metricsConfig.server.token !== input.Token
) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Token not found",
});
}
organizationId = result?.[0]?.id;
// For Dokploy server type, we don't have a specific organizationId
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy";
} else {
const result = await db

View File

@@ -12,11 +12,11 @@ import {
DEFAULT_UPDATE_DATA,
execAsync,
findServerById,
findUserById,
getDokployImage,
getDokployImageTag,
getLogCleanupStatus,
getUpdateData,
getWebServerSettings,
IS_CLOUD,
parseRawConfig,
paths,
@@ -40,7 +40,7 @@ import {
updateLetsEncryptEmail,
updateServerById,
updateServerTraefik,
updateUser,
updateWebServerSettings,
writeConfig,
writeMainConfig,
writeTraefikConfigInPath,
@@ -77,6 +77,13 @@ import {
} from "../trpc";
export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
}),
saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => {
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.ownerId, {
await updateWebServerSettings({
sshPrivateKey: input.sshPrivateKey,
});
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
}),
assignDomainServer: adminProcedure
.input(apiAssignDomain)
.mutation(async ({ ctx, input }) => {
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const user = await updateUser(ctx.user.ownerId, {
const settings = await updateWebServerSettings({
host: input.host,
letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType,
https: input.https,
});
if (!user) {
if (!settings) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
message: "Web server settings not found",
});
}
updateServerTraefik(user, input.host);
updateServerTraefik(settings, input.host);
if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail);
}
return user;
return settings;
}),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
return true;
}
await updateUser(ctx.user.ownerId, {
await updateWebServerSettings({
sshPrivateKey: null,
});
return true;
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
}
}
} else if (!IS_CLOUD) {
const userUpdated = await updateUser(ctx.user.ownerId, {
const settingsUpdated = await updateWebServerSettings({
enableDockerCleanup: input.enableDockerCleanup,
});
if (userUpdated?.enableDockerCleanup) {
if (settingsUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId);
}),
getIp: protectedProcedure.query(async ({ ctx }) => {
getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return true;
return "";
}
const user = await findUserById(ctx.user.ownerId);
return user.serverIp;
const settings = await getWebServerSettings();
return settings?.serverIp || "";
}),
updateServerIp: adminProcedure
.input(
z.object({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
return settings;
}),
getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => {

View File

@@ -5,6 +5,7 @@ import {
findUserById,
getDokployUrl,
getUserByToken,
getWebServerSettings,
IS_CLOUD,
removeUserById,
sendEmailNotification,
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
}),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return {
serverIp: user.serverIp,
serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig,
metricsConfig: settings?.metricsConfig,
};
}),
remove: protectedProcedure

View File

@@ -75,7 +75,6 @@
"qrcode": "^1.5.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1",
"slugify": "^1.6.6",
"ssh2": "1.15.0",

View File

@@ -35,3 +35,4 @@ export * from "./ssh-key";
export * from "./user";
export * from "./utils";
export * from "./volume-backups";
export * from "./web-server-settings";

View File

@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(),
// Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
// authId: true,
})
.required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
.optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({
email: z
.string()
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
currentPassword: z.string().optional(),
name: z.string().optional(),
lastName: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
});

View File

@@ -0,0 +1,178 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { certificateType } from "./shared";
export const webServerSettings = pgTable("webServerSettings", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// Web Server Configuration
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
// Metrics Configuration
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const webServerSettingsRelations = relations(
webServerSettings,
() => ({}),
);
const createSchema = createInsertSchema(webServerSettings, {
id: z.string().min(1),
});
export const apiUpdateWebServerSettings = createSchema.partial().extend({
serverIp: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
https: z.boolean().optional(),
host: z.string().optional(),
letsEncryptEmail: z.string().email().optional().nullable(),
sshPrivateKey: z.string().optional(),
enableDockerCleanup: z.boolean().optional(),
logCleanupCron: z.string().optional().nullable(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
});
export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
https: z.boolean().optional(),
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiSaveSSHKey = z
.object({
sshPrivateKey: z.string(),
})
.required();
export const apiUpdateDockerCleanup = z.object({
enableDockerCleanup: z.boolean(),
serverId: z.string().optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -41,6 +41,7 @@ export * from "./services/settings";
export * from "./services/ssh-key";
export * from "./services/user";
export * from "./services/volume-backups";
export * from "./services/web-server-settings";
export * from "./setup/config-paths";
export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup";

View File

@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db";
import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user";
import {
getWebServerSettings,
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils";
@@ -35,22 +38,14 @@ const { handler, api } = betterAuth({
},
...(!IS_CLOUD && {
async trustedOrigins() {
const admin = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
with: {
user: true,
},
});
if (admin?.user) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
const settings = await getWebServerSettings();
if (!settings) {
return [];
}
return [];
return [
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
];
},
}),
emailVerification: {
@@ -122,7 +117,7 @@ const { handler, api } = betterAuth({
});
if (!IS_CLOUD) {
await updateUser(user.id, {
await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(),
});
}

View File

@@ -8,6 +8,7 @@ import {
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => {
const userResult = await db.query.user.findFirst({
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) {
return "https://app.dokploy.com";
}
const owner = await findOwner();
const settings = await getWebServerSettings();
if (owner.user.host) {
const protocol = owner.user.https ? "https" : "http";
return `${protocol}://${owner.user.host}`;
if (settings?.host) {
const protocol = settings?.https ? "https" : "http";
return `${protocol}://${settings?.host}`;
}
return `http://${owner.user.serverIp}:${process.env.PORT}`;
return `http://${settings?.serverIp}:${process.env.PORT}`;
};

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = "";
if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId);
ip = organization?.owner.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
if (serverId) {

View File

@@ -3,10 +3,10 @@ import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server";
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName,
});
}
const admin = await findUserById(userId);
const settings = await getWebServerSettings();
return generateRandomDomain({
serverIp: admin?.serverIp || "",
serverIp: settings?.serverIp || "",
projectName: appName,
});
};

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
}
if (!ip) {
const admin = await findUserById(userId);
ip = admin?.serverIp || "";
const settings = await getWebServerSettings();
ip = settings?.serverIp || "";
}
const slugIp = ip.replaceAll(".", "-");

View File

@@ -0,0 +1,44 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
}
};
export const setupWebMonitoring = async (userId: string) => {
const user = await findUserById(userId);
export const setupWebMonitoring = async () => {
const webServerSettings = await getWebServerSettings();
const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest";
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
const settings: ContainerCreateOptions = {
name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
Image: imageName,
HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
Name: "always",
},
PortBindings: {
[`${user?.metricsConfig?.server?.port}/tcp`]: [
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
{
HostPort: user?.metricsConfig?.server?.port.toString(),
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
},
],
},
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
// NetworkMode: "host",
},
ExposedPorts: {
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
},
};
const docker = await getRemoteDocker();

View File

@@ -1,6 +1,8 @@
import { paths } from "@dokploy/server/constants";
import { findOwner } from "@dokploy/server/services/admin";
import { updateUser } from "@dokploy/server/services/user";
import {
getWebServerSettings,
updateWebServerSettings,
} from "@dokploy/server/services/web-server-settings";
import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync";
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
}
});
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: cronExpression,
});
}
await updateWebServerSettings({
logCleanupCron: cronExpression,
});
return true;
} catch (error) {
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
}
// Update database
const owner = await findOwner();
if (owner) {
await updateUser(owner.user.id, {
logCleanupCron: null,
});
}
await updateWebServerSettings({
logCleanupCron: null,
});
return true;
} catch (error) {
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean;
cronExpression: string | null;
}> => {
const owner = await findOwner();
const cronExpression = owner?.user.logCleanupCron ?? null;
const settings = await getWebServerSettings();
const cronExpression = settings?.logCleanupCron ?? null;
return {
enabled: cronExpression !== null,
cronExpression,

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule";
import { db } from "../../db/index";
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
return;
}
if (admin?.user?.enableDockerCleanup) {
const webServerSettings = await getWebServerSettings();
if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
}
}
if (admin?.user?.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
await startLogCleanup(admin.user.logCleanupCron);
if (webServerSettings?.logCleanupCron) {
console.log(
"Starting log requests cleanup",
webServerSettings.logCleanupCron,
);
await startLogCleanup(webServerSettings.logCleanupCron);
}
};

View File

@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune`;
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
}
return command;

View File

@@ -117,7 +117,7 @@ const getRegistryCommands = (
): string => {
return `
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
echo "❌ DockerHub Failed" ;
exit 1;
}

View File

@@ -146,17 +146,17 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
};
/**
* Docker commands passed through this method are held during Docker's build or pull process.
* Docker commands sent using this method are held in a hold when Docker is busy.
*
* https://github.com/Dokploy/dokploy/pull/3064
* https://github.com/fir4tozden
*/
export const dockerSafeExec = (exec: string) => `CHECK_INTERVAL=10
export const dockerSafeExec = (exec: string) => `
CHECK_INTERVAL=10
echo "Preparing for execution..."
while true; do
PROCESSES=$(ps aux | grep -E "docker build|docker pull" | grep -v grep)
PROCESSES=$(ps aux | grep -E "^.*docker [A-Za-z]" | grep -v grep)
if [ -z "$PROCESSES" ]; then
echo "Docker is idle. Starting execution..."
@@ -169,14 +169,15 @@ done
${exec}
echo "Execution completed."`;
echo "Execution completed."
`;
const cleanupCommands = {
containers: "docker container prune --force",
images: "docker image prune --all --force",
volumes: "docker volume prune --all --force",
builders: "docker builder prune --all --force",
system: "docker system prune --all --force",
volumes: "docker volume prune --all --force",
};
export const cleanupContainers = async (serverId?: string) => {
@@ -257,24 +258,48 @@ export const cleanupSystem = async (serverId?: string) => {
}
};
/**
* Volume cleanup should always be performed manually by the user. The reason is that during automatic cleanup, a volume may be deleted due to a stopped container, which is a dangerous situation.
*
* https://github.com/Dokploy/dokploy/pull/3267
*/
const excludedCleanupAllCommands: (keyof typeof cleanupCommands)[] = [
"volumes",
];
export const cleanupAll = async (serverId?: string) => {
await cleanupContainers(serverId);
await cleanupImages(serverId);
await cleanupBuilders(serverId);
await cleanupSystem(serverId);
for (const [key, command] of Object.entries(cleanupCommands) as [
keyof typeof cleanupCommands,
string,
][]) {
if (excludedCleanupAllCommands.includes(key)) continue;
try {
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(dockerSafeExec(command));
}
} catch {}
}
};
export const cleanupAllBackground = async (serverId?: string) => {
Promise.allSettled(
Object.values(cleanupCommands).map(async (command) => {
try {
(
Object.entries(cleanupCommands) as [
keyof typeof cleanupCommands,
string,
][]
)
.filter(([key]) => !excludedCleanupAllCommands.includes(key))
.map(async ([, command]) => {
if (serverId) {
await execAsyncRemote(serverId, dockerSafeExec(command));
} else {
await execAsync(dockerSafeExec(command));
}
} catch (error) {}
}),
}),
)
.then((results) => {
const failed = results.filter((r) => r.status === "rejected");

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user";
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
import { parse, stringify } from "yaml";
import {
loadOrCreateConfig,
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types";
export const updateServerTraefik = (
user: User | null,
settings: typeof webServerSettings.$inferSelect | null,
newHost: string | null,
) => {
const { https, certificateType } = user || {};
const { https, certificateType } = settings || {};
const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName);

12
pnpm-lock.yaml generated
View File

@@ -406,9 +406,6 @@ importers:
recharts:
specifier: ^2.15.3
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -732,9 +729,6 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
rotating-file-stream:
specifier: 3.2.3
version: 3.2.3
shell-quote:
specifier: ^1.8.1
version: 1.8.2
@@ -7064,10 +7058,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
rotating-file-stream@3.2.3:
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
engines: {node: '>=14.0'}
rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
@@ -14660,8 +14650,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3
rotating-file-stream@3.2.3: {}
rou3@0.5.1: {}
run-parallel@1.2.0: