diff --git a/.env.example b/.env.example
index 460e575..76a74b6 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1 @@
-VITE_API_URL=http://localhost:8080
+VITE_API_URL=http://localhost:8090/api
diff --git a/package-lock.json b/package-lock.json
index 6bc5b9e..4a01fa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,8 @@
"primevue": "^3.50.0",
"vue": "^3.4.21",
"vue-codemirror": "^6.1.1",
- "vue-router": "^4.3.0"
+ "vue-router": "^4.3.0",
+ "yaml": "^2.8.2"
},
"devDependencies": {
"@pinia/testing": "^0.1.6",
@@ -5391,6 +5392,21 @@
"node": ">=12"
}
},
+ "node_modules/yaml": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
+ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 005ce5c..a757144 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
"primevue": "^3.50.0",
"vue": "^3.4.21",
"vue-codemirror": "^6.1.1",
- "vue-router": "^4.3.0"
+ "vue-router": "^4.3.0",
+ "yaml": "^2.8.2"
},
"devDependencies": {
"@pinia/testing": "^0.1.6",
diff --git a/src/components/NewDeploymentModal.vue b/src/components/NewDeploymentModal.vue
index f2dca77..76f0212 100644
--- a/src/components/NewDeploymentModal.vue
+++ b/src/components/NewDeploymentModal.vue
@@ -754,24 +754,38 @@
Password
*
-
+
+
+
+
@@ -1345,6 +1359,7 @@ import BaseModal from "@/components/base/BaseModal.vue";
import { deploymentsApi, templatesApi, settingsApi, containersApi, composeApi, credentialsApi } from "@/services/api";
import type { RegistryCredential } from "@/types";
import { useNotificationsStore } from "@/stores/notifications";
+import { validateComposeYaml } from "@/utils/yaml";
interface TemplateMount {
id: string;
@@ -1407,6 +1422,8 @@ const extensions = shallowRef([yaml(), oneDark]);
const deploymentMode = ref<"" | "easy" | "compose" | "image">("");
const showRegistryPassword = ref(false);
+const showDbPassword = ref(false);
+const showDbRootPassword = ref(false);
const existingCredentials = ref([]);
const loadingCredentials = ref(false);
@@ -2331,6 +2348,12 @@ const validate = () => {
if (!form.composeContent.trim()) {
errors.composeContent = "Compose configuration is required";
valid = false;
+ } else {
+ const yamlResult = validateComposeYaml(form.composeContent);
+ if (!yamlResult.valid) {
+ errors.composeContent = yamlResult.error;
+ valid = false;
+ }
}
return valid;
diff --git a/src/utils/yaml.ts b/src/utils/yaml.ts
new file mode 100644
index 0000000..53a7bf0
--- /dev/null
+++ b/src/utils/yaml.ts
@@ -0,0 +1,21 @@
+import { parse } from "yaml";
+
+export type ComposeYamlValidation = { valid: true } | { valid: false; error: string };
+
+/**
+ * This function validates that a string is valid YAML. It is used before sending compose content
+ * to the API (e.g.during a deployment update or create).
+ */
+export function validateComposeYaml(content: string): ComposeYamlValidation {
+ const trimmed = content.trim();
+ if (!trimmed) {
+ return { valid: false, error: "Compose content is empty" };
+ }
+ try {
+ parse(trimmed);
+ return { valid: true };
+ } catch (err: unknown) {
+ const message = err instanceof Error ? err.message : "Invalid YAML syntax";
+ return { valid: false, error: message };
+ }
+}
diff --git a/src/views/DeploymentDetailView.vue b/src/views/DeploymentDetailView.vue
index dbdd9c9..9d0bc7c 100644
--- a/src/views/DeploymentDetailView.vue
+++ b/src/views/DeploymentDetailView.vue
@@ -1391,8 +1391,9 @@
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Codemirror } from "vue-codemirror";
-import { yaml } from "@codemirror/lang-yaml";
+import { yaml as yamlLang } from "@codemirror/lang-yaml";
import { oneDark } from "@codemirror/theme-one-dark";
+import { validateComposeYaml } from "@/utils/yaml";
import {
deploymentsApi,
proxyApi,
@@ -1526,7 +1527,7 @@ const composeFilename = ref("docker-compose.yml");
const isEditingConfig = ref(false);
const serviceConfig = ref("");
const isEditingServiceConfig = ref(false);
-const configExtensions = [yaml(), oneDark];
+const configExtensions = [yamlLang(), oneDark];
const activeConfigTab = ref<"compose" | "service">("compose");
const showOperationModal = ref(false);
@@ -2331,6 +2332,11 @@ const cancelServiceConfigEdit = () => {
};
const saveConfig = async () => {
+ const result = validateComposeYaml(composeConfig.value);
+ if (!result.valid) {
+ notifications.error("Invalid YAML", result.error);
+ return;
+ }
try {
await deploymentsApi.update(route.params.name as string, {
compose_content: composeConfig.value,