mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 20:55:21 +02:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b45b795e8 | ||
|
|
d187b52e09 | ||
|
|
5f13679a97 | ||
|
|
415327c246 | ||
|
|
12b8f8a4fd | ||
|
|
fea3ec9a6f | ||
|
|
2976bb5cf7 | ||
|
|
092afbe1fa | ||
|
|
a32e7e0041 | ||
|
|
ee9edd7ff4 | ||
|
|
3799aeab74 | ||
|
|
4f6eb51c06 | ||
|
|
7cf898dcf6 | ||
|
|
1c83919408 | ||
|
|
b230687c8a | ||
|
|
b499cefebc | ||
|
|
a04a4c05ea | ||
|
|
e7dc05d031 | ||
|
|
9544b2ace3 | ||
|
|
85632fd0c2 | ||
|
|
31cdae1b72 | ||
|
|
702af64444 | ||
|
|
eef27b67c2 | ||
|
|
70f50dd8bc | ||
|
|
3e25b97b99 | ||
|
|
22927c2716 | ||
|
|
8ab4ee8e0e | ||
|
|
99aa34f27e | ||
|
|
48be8544cf | ||
|
|
ee411ac74f | ||
|
|
c233ddb520 | ||
|
|
0cfe87cb72 | ||
|
|
7998b296a2 | ||
|
|
9e20f66bf5 | ||
|
|
1dc943ef5b | ||
|
|
0f63fdac4e | ||
|
|
ec8c516aa3 | ||
|
|
58be8f91c0 | ||
|
|
2036ac3dc8 | ||
|
|
17f83f746a | ||
|
|
bcd1cbe920 | ||
|
|
3993263615 | ||
|
|
97bd4de4f1 | ||
|
|
2fc29ff7c8 | ||
|
|
4a74016b52 |
BIN
.github/sponsors/awesome.png
vendored
Normal file
BIN
.github/sponsors/awesome.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -80,7 +80,9 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||
</a>
|
||||
|
||||
<a href="https://awesome.tools/" target="_blank">
|
||||
<img src=".github/sponsors/awesome.png" width="200" height="150" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Premium Supporters 🥇 -->
|
||||
|
||||
209
apps/dokploy/__test__/cluster/upload.test.ts
Normal file
209
apps/dokploy/__test__/cluster/upload.test.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import type { Registry } from "@dokploy/server";
|
||||
import { getRegistryTag } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("getRegistryTag", () => {
|
||||
// Helper to create a mock registry
|
||||
const createMockRegistry = (overrides: Partial<Registry> = {}): Registry => {
|
||||
return {
|
||||
registryId: "test-registry-id",
|
||||
registryName: "Test Registry",
|
||||
username: "myuser",
|
||||
password: "test-password",
|
||||
registryUrl: "docker.io",
|
||||
registryType: "cloud",
|
||||
imagePrefix: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
organizationId: "test-org-id",
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
describe("with username (no imagePrefix)", () => {
|
||||
it("should handle simple image name without tag", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle image name with tag", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/myuser/nginx:latest");
|
||||
});
|
||||
|
||||
it("should handle image name with username already present (no duplication)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle image name with username and tag already present", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo:latest");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo:latest");
|
||||
});
|
||||
|
||||
it("should handle complex image name with username", () => {
|
||||
const registry = createMockRegistry({ username: "siumauricio" });
|
||||
const result = getRegistryTag(
|
||||
registry,
|
||||
"siumauricio/app-parse-multi-byte-port-e32uh7",
|
||||
);
|
||||
// Should not duplicate username
|
||||
expect(result).toBe(
|
||||
"docker.io/siumauricio/app-parse-multi-byte-port-e32uh7",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle image name with different username (should not duplicate)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "otheruser/myprivaterepo");
|
||||
expect(result).toBe("docker.io/myuser/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle image name with full registry URL (no username)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "docker.io/nginx");
|
||||
// Should add username since imageName doesn't have one
|
||||
expect(result).toBe("docker.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle image name with custom registry URL and username", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "ghcr.io/myuser/repo");
|
||||
// Should not duplicate username even if registry URL is different
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
|
||||
it("should handle image name with custom registry URL (different username)", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "ghcr.io/otheruser/repo");
|
||||
// Should use registry username, not the one in imageName
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with imagePrefix", () => {
|
||||
it("should use imagePrefix instead of username", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("docker.io/myorg/nginx");
|
||||
});
|
||||
|
||||
it("should use imagePrefix with image tag", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx:latest");
|
||||
expect(result).toBe("docker.io/myorg/nginx:latest");
|
||||
});
|
||||
|
||||
it("should handle imagePrefix with username already in image name", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
expect(result).toBe("docker.io/myorg/myprivaterepo");
|
||||
});
|
||||
|
||||
it("should handle imagePrefix matching image name prefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myorg/myprivaterepo");
|
||||
// Should not duplicate prefix
|
||||
expect(result).toBe("docker.io/myorg/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("without registryUrl", () => {
|
||||
it("should work without registryUrl", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("myuser/nginx");
|
||||
});
|
||||
|
||||
it("should work without registryUrl with imagePrefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("myorg/nginx");
|
||||
});
|
||||
|
||||
it("should handle username already present without registryUrl", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("myuser/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with custom registryUrl", () => {
|
||||
it("should handle custom registry URL", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("ghcr.io/myuser/nginx");
|
||||
});
|
||||
|
||||
it("should handle custom registry URL with imagePrefix", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
imagePrefix: "myorg",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "nginx");
|
||||
expect(result).toBe("ghcr.io/myorg/nginx");
|
||||
});
|
||||
|
||||
it("should handle custom registry URL with username already present", () => {
|
||||
const registry = createMockRegistry({
|
||||
username: "myuser",
|
||||
registryUrl: "ghcr.io",
|
||||
});
|
||||
const result = getRegistryTag(registry, "myuser/myprivaterepo");
|
||||
// Should not duplicate username
|
||||
expect(result).toBe("ghcr.io/myuser/myprivaterepo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty image name", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "");
|
||||
expect(result).toBe("docker.io/myuser/");
|
||||
});
|
||||
|
||||
it("should handle image name with multiple slashes", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "org/suborg/repo");
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
|
||||
it("should handle image name with username at different position", () => {
|
||||
const registry = createMockRegistry({ username: "myuser" });
|
||||
const result = getRegistryTag(registry, "org/myuser/repo");
|
||||
expect(result).toBe("docker.io/myuser/repo");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ const baseApp: ApplicationNested = {
|
||||
railpackVersion: "0.2.2",
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
herokuVersion: "",
|
||||
giteaBranch: "",
|
||||
buildServerId: "",
|
||||
@@ -67,6 +68,7 @@ const baseApp: ApplicationNested = {
|
||||
previewWildcard: "",
|
||||
environment: {
|
||||
env: "",
|
||||
isDefault: false,
|
||||
environmentId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -54,4 +54,22 @@ describe("processLogs", () => {
|
||||
const result = parseRawConfig(entryWithWhitespace);
|
||||
expect(result.data).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should filter out Dokploy dashboard requests", () => {
|
||||
const dokployDashboardEntry = `{"ClientAddr":"172.71.187.131:9485","ClientHost":"172.71.187.131","ClientPort":"9485","ClientUsername":"-","DownstreamContentSize":14550,"DownstreamStatus":200,"Duration":57681682,"OriginContentSize":14550,"OriginDuration":57612242,"OriginStatus":200,"Overhead":69440,"RequestAddr":"hostinger.dokploy.com","RequestContentSize":0,"RequestCount":20142,"RequestHost":"hostinger.dokploy.com","RequestMethod":"GET","RequestPath":"/_next/data/cb_zzI4Rp9G7Q7djrFKh0/en/dashboard/traefik.json","RequestPort":"-","RequestProtocol":"HTTP/2.0","RequestScheme":"https","RetryAttempts":0,"RouterName":"dokploy-router-app-secure@file","ServiceAddr":"dokploy:3000","ServiceName":"dokploy-service-app@file","ServiceURL":"http://dokploy:3000","StartLocal":"2025-12-10T05:10:41.957755949Z","StartUTC":"2025-12-10T05:10:41.957755949Z","TLSCipher":"TLS_AES_128_GCM_SHA256","TLSVersion":"1.3","entryPointName":"websecure","level":"info","msg":"","time":"2025-12-10T05:10:42Z"}`;
|
||||
|
||||
// Test with only Dokploy dashboard entry - should be filtered out
|
||||
const resultOnlyDokploy = parseRawConfig(dokployDashboardEntry);
|
||||
expect(resultOnlyDokploy.data).toHaveLength(0);
|
||||
expect(resultOnlyDokploy.totalCount).toBe(0);
|
||||
|
||||
// Test with mixed entries - Dokploy should be filtered, others should remain
|
||||
const mixedEntries = `${dokployDashboardEntry}\n${sampleLogEntry}`;
|
||||
const resultMixed = parseRawConfig(mixedEntries);
|
||||
expect(resultMixed.data).toHaveLength(1);
|
||||
expect(resultMixed.totalCount).toBe(1);
|
||||
expect(resultMixed.data[0]?.ServiceName).not.toBe(
|
||||
"dokploy-service-app@file",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ const baseApp: ApplicationNested = {
|
||||
rollbackActive: false,
|
||||
applicationId: "",
|
||||
previewLabels: [],
|
||||
createEnvFile: true,
|
||||
herokuVersion: "",
|
||||
giteaRepository: "",
|
||||
giteaOwner: "",
|
||||
@@ -49,6 +50,7 @@ const baseApp: ApplicationNested = {
|
||||
environmentId: "",
|
||||
environment: {
|
||||
env: "",
|
||||
isDefault: false,
|
||||
environmentId: "",
|
||||
name: "",
|
||||
createdAt: "",
|
||||
|
||||
@@ -38,10 +38,31 @@ interface Props {
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
const schema = z.object({
|
||||
buildServerId: z.string().min(1, "Build server is required"),
|
||||
buildRegistryId: z.string().min(1, "Build registry is required"),
|
||||
});
|
||||
const schema = z
|
||||
.object({
|
||||
buildServerId: z.string().optional(),
|
||||
buildRegistryId: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// Both empty/none is valid
|
||||
const buildServerIsNone =
|
||||
!data.buildServerId || data.buildServerId === "none";
|
||||
const buildRegistryIsNone =
|
||||
!data.buildRegistryId || data.buildRegistryId === "none";
|
||||
|
||||
// Both should be either filled or empty
|
||||
if (buildServerIsNone && buildRegistryIsNone) return true;
|
||||
if (!buildServerIsNone && !buildRegistryIsNone) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Both Build Server and Build Registry must be selected together, or both set to None",
|
||||
path: ["buildServerId"], // Show error on buildServerId field
|
||||
},
|
||||
);
|
||||
|
||||
type Schema = z.infer<typeof schema>;
|
||||
|
||||
@@ -121,6 +142,11 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
|
||||
container starts running.
|
||||
</AlertBlock>
|
||||
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> Build Server and Build Registry must be
|
||||
configured together. You can either select both or set both to None.
|
||||
</AlertBlock>
|
||||
|
||||
{!registries || registries.length === 0 ? (
|
||||
<AlertBlock type="warning">
|
||||
You need to add at least one registry to use build servers. Please
|
||||
@@ -147,7 +173,13 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Build Server</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
// If setting to "none", also reset build registry to "none"
|
||||
if (value === "none") {
|
||||
form.setValue("buildRegistryId", "none");
|
||||
}
|
||||
}}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<FormControl>
|
||||
@@ -197,7 +229,13 @@ export const ShowBuildServer = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Build Registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
// If setting to "none", also reset build server to "none"
|
||||
if (value === "none") {
|
||||
form.setValue("buildServerId", "none");
|
||||
}
|
||||
}}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<FormControl>
|
||||
|
||||
@@ -208,6 +208,8 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
const certificateType = form.watch("certificateType");
|
||||
const https = form.watch("https");
|
||||
const domainType = form.watch("domainType");
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -502,6 +504,13 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
to make your traefik.me domain work.
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -5,14 +5,23 @@ import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/ui/form";
|
||||
import { Secrets } from "@/components/ui/secrets";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const addEnvironmentSchema = z.object({
|
||||
env: z.string(),
|
||||
buildArgs: z.string(),
|
||||
buildSecrets: z.string(),
|
||||
createEnvFile: z.boolean(),
|
||||
});
|
||||
|
||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||
@@ -39,6 +48,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: "",
|
||||
buildArgs: "",
|
||||
buildSecrets: "",
|
||||
createEnvFile: true,
|
||||
},
|
||||
resolver: zodResolver(addEnvironmentSchema),
|
||||
});
|
||||
@@ -47,10 +57,12 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
const currentEnv = form.watch("env");
|
||||
const currentBuildArgs = form.watch("buildArgs");
|
||||
const currentBuildSecrets = form.watch("buildSecrets");
|
||||
const currentCreateEnvFile = form.watch("createEnvFile");
|
||||
const hasChanges =
|
||||
currentEnv !== (data?.env || "") ||
|
||||
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||
currentBuildSecrets !== (data?.buildSecrets || "") ||
|
||||
currentCreateEnvFile !== (data?.createEnvFile ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
@@ -58,6 +70,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: data.env || "",
|
||||
buildArgs: data.buildArgs || "",
|
||||
buildSecrets: data.buildSecrets || "",
|
||||
createEnvFile: data.createEnvFile ?? true,
|
||||
});
|
||||
}
|
||||
}, [data, form]);
|
||||
@@ -67,6 +80,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: formData.env,
|
||||
buildArgs: formData.buildArgs,
|
||||
buildSecrets: formData.buildSecrets,
|
||||
createEnvFile: formData.createEnvFile,
|
||||
applicationId,
|
||||
})
|
||||
.then(async () => {
|
||||
@@ -83,6 +97,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
env: data?.env || "",
|
||||
buildArgs: data?.buildArgs || "",
|
||||
buildSecrets: data?.buildSecrets || "",
|
||||
createEnvFile: data?.createEnvFile ?? true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -167,6 +182,31 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
placeholder="NPM_TOKEN=xyz"
|
||||
/>
|
||||
)}
|
||||
{data?.buildType === "dockerfile" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="createEnvFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environment File</FormLabel>
|
||||
<FormDescription>
|
||||
When enabled, an .env file will be created in the same
|
||||
directory as your Dockerfile during the build process.
|
||||
Disable this if you don't want to generate an environment
|
||||
file.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
{hasChanges && (
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
|
||||
@@ -86,6 +86,9 @@ export const AddPreviewDomain = ({
|
||||
resolver: zodResolver(domain),
|
||||
});
|
||||
|
||||
const host = form.watch("host");
|
||||
const isTraefikMeDomain = host?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
@@ -157,6 +160,13 @@ export const AddPreviewDomain = ({
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP
|
||||
service and does not support SSL/HTTPS. HTTPS and
|
||||
certificate options will not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<FormLabel>Host</FormLabel>
|
||||
<div className="flex gap-2">
|
||||
<FormControl>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -100,6 +101,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
});
|
||||
|
||||
const previewHttps = form.watch("previewHttps");
|
||||
const wildcardDomain = form.watch("wildcardDomain");
|
||||
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
|
||||
|
||||
useEffect(() => {
|
||||
setIsEnabled(data?.isPreviewDeploymentsActive || false);
|
||||
@@ -168,6 +171,13 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
{isTraefikMeDomain && (
|
||||
<AlertBlock type="info">
|
||||
<strong>Note:</strong> traefik.me is a public HTTP service and
|
||||
does not support SSL/HTTPS. HTTPS and certificate options will
|
||||
not have any effect.
|
||||
</AlertBlock>
|
||||
)}
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
|
||||
@@ -102,7 +102,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to create environment");
|
||||
toast.error(
|
||||
`Failed to create environment: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +125,9 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error) {
|
||||
toast.error("Failed to update environment");
|
||||
toast.error(
|
||||
`Failed to update environment: ${error instanceof Error ? error.message : error}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -140,15 +144,18 @@ export const AdvancedEnvironmentSelector = ({
|
||||
setIsDeleteDialogOpen(false);
|
||||
setSelectedEnvironment(null);
|
||||
|
||||
// Redirect to production if we deleted the current environment
|
||||
// Redirect to first available environment if we deleted the current environment
|
||||
if (selectedEnvironment.environmentId === currentEnvironmentId) {
|
||||
const productionEnv = environments?.find(
|
||||
(env) => env.name === "production",
|
||||
const firstEnv = environments?.find(
|
||||
(env) => env.environmentId !== selectedEnvironment.environmentId,
|
||||
);
|
||||
if (productionEnv) {
|
||||
if (firstEnv) {
|
||||
router.push(
|
||||
`/dashboard/project/${projectId}/environment/${productionEnv.environmentId}`,
|
||||
`/dashboard/project/${projectId}/environment/${firstEnv.environmentId}`,
|
||||
);
|
||||
} else {
|
||||
// No other environments, redirect to project page
|
||||
router.push(`/dashboard/project/${projectId}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -239,8 +246,8 @@ export const AdvancedEnvironmentSelector = ({
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{environment.name !== "production" && (
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
{!environment.isDefault && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -252,22 +259,21 @@ export const AdvancedEnvironmentSelector = ({
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
{canDeleteEnvironments && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDeleteDialog(environment);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
{canDeleteEnvironments && !environment.isDefault && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openDeleteDialog(environment);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -286,13 +286,17 @@ export const ShowProjects = () => {
|
||||
)
|
||||
.some(Boolean);
|
||||
|
||||
const productionEnvironment = project?.environments.find(
|
||||
(env) => env.isDefault,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={project.projectId}
|
||||
className="w-full lg:max-w-md"
|
||||
>
|
||||
<Link
|
||||
href={`/dashboard/project/${project.projectId}/environment/${project?.environments?.[0]?.environmentId}`}
|
||||
href={`/dashboard/project/${project.projectId}/environment/${productionEnvironment?.environmentId}`}
|
||||
>
|
||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||
{haveServicesWithDomains ? (
|
||||
|
||||
@@ -89,24 +89,26 @@ export const SearchCommand = () => {
|
||||
<CommandGroup heading={"Projects"}>
|
||||
<CommandList>
|
||||
{data?.map((project) => {
|
||||
const productionEnvironment = project.environments.find(
|
||||
(environment) => environment.name === "production",
|
||||
);
|
||||
// Find default environment, or fall back to first environment
|
||||
const defaultEnvironment =
|
||||
project.environments.find(
|
||||
(environment) => environment.isDefault,
|
||||
) || project?.environments?.[0];
|
||||
|
||||
if (!productionEnvironment) return null;
|
||||
if (!defaultEnvironment) return null;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={project.projectId}
|
||||
onSelect={() => {
|
||||
router.push(
|
||||
`/dashboard/project/${project.projectId}/environment/${productionEnvironment!.environmentId}`,
|
||||
`/dashboard/project/${project.projectId}/environment/${defaultEnvironment.environmentId}`,
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||
{project.name} / {productionEnvironment!.name}
|
||||
{project.name} / {defaultEnvironment.name}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -45,7 +45,34 @@ const AddRegistrySchema = z.object({
|
||||
password: z.string().min(1, {
|
||||
message: "Password is required",
|
||||
}),
|
||||
registryUrl: z.string(),
|
||||
registryUrl: z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
// If empty or undefined, skip validation (field is optional)
|
||||
if (!val || val.trim().length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Validate that it's a valid hostname (no protocol, no path, optional port)
|
||||
// Valid formats: example.com, registry.example.com, [::1], example.com:5000
|
||||
// Invalid: https://example.com, example.com/path
|
||||
const trimmed = val.trim();
|
||||
// Check for protocol or path - these are not allowed
|
||||
if (/^https?:\/\//i.test(trimmed) || trimmed.includes("/")) {
|
||||
return false;
|
||||
}
|
||||
// Basic hostname validation: allow alphanumeric, dots, hyphens, underscores, and IPv6 in brackets
|
||||
// Allow optional port at the end
|
||||
const hostnameRegex =
|
||||
/^(?:\[[^\]]+\]|[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,253}[a-zA-Z0-9])?)(?::\d+)?$/;
|
||||
return hostnameRegex.test(trimmed);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid registry URL. Please enter only the hostname (e.g., example.com or registry.example.com). Do not include protocol (https://) or paths.",
|
||||
},
|
||||
),
|
||||
imagePrefix: z.string(),
|
||||
serverId: z.string().optional(),
|
||||
});
|
||||
@@ -99,6 +126,9 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
const registryName = form.watch("registryName");
|
||||
const imagePrefix = form.watch("imagePrefix");
|
||||
const serverId = form.watch("serverId");
|
||||
const selectedServer = servers?.find(
|
||||
(server) => server.serverId === serverId,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (registry) {
|
||||
@@ -125,7 +155,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
password: data.password,
|
||||
registryName: data.registryName,
|
||||
username: data.username,
|
||||
registryUrl: data.registryUrl,
|
||||
registryUrl: data.registryUrl || "",
|
||||
registryType: "cloud",
|
||||
imagePrefix: data.imagePrefix,
|
||||
serverId: data.serverId,
|
||||
@@ -261,6 +291,10 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry URL</FormLabel>
|
||||
<FormDescription>
|
||||
Enter only the hostname (e.g.,
|
||||
aws_account_id.dkr.ecr.us-west-2.amazonaws.com).
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="aws_account_id.dkr.ecr.us-west-2.amazonaws.com"
|
||||
@@ -282,8 +316,40 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Server {!isCloud && "(Optional)"}</FormLabel>
|
||||
<FormDescription>
|
||||
Select a server to test the registry. this will run the
|
||||
following command on the server
|
||||
{!isCloud ? (
|
||||
<>
|
||||
{serverId && serverId !== "none" && selectedServer ? (
|
||||
<>
|
||||
Authentication will be performed on{" "}
|
||||
<strong>{selectedServer.name}</strong>. This
|
||||
registry will be available on this server.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Choose where to authenticate with the registry. By
|
||||
default, authentication occurs on the Dokploy
|
||||
server. Select a specific server to authenticate
|
||||
from that server instead.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{serverId && serverId !== "none" && selectedServer ? (
|
||||
<>
|
||||
Authentication will be performed on{" "}
|
||||
<strong>{selectedServer.name}</strong>. This
|
||||
registry will be available on this server.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Select a server to authenticate with the registry.
|
||||
The authentication will be performed from the
|
||||
selected server.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Select
|
||||
@@ -345,7 +411,7 @@ export const HandleRegistry = ({ registryId }: Props) => {
|
||||
await testRegistry({
|
||||
username: username,
|
||||
password: password,
|
||||
registryUrl: registryUrl,
|
||||
registryUrl: registryUrl || "",
|
||||
registryName: registryName,
|
||||
registryType: "cloud",
|
||||
imagePrefix: imagePrefix,
|
||||
|
||||
@@ -369,6 +369,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
webhookUrl: notification.lark?.webhookUrl,
|
||||
name: notification.name,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
} else if (notification.notificationType === "custom") {
|
||||
@@ -388,6 +389,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
)
|
||||
: [],
|
||||
name: notification.name,
|
||||
volumeBackup: notification.volumeBackup,
|
||||
dockerCleanup: notification.dockerCleanup,
|
||||
serverThreshold: notification.serverThreshold,
|
||||
});
|
||||
@@ -522,6 +524,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
webhookUrl: data.webhookUrl,
|
||||
name: data.name,
|
||||
dockerCleanup: dockerCleanup,
|
||||
@@ -547,6 +550,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
volumeBackup: volumeBackup,
|
||||
endpoint: data.endpoint,
|
||||
headers: headersRecord,
|
||||
name: data.name,
|
||||
|
||||
@@ -173,7 +173,7 @@ export const ShowStorageActions = ({ serverId }: Props) => {
|
||||
serverId: serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Cleaned all");
|
||||
toast.success("Cleaning in progress... Please wait");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error cleaning all");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { api } from "@/utils/api";
|
||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
||||
import { ChatwootWidget } from "../shared/ChatwootWidget";
|
||||
import { HubSpotWidget } from "../shared/HubSpotWidget";
|
||||
import Page from "./side";
|
||||
|
||||
interface Props {
|
||||
@@ -25,7 +25,9 @@ export const DashboardLayout = ({ children }: Props) => {
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && isUserSubscribed === true && (
|
||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
</>
|
||||
)}
|
||||
|
||||
{haveRootAccess === true && <ImpersonationBar />}
|
||||
|
||||
14
apps/dokploy/components/shared/HubSpotWidget.tsx
Normal file
14
apps/dokploy/components/shared/HubSpotWidget.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Script from "next/script";
|
||||
|
||||
export const HubSpotWidget = () => {
|
||||
return (
|
||||
<Script
|
||||
id="hs-script-loader"
|
||||
type="text/javascript"
|
||||
src="//js-eu1.hs-scripts.com/147033433.js"
|
||||
strategy="lazyOnload"
|
||||
async
|
||||
defer
|
||||
/>
|
||||
);
|
||||
};
|
||||
1
apps/dokploy/drizzle/0131_volatile_beast.sql
Normal file
1
apps/dokploy/drizzle/0131_volatile_beast.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "createEnvFile" boolean DEFAULT true NOT NULL;
|
||||
4
apps/dokploy/drizzle/0132_clean_layla_miller.sql
Normal file
4
apps/dokploy/drizzle/0132_clean_layla_miller.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "environment" ADD COLUMN "isDefault" boolean DEFAULT false NOT NULL;
|
||||
|
||||
-- Set isDefault to true for existing production environments
|
||||
UPDATE "environment" SET "isDefault" = true WHERE "name" = 'production';
|
||||
6928
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
6928
apps/dokploy/drizzle/meta/0131_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6935
apps/dokploy/drizzle/meta/0132_snapshot.json
Normal file
6935
apps/dokploy/drizzle/meta/0132_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -918,6 +918,20 @@
|
||||
"when": 1765167657813,
|
||||
"tag": "0130_perpetual_screwball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 131,
|
||||
"version": "7",
|
||||
"when": 1765342621312,
|
||||
"tag": "0131_volatile_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 132,
|
||||
"version": "7",
|
||||
"when": 1765346573500,
|
||||
"tag": "0132_clean_layla_miller",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.26.0",
|
||||
"version": "v0.26.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -13,7 +13,6 @@
|
||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
|
||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||
"dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json",
|
||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||
@@ -118,7 +117,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"next": "^16.0.7",
|
||||
"next": "^16.0.10",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
|
||||
@@ -81,41 +81,34 @@ export default async function handler(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookImageName) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Image Name Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// If webhook provides image information, validate it matches the configured image
|
||||
// If webhook doesn't provide image information, fall back to using the configured image (backward compatibility)
|
||||
if (webhookImageName) {
|
||||
// Validate image name matches
|
||||
if (webhookImageName !== applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate image name matches
|
||||
if (webhookImageName !== applicationImageName) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Name (${applicationImageName}) doesn't match request event payload Image Name (${webhookImageName}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Application Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!webhookDockerTag) {
|
||||
res.status(301).json({
|
||||
message: "Webhook Docker Tag Not Found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (webhookDockerTag !== applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||
});
|
||||
return;
|
||||
if (webhookDockerTag) {
|
||||
if (webhookDockerTag !== applicationDockerTag) {
|
||||
res.status(301).json({
|
||||
message: `Application Image Tag (${applicationDockerTag}) doesn't match request event payload Image Tag (${webhookDockerTag}).`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If webhook doesn't provide image info, we'll use the configured image (old behavior)
|
||||
} else if (sourceType === "github") {
|
||||
const normalizedCommits = req.body?.commits?.flatMap(
|
||||
(commit: any) => commit.modified,
|
||||
@@ -249,17 +242,19 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
jobData.serverId = application.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
} else {
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Error deploying Application", error });
|
||||
return;
|
||||
|
||||
@@ -179,17 +179,19 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && composeResult.serverId) {
|
||||
jobData.serverId = composeResult.serverId;
|
||||
await deploy(jobData);
|
||||
return true;
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
} else {
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
await myQueue.add(
|
||||
"deployments",
|
||||
{ ...jobData },
|
||||
{
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(400).json({ message: "Error deploying Compose", error });
|
||||
return;
|
||||
|
||||
@@ -128,7 +128,9 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -165,7 +167,9 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -246,7 +250,9 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -291,7 +297,9 @@ export default async function handler(
|
||||
}
|
||||
if (IS_CLOUD && composeApp.serverId) {
|
||||
jobData.serverId = composeApp.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -491,7 +499,9 @@ export default async function handler(
|
||||
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
await myQueue.add(
|
||||
|
||||
@@ -336,7 +336,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
jobData.serverId = application.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -365,6 +367,7 @@ export const applicationRouter = createTRPCRouter({
|
||||
env: input.env,
|
||||
buildArgs: input.buildArgs,
|
||||
buildSecrets: input.buildSecrets,
|
||||
createEnvFile: input.createEnvFile,
|
||||
});
|
||||
return true;
|
||||
}),
|
||||
@@ -700,7 +703,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
};
|
||||
if (IS_CLOUD && application.serverId) {
|
||||
jobData.serverId = application.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -812,7 +817,9 @@ export const applicationRouter = createTRPCRouter({
|
||||
};
|
||||
if (IS_CLOUD && app.serverId) {
|
||||
jobData.serverId = app.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -417,7 +417,9 @@ export const composeRouter = createTRPCRouter({
|
||||
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
jobData.serverId = compose.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
@@ -453,7 +455,9 @@ export const composeRouter = createTRPCRouter({
|
||||
};
|
||||
if (IS_CLOUD && compose.serverId) {
|
||||
jobData.serverId = compose.serverId;
|
||||
await deploy(jobData);
|
||||
deploy(jobData).catch((error) => {
|
||||
console.error("Background deployment failed:", error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
await myQueue.add(
|
||||
|
||||
@@ -66,10 +66,12 @@ export const environmentRouter = createTRPCRouter({
|
||||
if (input.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
message:
|
||||
"You cannot create a environment with the name 'production'",
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to create environments with any name, including "production"
|
||||
const environment = await createEnvironment(input);
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
@@ -206,6 +208,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent deletion of the default environment
|
||||
if (environment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot delete the default environment",
|
||||
});
|
||||
}
|
||||
|
||||
// Check environment deletion permission
|
||||
await checkEnvironmentDeletionPermission(
|
||||
ctx.user.id,
|
||||
@@ -243,13 +253,7 @@ export const environmentRouter = createTRPCRouter({
|
||||
try {
|
||||
const { environmentId, ...updateData } = input;
|
||||
|
||||
if (updateData.name === "production") {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Environment name cannot be production",
|
||||
});
|
||||
}
|
||||
|
||||
// Allow users to rename environments to any name, including "production"
|
||||
if (ctx.user.role === "member") {
|
||||
await checkEnvironmentAccess(
|
||||
ctx.user.id,
|
||||
@@ -259,6 +263,14 @@ export const environmentRouter = createTRPCRouter({
|
||||
);
|
||||
}
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
|
||||
// Prevent renaming the default environment, but allow updating env and description
|
||||
if (currentEnvironment.isDefault && updateData.name !== undefined) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot rename the default environment",
|
||||
});
|
||||
}
|
||||
if (
|
||||
currentEnvironment.project.organizationId !==
|
||||
ctx.session.activeOrganizationId
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
checkGPUStatus,
|
||||
checkPortInUse,
|
||||
cleanupAll,
|
||||
cleanupAllBackground,
|
||||
cleanupBuilders,
|
||||
cleanupContainers,
|
||||
cleanupImages,
|
||||
@@ -193,9 +194,10 @@ export const settingsRouter = createTRPCRouter({
|
||||
cleanAll: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanupAll(input?.serverId);
|
||||
// Execute cleanup in background and return immediately to avoid gateway timeouts
|
||||
const result = await cleanupAllBackground(input?.serverId);
|
||||
|
||||
return true;
|
||||
return result;
|
||||
}),
|
||||
cleanMonitoring: adminProcedure.mutation(async () => {
|
||||
if (IS_CLOUD) {
|
||||
|
||||
@@ -247,3 +247,18 @@
|
||||
.cm-lineWrapping {
|
||||
@apply font-mono;
|
||||
}
|
||||
|
||||
/* HubSpot Widget - Force light color-scheme to prevent white background */
|
||||
#hubspot-messages-iframe-container,
|
||||
#hubspot-messages-iframe-container * {
|
||||
background-color: transparent !important;
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
#hubspot-messages-iframe-container .hs-shadow-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#hubspot-conversations-iframe {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"scripts": {
|
||||
"dokploy:setup": "pnpm --filter=dokploy run setup",
|
||||
"dokploy:dev": "pnpm --filter=dokploy run dev",
|
||||
"dokploy:dev:turbopack": "pnpm --filter=dokploy run dev-turbopack",
|
||||
"dokploy:build": "pnpm --filter=dokploy run build",
|
||||
"dokploy:start": "pnpm --filter=dokploy run start",
|
||||
"test": "pnpm --filter=dokploy run test",
|
||||
|
||||
@@ -181,6 +181,7 @@ export const applications = pgTable("application", {
|
||||
herokuVersion: text("herokuVersion").default("24"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
isStaticSpa: boolean("isStaticSpa"),
|
||||
createEnvFile: boolean("createEnvFile").notNull().default(true),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@@ -332,6 +333,7 @@ const createSchema = createInsertSchema(applications, {
|
||||
herokuVersion: z.string().optional(),
|
||||
publishDirectory: z.string().optional(),
|
||||
isStaticSpa: z.boolean().optional(),
|
||||
createEnvFile: z.boolean().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@@ -501,6 +503,7 @@ export const apiSaveEnvironmentVariables = createSchema
|
||||
env: true,
|
||||
buildArgs: true,
|
||||
buildSecrets: true,
|
||||
createEnvFile: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
@@ -26,6 +26,7 @@ export const environments = pgTable("environment", {
|
||||
projectId: text("projectId")
|
||||
.notNull()
|
||||
.references(() => projects.projectId, { onDelete: "cascade" }),
|
||||
isDefault: boolean("isDefault").notNull().default(false),
|
||||
});
|
||||
|
||||
export const environmentRelations = relations(
|
||||
@@ -69,9 +70,14 @@ export const apiRemoveEnvironment = createSchema
|
||||
})
|
||||
.required();
|
||||
|
||||
export const apiUpdateEnvironment = createSchema.partial().extend({
|
||||
environmentId: z.string().min(1),
|
||||
});
|
||||
export const apiUpdateEnvironment = createSchema
|
||||
.partial()
|
||||
.extend({
|
||||
environmentId: z.string().min(1),
|
||||
})
|
||||
.omit({
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
export const apiDuplicateEnvironment = createSchema
|
||||
.pick({
|
||||
|
||||
@@ -390,6 +390,7 @@ export const apiCreateCustom = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -416,6 +417,7 @@ export const apiCreateLark = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
|
||||
@@ -103,10 +103,10 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
|
||||
export const deleteEnvironment = async (environmentId: string) => {
|
||||
const currentEnvironment = await findEnvironmentById(environmentId);
|
||||
if (currentEnvironment.name === "production") {
|
||||
if (currentEnvironment.isDefault) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot delete the production environment",
|
||||
message: "You cannot delete the default environment",
|
||||
});
|
||||
}
|
||||
const deletedEnvironment = await db
|
||||
@@ -162,9 +162,23 @@ export const duplicateEnvironment = async (
|
||||
};
|
||||
|
||||
export const createProductionEnvironment = async (projectId: string) => {
|
||||
return createEnvironment({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
});
|
||||
const newEnvironment = await db
|
||||
.insert(environments)
|
||||
.values({
|
||||
name: "production",
|
||||
description: "Production environment",
|
||||
projectId,
|
||||
isDefault: true,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
if (!newEnvironment) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error creating the production environment",
|
||||
});
|
||||
}
|
||||
|
||||
return newEnvironment;
|
||||
};
|
||||
|
||||
@@ -653,6 +653,7 @@ export const updateCustomNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -772,6 +773,7 @@ export const updateLarkNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
|
||||
@@ -99,6 +99,11 @@ export function parseRawConfig(
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
// Filter out Dokploy dashboard requests
|
||||
parsedLogs = parsedLogs.filter(
|
||||
(log) => log.ServiceName !== "dokploy-service-app@file",
|
||||
);
|
||||
|
||||
// Apply date range filter if provided
|
||||
if (dateRange?.start || dateRange?.end) {
|
||||
parsedLogs = parsedLogs.filter((log) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const initCronJobs = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (admin.user.enableDockerCleanup) {
|
||||
if (admin?.user?.enableDockerCleanup) {
|
||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
@@ -82,7 +82,7 @@ export const initCronJobs = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (admin?.user.logCleanupCron) {
|
||||
if (admin?.user?.logCleanupCron) {
|
||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
||||
await startLogCleanup(admin.user.logCleanupCron);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createEnvFileCommand } from "./utils";
|
||||
|
||||
export const getDockerCommand = (application: ApplicationNested) => {
|
||||
const {
|
||||
@@ -18,6 +19,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
buildSecrets,
|
||||
dockerBuildStage,
|
||||
cleanCache,
|
||||
createEnvFile,
|
||||
} = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
|
||||
@@ -60,6 +62,21 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
.map(([key, value]) => `${key}=${quote([value])}`)
|
||||
.join(" ");
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
Also respect the createEnvFile flag.
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory && createEnvFile) {
|
||||
command += createEnvFileCommand(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
}
|
||||
|
||||
for (const key in secrets) {
|
||||
// Although buildx is smart enough to know we may be referring to an environment variable name,
|
||||
// we still make sure it doesn't fall back to `type=file`.
|
||||
@@ -67,7 +84,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
const command = `
|
||||
command += `
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
|
||||
@@ -74,11 +74,40 @@ export const uploadImageRemoteCommand = async (
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Extract the repository name from imageName by taking the last part after '/'
|
||||
* Examples:
|
||||
* - "nginx" -> "nginx"
|
||||
* - "nginx:latest" -> "nginx:latest"
|
||||
* - "myuser/myrepo" -> "myrepo"
|
||||
* - "myuser/myrepo:tag" -> "myrepo:tag"
|
||||
* - "docker.io/myuser/myrepo" -> "myrepo"
|
||||
*/
|
||||
const extractRepositoryName = (imageName: string): string => {
|
||||
const lastSlashIndex = imageName.lastIndexOf("/");
|
||||
|
||||
// If no '/', return the imageName as is
|
||||
if (lastSlashIndex === -1) {
|
||||
return imageName;
|
||||
}
|
||||
|
||||
// Extract everything after the last '/'
|
||||
return imageName.substring(lastSlashIndex + 1);
|
||||
};
|
||||
|
||||
export const getRegistryTag = (registry: Registry, imageName: string) => {
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
return imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
|
||||
// Extract the repository name (last part after '/')
|
||||
const repositoryName = extractRepositoryName(imageName);
|
||||
|
||||
// Build the final tag using registry's username/prefix
|
||||
const targetPrefix = imagePrefix || username;
|
||||
const finalRegistry = registryUrl || "";
|
||||
|
||||
return finalRegistry
|
||||
? `${finalRegistry}/${targetPrefix}/${repositoryName}`
|
||||
: `${targetPrefix}/${repositoryName}`;
|
||||
};
|
||||
|
||||
const getRegistryCommands = (
|
||||
|
||||
@@ -171,9 +171,17 @@ ${exec}
|
||||
|
||||
echo "Execution completed."`;
|
||||
|
||||
const cleanupCommands = {
|
||||
containers: "docker container prune --force",
|
||||
images: "docker image 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) => {
|
||||
try {
|
||||
const command = "docker container prune --force";
|
||||
const command = cleanupCommands.containers;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
@@ -189,7 +197,7 @@ export const cleanupContainers = async (serverId?: string) => {
|
||||
|
||||
export const cleanupImages = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker image prune --all --force";
|
||||
const command = cleanupCommands.images;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
@@ -203,7 +211,7 @@ export const cleanupImages = async (serverId?: string) => {
|
||||
|
||||
export const cleanupVolumes = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume prune --all --force";
|
||||
const command = cleanupCommands.volumes;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
@@ -219,7 +227,7 @@ export const cleanupVolumes = async (serverId?: string) => {
|
||||
|
||||
export const cleanupBuilders = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker builder prune --all --force";
|
||||
const command = cleanupCommands.builders;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
@@ -235,7 +243,7 @@ export const cleanupBuilders = async (serverId?: string) => {
|
||||
|
||||
export const cleanupSystem = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker system prune --all --force";
|
||||
const command = cleanupCommands.system;
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
@@ -256,6 +264,34 @@ export const cleanupAll = async (serverId?: string) => {
|
||||
await cleanupSystem(serverId);
|
||||
};
|
||||
|
||||
export const cleanupAllBackground = async (serverId?: string) => {
|
||||
Promise.allSettled(
|
||||
Object.values(cleanupCommands).map(async (command) => {
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else {
|
||||
await execAsync(dockerSafeExec(command));
|
||||
}
|
||||
} catch (error) {}
|
||||
}),
|
||||
)
|
||||
.then((results) => {
|
||||
const failed = results.filter((r) => r.status === "rejected");
|
||||
if (failed.length > 0) {
|
||||
console.error(`Docker cleanup: ${failed.length} operations failed`);
|
||||
} else {
|
||||
console.log("Docker cleanup completed successfully");
|
||||
}
|
||||
})
|
||||
.catch((error) => console.error("Error in cleanup:", error));
|
||||
|
||||
return {
|
||||
status: "scheduled",
|
||||
message: "Docker cleanup has been initiated in the background",
|
||||
};
|
||||
};
|
||||
|
||||
export const startService = async (appName: string) => {
|
||||
try {
|
||||
await execAsync(`docker service scale ${appName}=1 `);
|
||||
|
||||
@@ -162,6 +162,7 @@ export const readMonitoringConfig = async (readAll = false) => {
|
||||
trimmed.endsWith("}")
|
||||
) {
|
||||
const log = JSON.parse(trimmed);
|
||||
// Exclude Dokploy service app and Dashboard requests
|
||||
if (log.ServiceName !== "dokploy-service-app@file") {
|
||||
content += `${line}\n`;
|
||||
validCount++;
|
||||
|
||||
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@@ -62,7 +62,7 @@ importers:
|
||||
version: 4.7.10
|
||||
inngest:
|
||||
specifier: 3.40.1
|
||||
version: 3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3)
|
||||
version: 3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3)
|
||||
pino:
|
||||
specifier: 9.4.0
|
||||
version: 9.4.0
|
||||
@@ -237,7 +237,7 @@ importers:
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/next':
|
||||
specifier: ^10.45.2
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/react-query':
|
||||
specifier: ^10.45.2
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -338,17 +338,17 @@ importers:
|
||||
specifier: 3.3.11
|
||||
version: 3.3.11
|
||||
next:
|
||||
specifier: ^16.0.7
|
||||
version: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^16.0.10
|
||||
version: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-i18next:
|
||||
specifier: ^15.4.2
|
||||
version: 15.4.2(i18next@23.16.8)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0)
|
||||
version: 15.4.2(i18next@23.16.8)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0)
|
||||
next-themes:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 0.2.1(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nextjs-toploader:
|
||||
specifier: ^3.9.17
|
||||
version: 3.9.17(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 3.9.17(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
node-os-utils:
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
@@ -1962,53 +1962,53 @@ packages:
|
||||
peerDependencies:
|
||||
redis: ^4.7.0
|
||||
|
||||
'@next/env@16.0.7':
|
||||
resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==}
|
||||
'@next/env@16.0.10':
|
||||
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==}
|
||||
'@next/swc-darwin-arm64@16.0.10':
|
||||
resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==}
|
||||
'@next/swc-darwin-x64@16.0.10':
|
||||
resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==}
|
||||
'@next/swc-linux-arm64-gnu@16.0.10':
|
||||
resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==}
|
||||
'@next/swc-linux-arm64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==}
|
||||
'@next/swc-linux-x64-gnu@16.0.10':
|
||||
resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==}
|
||||
'@next/swc-linux-x64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==}
|
||||
'@next/swc-win32-arm64-msvc@16.0.10':
|
||||
resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==}
|
||||
'@next/swc-win32-x64-msvc@16.0.10':
|
||||
resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -6257,8 +6257,8 @@ packages:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
next@16.0.7:
|
||||
resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==}
|
||||
next@16.0.10:
|
||||
resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -8777,30 +8777,30 @@ snapshots:
|
||||
async-await-queue: 2.1.4
|
||||
redis: 4.7.0
|
||||
|
||||
'@next/env@16.0.7': {}
|
||||
'@next/env@16.0.10': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
'@next/swc-darwin-arm64@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
'@next/swc-darwin-x64@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
'@next/swc-linux-arm64-gnu@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
'@next/swc-linux-arm64-musl@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
'@next/swc-linux-x64-gnu@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
'@next/swc-linux-x64-musl@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
'@next/swc-win32-arm64-msvc@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
'@next/swc-win32-x64-msvc@16.0.10':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@0.6.0': {}
|
||||
@@ -11244,13 +11244,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@trpc/server': 10.45.2
|
||||
|
||||
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/client': 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/server': 10.45.2
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
@@ -12951,7 +12951,7 @@ snapshots:
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3):
|
||||
inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.6.3
|
||||
'@inngest/ai': 0.1.5
|
||||
@@ -12978,7 +12978,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
h3: 1.15.3
|
||||
hono: 4.7.10
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -13789,7 +13789,7 @@ snapshots:
|
||||
|
||||
neotraverse@0.6.18: {}
|
||||
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0):
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.3
|
||||
'@types/hoist-non-react-statics': 3.3.6
|
||||
@@ -13797,19 +13797,19 @@ snapshots:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
i18next: 23.16.8
|
||||
i18next-fs-backend: 2.6.0
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-i18next: 15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
|
||||
next-themes@0.2.1(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
next-themes@0.2.1(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@next/env': 16.0.7
|
||||
'@next/env': 16.0.10
|
||||
'@swc/helpers': 0.5.15
|
||||
caniuse-lite: 1.0.30001718
|
||||
postcss: 8.4.31
|
||||
@@ -13817,23 +13817,23 @@ snapshots:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.6(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.0.7
|
||||
'@next/swc-darwin-x64': 16.0.7
|
||||
'@next/swc-linux-arm64-gnu': 16.0.7
|
||||
'@next/swc-linux-arm64-musl': 16.0.7
|
||||
'@next/swc-linux-x64-gnu': 16.0.7
|
||||
'@next/swc-linux-x64-musl': 16.0.7
|
||||
'@next/swc-win32-arm64-msvc': 16.0.7
|
||||
'@next/swc-win32-x64-msvc': 16.0.7
|
||||
'@next/swc-darwin-arm64': 16.0.10
|
||||
'@next/swc-darwin-x64': 16.0.10
|
||||
'@next/swc-linux-arm64-gnu': 16.0.10
|
||||
'@next/swc-linux-arm64-musl': 16.0.10
|
||||
'@next/swc-linux-x64-gnu': 16.0.10
|
||||
'@next/swc-linux-x64-musl': 16.0.10
|
||||
'@next/swc-win32-arm64-msvc': 16.0.10
|
||||
'@next/swc-win32-x64-msvc': 16.0.10
|
||||
'@opentelemetry/api': 1.9.0
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
nextjs-toploader@3.9.17(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
nextjs-toploader@3.9.17(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nprogress: 0.2.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
|
||||
Reference in New Issue
Block a user